├── .gitattributes ├── .gitignore ├── LICENSE.md ├── Markdown Previewer ├── Base.lproj │ └── PreviewViewController.xib ├── Common.swift ├── Info.plist ├── PreviewViewController.swift ├── Previewer.entitlements └── licences.txt ├── Markdown Thumbnailer ├── Info.plist ├── ThumbnailProvider.swift ├── Thumbnailer.entitlements └── licences.txt ├── PreviewMarkdown.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── PreviewMarkdown.xcscheme │ ├── Previewer.xcscheme │ └── Thumbnailer.xcscheme ├── PreviewMarkdown ├── AppDelegate.swift ├── Base.lproj │ └── MainMenu.xib ├── Constants.swift ├── GenericColorExtensions.swift ├── GenericExtensions.swift ├── Info.plist ├── PMFont.swift ├── PMFontExtensions.swift ├── PreviewMarkdown.entitlements └── sample.md ├── PreviewMarkdownTests ├── Info.plist └── PreviewMarkdownTests.swift ├── README.md ├── RenderDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── MainMenu.xib └── RenderDemo.entitlements └── qr-code.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT LICENCE 2 | 3 | ### PreviewMarkdown Copyright © 2023 Tony Smith (@smittytone) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | SEE ALSO [PREVIEWER LICENCE](./Previewer/licences.txt) AND [THUMBNAILER LICENCE](./Thumbnailer/licences.txt). 24 | -------------------------------------------------------------------------------- /Markdown Previewer/Base.lproj/PreviewViewController.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 | 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 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Markdown Previewer/Common.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Common.swift 3 | * Code common to Previewer and Thumbnailer 4 | * 5 | * Created by Tony Smith on 23/09/2020. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Foundation 11 | import SwiftyMarkdown 12 | import Yaml 13 | import AppKit 14 | 15 | 16 | // FROM 1.4.0 17 | // Implement as a class 18 | class Common: NSObject { 19 | 20 | // MARK: - Public Properties 21 | 22 | var doShowLightBackground: Bool = false 23 | var doShowTag: Bool = true 24 | 25 | // MARK: - Private Properties 26 | 27 | private var doIndentScalars: Bool = true 28 | private var doShowYaml: Bool = false 29 | private var fontSize: CGFloat = CGFloat(BUFFOON_CONSTANTS.PREVIEW_FONT_SIZE) 30 | 31 | // FROM 1.3.0 32 | // Front Matter string attributes... 33 | private var keyAtts: [NSAttributedString.Key:Any] = [:] 34 | private var valAtts: [NSAttributedString.Key:Any] = [:] 35 | 36 | // Front Matter rendering artefacts... 37 | private var hr: NSAttributedString = NSAttributedString.init(string: "") 38 | private var newLine: NSAttributedString = NSAttributedString.init(string: "") 39 | 40 | // FROM 1.4.0 41 | private var codeColourHex: String = BUFFOON_CONSTANTS.CODE_COLOUR_HEX 42 | private var headColourHex: String = BUFFOON_CONSTANTS.HEAD_COLOUR_HEX 43 | private var linkColourHex: String = BUFFOON_CONSTANTS.LINK_COLOUR_HEX 44 | private var codeFontName: String = BUFFOON_CONSTANTS.CODE_FONT_NAME 45 | private var bodyFontName: String = BUFFOON_CONSTANTS.BODY_FONT_NAME 46 | 47 | // FROM 1.5.0 48 | private var lineSpacing: CGFloat = BUFFOON_CONSTANTS.BASE_LINE_SPACING 49 | private var quoteColourHex: String = BUFFOON_CONSTANTS.LINK_COLOUR_HEX 50 | 51 | /* 52 | Replace the following string with your own team ID. This is used to 53 | identify the app suite and so share preferences set by the main app with 54 | the previewer and thumbnailer extensions. 55 | */ 56 | private var appSuiteName: String = MNU_SECRETS.PID + BUFFOON_CONSTANTS.SUITE_NAME 57 | 58 | // MARK: - Lifecycle Functions 59 | 60 | init(_ isThumbnail: Bool) { 61 | 62 | super.init() 63 | 64 | // Load in the user's preferred values, or set defaults 65 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 66 | self.fontSize = CGFloat(isThumbnail 67 | ? BUFFOON_CONSTANTS.THUMBNAIL_FONT_SIZE 68 | : defaults.float(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_SIZE)) 69 | 70 | self.doShowLightBackground = defaults.bool(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_USE_LIGHT) 71 | self.doShowYaml = defaults.bool(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_SHOW_YAML) 72 | 73 | // FROM 1.4.0 74 | self.codeColourHex = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_COLOUR) ?? BUFFOON_CONSTANTS.CODE_COLOUR_HEX 75 | self.headColourHex = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_HEAD_COLOUR) ?? BUFFOON_CONSTANTS.HEAD_COLOUR_HEX 76 | self.linkColourHex = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINK_COLOUR) ?? BUFFOON_CONSTANTS.LINK_COLOUR_HEX 77 | self.codeFontName = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_FONT_NAME) ?? BUFFOON_CONSTANTS.CODE_FONT_NAME 78 | self.bodyFontName = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_NAME) ?? BUFFOON_CONSTANTS.BODY_FONT_NAME 79 | self.doShowTag = defaults.bool(forKey: BUFFOON_CONSTANTS.PREFS_IDS.THUMB_SHOW_TAG) 80 | 81 | // FROM 1.5.0 82 | self.lineSpacing = CGFloat(defaults.float(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINE_SPACE)) 83 | self.quoteColourHex = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_QUOTE_COLOUR) ?? BUFFOON_CONSTANTS.QUOTE_COLOUR_HEX 84 | } 85 | 86 | // Just in case the above block reads in zero values 87 | // NOTE The other values CAN be zero 88 | if self.fontSize < BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[0] || 89 | self.fontSize > BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS.count - 1] { 90 | self.fontSize = CGFloat(BUFFOON_CONSTANTS.PREVIEW_FONT_SIZE) 91 | } 92 | 93 | // FROM 1.3.0 94 | // Set the front matter key:value fonts and sizes 95 | var font: NSFont 96 | if let otherFont = NSFont.init(name: self.codeFontName, size: self.fontSize) { 97 | font = otherFont 98 | } else { 99 | // This should not be hit, but just in case... 100 | font = NSFont.systemFont(ofSize: self.fontSize) 101 | } 102 | 103 | self.keyAtts = [ 104 | .foregroundColor: NSColor.hexToColour(self.codeColourHex), 105 | .font: font 106 | ] 107 | 108 | self.valAtts = [ 109 | .foregroundColor: (isThumbnail || self.doShowLightBackground ? NSColor.black : NSColor.labelColor), 110 | .font: font 111 | ] 112 | 113 | self.hr = NSAttributedString(string: "\n\u{00A0}\u{0009}\u{00A0}\n\n", 114 | attributes: [.strikethroughStyle: NSUnderlineStyle.thick.rawValue, 115 | .strikethroughColor: (self.doShowLightBackground ? NSColor.black : NSColor.white)]) 116 | 117 | self.newLine = NSAttributedString.init(string: "\n", 118 | attributes: self.valAtts) 119 | } 120 | 121 | 122 | // MARK: - The Primary Function 123 | 124 | /** 125 | Use SwiftyMarkdown to render the input markdown. 126 | 127 | - parameters: 128 | - markdownString: The markdown file contents. 129 | - isThumbnail: Are we rendering for a thumbnail (`true`) or a preview (`false`)? 130 | 131 | - returns: The rendered markdown as an NSAttributedString. 132 | */ 133 | func getAttributedString(_ markdownString: String, _ isThumbnail: Bool) -> NSAttributedString { 134 | 135 | let swiftyMarkdown: SwiftyMarkdown = SwiftyMarkdown.init(string: "") 136 | setSwiftStyles(swiftyMarkdown, isThumbnail) 137 | var processed: String = processCodeTags(markdownString) 138 | processed = convertSpaces(processed) 139 | processed = processSymbols(processed) 140 | processed = processCheckboxes(processed) 141 | 142 | // Process the markdown string 143 | var output: NSMutableAttributedString = NSMutableAttributedString.init(attributedString: swiftyMarkdown.attributedString(from: processed)) 144 | 145 | // FROM 1.5.0 146 | // Adjust the line spacing of previews 147 | if !isThumbnail { 148 | let spacedParaStyle: NSMutableParagraphStyle = NSMutableParagraphStyle.init() 149 | // NOTE Default line spacing value, ie. for single line spacing, is zero 150 | spacedParaStyle.lineSpacing = (self.lineSpacing - 1.0) * self.fontSize 151 | output.addParaStyle(with: spacedParaStyle) 152 | } 153 | 154 | // FROM 1.3.0 155 | // Render YAML front matter if requested by the user, and we're not 156 | // rendering a thumbnail image (this is for previews only) 157 | if !isThumbnail && self.doShowYaml { 158 | // Extract the front matter 159 | let frontMatter: String = getFrontMatter(markdownString, #"^(-)+"#, #"^(\.)+"#) 160 | if frontMatter.count > 0 { 161 | // Only attempt to render the front matter if there is any 162 | do { 163 | let yaml: Yaml = try Yaml.load(frontMatter) 164 | 165 | // Assemble the front matter string 166 | let renderedString: NSMutableAttributedString = NSMutableAttributedString.init(string: "", 167 | attributes: self.valAtts) 168 | 169 | // Initial line 170 | renderedString.append(self.hr) 171 | 172 | // Render the YAML to NSAttributedString 173 | if let yamlString = renderYaml(yaml, 0, false) { 174 | renderedString.append(yamlString) 175 | } 176 | 177 | // Add a line after the front matter 178 | renderedString.append(self.hr) 179 | 180 | // Add in the orignal rendered markdown and then set the 181 | // output string to the combined string 182 | renderedString.append(output) 183 | output = renderedString 184 | } catch { 185 | // No YAML to render, or mis-formatted 186 | // No YAML to render, or the YAML was mis-formatted 187 | // Get the error as reported by YamlSwift 188 | let yamlErr: Yaml.ResultError = error as! Yaml.ResultError 189 | var yamlErrString: String 190 | switch(yamlErr) { 191 | case .message(let s): 192 | yamlErrString = s ?? "unknown" 193 | } 194 | 195 | // Assemble the error string 196 | let errorString: NSMutableAttributedString = NSMutableAttributedString.init(string: "Could not render the YAML. Error: " + yamlErrString, 197 | attributes: self.keyAtts) 198 | 199 | // Should we include the raw text? 200 | // At least the user can see the data this way 201 | #if DEBUG 202 | errorString.append(self.hr) 203 | errorString.append(NSMutableAttributedString.init(string: frontMatter, 204 | attributes: self.valAtts)) 205 | #endif 206 | 207 | errorString.append(self.hr) 208 | errorString.append(output) 209 | output = errorString 210 | } 211 | } 212 | } 213 | 214 | // FROM 1.3.0 215 | // Guard against non-trapped errors 216 | if output.length == 0 { 217 | return NSAttributedString.init(string: "No valid Markdown to render.", 218 | attributes: self.keyAtts) 219 | } 220 | 221 | // Return the rendered NSAttributedString to Previewer or Thumbnailer 222 | return output as NSAttributedString 223 | } 224 | 225 | 226 | // MARK: - SwiftyMarkdown Rendering Support Functions 227 | 228 | func processSymbols(_ base: String) -> String { 229 | 230 | // FROM 1.1.0 231 | // Find and and replace any HTML symbol markup 232 | // Processed here because SwiftyMarkdown doesn't handle this markup 233 | 234 | let codes: [String] = [""", "&", "⁄", "<", ">", "‘", "’", "“", "”", "•", "–", "—", "™", " ", "¡", "¢", "£", "¥", "§", "©", "ª", "®", "°", "º", "±", "²", "³", "µ", "¶", "·", "¿", "÷", "€", "†", "‡"] 235 | let symbols: [String] = ["\"", "&", "/", "<", ">", "‘", "’", "“", "”", "•", "-", "—", "™", " ", "¡", "¢", "£", "¥", "§", "©", "ª", "®", "º", "º", "±", "²", "³", "µ", "¶", "·", "¿", "÷", "€", "†", "‡"] 236 | 237 | // Look for HTML symbol code '&...;' substrings, eg. '²' 238 | let pattern = #"&[a-zA-Z]+[1-9]*;"# 239 | var result = base 240 | var range = base.range(of: pattern, options: .regularExpression) 241 | 242 | while range != nil { 243 | // Get the symbol from the 'symbols' array that has the same index 244 | // as the symbol code from the 'codes' array 245 | var repText = "" 246 | let find = String(result[range!]) 247 | if codes.contains(find) { 248 | repText = symbols[codes.firstIndex(of: find)!] 249 | } 250 | 251 | // Swap out the HTML symbol code for the actual symbol 252 | result = result.replacingCharacters(in: range!, with: repText) 253 | 254 | // Get the next occurence of the pattern ready for the 'while...' check 255 | range = result.range(of: pattern, options: .regularExpression) 256 | } 257 | 258 | return result 259 | } 260 | 261 | 262 | func processCheckboxes(_ base: String) -> String { 263 | 264 | // FROM 1.4.2 265 | // Hack to present checkboxes a la GitHub 266 | 267 | let patterns: [String] = [#"\[\s?\](?!\()"#, #"\[[xX]{1}\](?!\()"#] 268 | let symbols: [String] = ["❎", "✅"] 269 | 270 | // Look for HTML symbol code '&...;' substrings, eg. '²' 271 | var i = 0 272 | var result = base 273 | for pattern in patterns { 274 | var range = result.range(of: pattern, options: .regularExpression) 275 | 276 | while range != nil { 277 | // Swap out the HTML symbol code for the actual symbol 278 | result = result.replacingCharacters(in: range!, with: symbols[i]) 279 | 280 | // Get the next occurence of the pattern ready for the 'while...' check 281 | range = result.range(of: pattern, options: .regularExpression) 282 | } 283 | 284 | i += 1 285 | } 286 | 287 | return result 288 | } 289 | 290 | 291 | func processCodeTags(_ base: String) -> String { 292 | 293 | // FROM 1.1.0 294 | // Look for markdown code blocks top'n'tailed with three ticks ``` 295 | // Processed here because SwiftyMarkdown doesn't handle this markup 296 | 297 | var isBlock = false 298 | var index = 0 299 | var lines = base.components(separatedBy: CharacterSet.newlines) 300 | 301 | // Run through the lines looking for initial ``` 302 | // Remove any found and inset the lines in between (for SwiftyMarkdown to format) 303 | for line in lines { 304 | if line.hasPrefix("```") { 305 | // Found a code block marker: remove the line and set 306 | // the marker to the opposite what it was, off or on 307 | lines.remove(at: index) 308 | isBlock = !isBlock 309 | continue 310 | } 311 | 312 | if isBlock { 313 | // Pad each line with an initial four spaces - this is what SwiftyMarkdown 314 | // looks for in a code block 315 | lines[index] = " " + lines[index] 316 | } 317 | 318 | index += 1 319 | } 320 | 321 | // Re-assemble the string from the lines, spacing them with a newline 322 | // (except for the final line, of course) 323 | index = 0 324 | var result = "" 325 | for line in lines { 326 | result += line + (index < lines.count - 1 ? "\n" : "") 327 | index += 1 328 | } 329 | 330 | return result 331 | } 332 | 333 | 334 | func convertSpaces(_ base: String) -> String { 335 | 336 | // FROM 1.1.1 337 | // Convert space-formatted lists to tab-formatte lists 338 | // Required because SwiftyMarkdown doesn't indent on spaces 339 | 340 | // Find (multiline) x spaces followed by *, - or 1-9, 341 | // where x >= 1 342 | let pattern = #"(?m)^[ ]+([1-9]|\*|-)"# 343 | var result = base as NSString 344 | var nrange: NSRange = result.range(of: pattern, options: .regularExpression) 345 | 346 | // Use NSRange and NSString because it's easier to modify the 347 | // range to exclude the character *after* the spaces 348 | while nrange.location != NSNotFound { 349 | var tabs = "" 350 | 351 | // Get the range of the spaces minus the detected list character 352 | let crange: NSRange = NSMakeRange(nrange.location, nrange.length - 1) 353 | 354 | // Get the number of tabs characters we need to insert 355 | let tabCount = (nrange.length - 1) / BUFFOON_CONSTANTS.SPACES_FOR_A_TAB 356 | 357 | // Assemble the required number of tabs 358 | for _ in 0.. String { 388 | 389 | let lines = markdown.components(separatedBy: CharacterSet.newlines) 390 | var fm: [String] = [] 391 | var doAdd: Bool = false 392 | 393 | for line in lines { 394 | // Look for the pattern on the current line 395 | let dashRange: NSRange = (line as NSString).range(of: startPattern, options: .regularExpression) 396 | // FROM 1.5.1 397 | let dotRange: NSRange = (line as NSString).range(of: endPattern, options: .regularExpression) 398 | 399 | if !doAdd && line.count > 0 { 400 | if dashRange.location == 0 { 401 | // Front matter start 402 | doAdd = true 403 | continue 404 | } else { 405 | // Some other text than front matter at the start 406 | // so break 407 | break 408 | } 409 | } 410 | 411 | let dashesFound: Bool = (dashRange.location != NSNotFound) 412 | let dotsFound: Bool = (dotRange.location != NSNotFound) 413 | if doAdd && (dashesFound || dotsFound) { 414 | // End of front matter 415 | var rs: String = "" 416 | for item in fm { 417 | rs += item + "\n" 418 | } 419 | 420 | return rs 421 | } 422 | 423 | if doAdd && line.count > 0 { 424 | // Add the line of front matter to the store 425 | fm.append(line) 426 | } 427 | } 428 | 429 | return "" 430 | } 431 | 432 | 433 | /** 434 | Render a supplied YAML sub-component ('part') to an NSAttributedString. 435 | 436 | Indents the value as required. 437 | 438 | FROM 1.3.0 439 | 440 | - Parameters: 441 | - part: A partial Yaml object. 442 | - indent: The number of indent spaces to add. 443 | - isKey: Is the Yaml part a key? 444 | 445 | - Returns: The rendered string as an NSAttributedString, or nil on error. 446 | */ 447 | func renderYaml(_ part: Yaml, _ indent: Int, _ isKey: Bool) -> NSAttributedString? { 448 | 449 | let returnString: NSMutableAttributedString = NSMutableAttributedString.init(string: "", 450 | attributes: keyAtts) 451 | 452 | switch (part) { 453 | case .array: 454 | if let value = part.array { 455 | // Iterate through array elements 456 | // NOTE A given element can be of any YAML type 457 | for i in 0.. 0 && (value[i].array != nil || value[i].dictionary != nil) { 461 | returnString.append(self.newLine) 462 | } 463 | 464 | // Add the element itself 465 | returnString.append(yamlString) 466 | } 467 | } 468 | 469 | return returnString 470 | } 471 | case .dictionary: 472 | if let dict = part.dictionary { 473 | // Iterate through the dictionary's keys and their values 474 | // NOTE A given value can be of any YAML type 475 | 476 | // Sort the dictionary's keys (ascending) 477 | // We assume all keys will be strings, ints, doubles or bools 478 | var keys: [Yaml] = Array(dict.keys) 479 | keys = keys.sorted(by: { (a, b) -> Bool in 480 | // Strings? 481 | if let a_s: String = a.string { 482 | if let b_s: String = b.string { 483 | return (a_s.lowercased() < b_s.lowercased()) 484 | } 485 | } 486 | 487 | // Ints? 488 | if let a_i: Int = a.int { 489 | if let b_i: Int = b.int { 490 | return (a_i < b_i) 491 | } 492 | } 493 | 494 | // Doubles? 495 | if let a_d: Double = a.double { 496 | if let b_d: Double = b.double { 497 | return (a_d < b_d) 498 | } 499 | } 500 | 501 | // Bools 502 | if let a_b: Bool = a.bool { 503 | if let b_b: Bool = b.bool { 504 | return (a_b && !b_b) 505 | } 506 | } 507 | 508 | return false 509 | }) 510 | 511 | // Iterate through the sorted keys array 512 | for i in 0.. 0 { 515 | returnString.append(self.newLine) 516 | } 517 | 518 | // Get the key:value pairs 519 | let key: Yaml = keys[i] 520 | let value: Yaml = dict[key] ?? "" 521 | 522 | // Render the key 523 | if let yamlString = renderYaml(key, indent, true) { 524 | returnString.append(yamlString) 525 | } 526 | 527 | // If the value is a collection, we drop to the next line and indent 528 | var valueIndent: Int = 0 529 | if value.array != nil || value.dictionary != nil || self.doIndentScalars { 530 | valueIndent = indent + BUFFOON_CONSTANTS.YAML_INDENT 531 | returnString.append(self.newLine) 532 | } 533 | 534 | // Render the key's value 535 | if let yamlString = renderYaml(value, valueIndent, false) { 536 | returnString.append(yamlString) 537 | } 538 | } 539 | 540 | return returnString 541 | } 542 | case .string: 543 | if let keyOrValue = part.string { 544 | let parts: [String] = keyOrValue.components(separatedBy: "\n") 545 | if parts.count > 2 { 546 | for i in 0.. NSAttributedString { 600 | 601 | let trimmedString = baseString.trimmingCharacters(in: .whitespaces) 602 | let spaces = " " 603 | let spaceString = String(spaces.suffix(indent)) 604 | let indentedString: NSMutableAttributedString = NSMutableAttributedString.init() 605 | indentedString.append(NSAttributedString.init(string: spaceString)) 606 | indentedString.append(NSAttributedString.init(string: trimmedString)) 607 | return indentedString.attributedSubstring(from: NSMakeRange(0, indentedString.length)) 608 | } 609 | 610 | 611 | // MARK: - Formatting Functions 612 | 613 | /** 614 | Set common style values for the markdown render. 615 | 616 | - Parameters: 617 | - sm: The SwiftyMarkdown instance used for rendering 618 | - isThumbnail: Are we rendering a thumbnail (`true`) or a preview (`false`). 619 | */ 620 | func setSwiftStyles(_ sm: SwiftyMarkdown, _ isThumbnail: Bool) { 621 | 622 | sm.setFontColorForAllStyles(with: (isThumbnail || self.doShowLightBackground) ? NSColor.black : NSColor.labelColor) 623 | sm.setFontSizeForAllStyles(with: self.fontSize) 624 | 625 | // FROM 1.4.0 -- add colour settings for headings too 626 | sm.h4.fontSize = self.fontSize * 1.2 627 | sm.h4.color = NSColor.hexToColour(self.headColourHex) 628 | sm.h3.fontSize = self.fontSize * 1.4 629 | sm.h3.color = sm.h4.color 630 | sm.h2.fontSize = self.fontSize * 1.6 631 | sm.h2.color = sm.h4.color 632 | sm.h1.fontSize = self.fontSize * 2.0 633 | sm.h1.color = sm.h4.color 634 | 635 | sm.setFontNameForAllStyles(with: self.bodyFontName) 636 | sm.code.fontName = self.codeFontName 637 | 638 | // Set the code colour 639 | sm.code.color = NSColor.hexToColour(self.codeColourHex) // getColour(codeColourIndex) 640 | 641 | // NOTE The following do not set link colour - this is 642 | // a bug or issue with SwiftyMarkdown 1.2.3 643 | // NOTE RESOLVED: Customise SWiftyMarkdown not to use the .link attribute as this 644 | // causes NSAttributedString to apply its own colour 645 | sm.link.color = NSColor.hexToColour(self.linkColourHex) // getColour(linkColourIndex) 646 | sm.link.underlineColor = NSColor.hexToColour(self.linkColourHex) 647 | sm.underlineLinks = true 648 | 649 | // FROM 1.5.0 650 | sm.blockquotes.color = NSColor.hexToColour(self.quoteColourHex) 651 | sm.blockquotes.fontSize = self.fontSize * 1.4 652 | sm.blockquotes.fontStyle = .boldItalic 653 | } 654 | 655 | } 656 | 657 | 658 | /** 659 | Get the encoding of the string formed from data. 660 | 661 | - Returns: The string's encoding or nil. 662 | */ 663 | 664 | extension Data { 665 | 666 | var stringEncoding: String.Encoding? { 667 | var nss: NSString? = nil 668 | guard case let rawValue = NSString.stringEncoding(for: self, 669 | encodingOptions: nil, 670 | convertedString: &nss, 671 | usedLossyConversion: nil), rawValue != 0 else { return nil } 672 | return .init(rawValue: rawValue) 673 | } 674 | } 675 | 676 | 677 | /** 678 | Swap the paragraph style in all of the attributes of 679 | an NSMutableAttributedString. 680 | 681 | - Parameters: 682 | - paraStyle: The injected NSParagraphStyle. 683 | */ 684 | extension NSMutableAttributedString { 685 | 686 | func addParaStyle(with paraStyle: NSParagraphStyle) { 687 | beginEditing() 688 | self.enumerateAttribute(.paragraphStyle, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in 689 | if let _ = value as? NSParagraphStyle { 690 | addAttribute(.paragraphStyle, value: paraStyle, range: range) 691 | } 692 | } 693 | endEditing() 694 | } 695 | } 696 | -------------------------------------------------------------------------------- /Markdown Previewer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Markdown Previewer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 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 | ITSAppUsesNonExemptEncryption 24 | 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSExtension 28 | 29 | NSExtensionAttributes 30 | 31 | QLSupportedContentTypes 32 | 33 | net.daringfireball.markdown 34 | net.daringfireball 35 | net.multimarkdown.text 36 | org.vim.markdown-file 37 | com.unknown.md 38 | com.foldingtext.FoldingText.document 39 | pro.writer.markdown 40 | net.ia.markdown 41 | com.nutstore.down 42 | 43 | QLSupportsSearchableItems 44 | 45 | 46 | NSExtensionPointIdentifier 47 | com.apple.quicklook.preview 48 | NSExtensionPrincipalClass 49 | $(PRODUCT_MODULE_NAME).PreviewViewController 50 | 51 | NSHumanReadableCopyright 52 | Copyright © 2024 Tony Smith. All rights reserved. 53 | 54 | 55 | -------------------------------------------------------------------------------- /Markdown Previewer/PreviewViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * PreviewViewController.swift 3 | * Previewer 4 | * 5 | * Created by Tony Smith on 31/10/2019. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Cocoa 11 | import Quartz 12 | 13 | 14 | class PreviewViewController: NSViewController, 15 | QLPreviewingController { 16 | 17 | // MARK: - Class UI Properties 18 | 19 | @IBOutlet var errorReportField: NSTextField! 20 | @IBOutlet var renderTextView: NSTextView! 21 | @IBOutlet var renderTextScrollView: NSScrollView! 22 | 23 | 24 | // MARK: - Private Properties 25 | 26 | override var nibName: NSNib.Name? { 27 | return NSNib.Name("PreviewViewController") 28 | } 29 | 30 | 31 | // MARK: - QLPreviewingController Required Functions 32 | 33 | func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) { 34 | 35 | // Hide the error message field 36 | self.errorReportField.stringValue = "" 37 | self.errorReportField.isHidden = true 38 | self.renderTextScrollView.isHidden = false 39 | 40 | // FROM 1.1.0 41 | // Get an error message ready for use 42 | var reportError: NSError? = nil 43 | 44 | // Load the source file using a co-ordinator as we don't know what thread this function 45 | // will be executed in when it's called by macOS' QuickLook code 46 | // NOTE From 1.1.0 we use plain old FileManager for this 47 | if FileManager.default.isReadableFile(atPath: url.path) { 48 | // Only proceed if the file is accessible from here 49 | do { 50 | // Get the file contents as a string 51 | let data: Data = try Data.init(contentsOf: url, options: [.uncached]) 52 | 53 | // FROM 1.4.3 54 | // Get the string's encoding, or fail back to .utf8 55 | let encoding: String.Encoding = data.stringEncoding ?? .utf8 56 | 57 | if let markdownString: String = String.init(data: data, encoding: encoding) { 58 | // Instantiate the common code 59 | let common: Common = Common.init(false) 60 | 61 | // Update the NSTextView 62 | 63 | // FROM 1.3.0 64 | // Knock back the light background to make the scroll bars visible in dark mode 65 | // NOTE If !doShowLightBackground, 66 | // in light mode, the scrollers show up dark-on-light, in dark mode light-on-dark 67 | // If doShowLightBackground, 68 | // in light mode, the scrollers show up light-on-light, in dark mode light-on-dark 69 | // NOTE Changing the scrollview scroller knob style has no effect 70 | self.renderTextView.backgroundColor = common.doShowLightBackground ? NSColor.init(white: 1.0, alpha: 0.9) : NSColor.textBackgroundColor 71 | self.renderTextScrollView.scrollerKnobStyle = common.doShowLightBackground ? .dark : .light 72 | 73 | if let renderTextStorage: NSTextStorage = self.renderTextView.textStorage { 74 | renderTextStorage.beginEditing() 75 | renderTextStorage.setAttributedString(common.getAttributedString(markdownString, false)) 76 | renderTextStorage.endEditing() 77 | 78 | // Add the subview to the instance's own view and draw 79 | self.view.display() 80 | 81 | // Call the QLPreviewingController indicating no error (nil) 82 | handler(nil) 83 | return 84 | } 85 | 86 | // We couldn't access the preview NSTextView's NSTextStorage 87 | reportError = setError(BUFFOON_CONSTANTS.ERRORS.CODES.BAD_TS_STRING) 88 | } else { 89 | // FROM 1.4.3 90 | // We couldn't convert to data to a valid encoding 91 | let errDesc: String = "\(BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_TS_STRING) \(encoding)" 92 | reportError = NSError(domain: BUFFOON_CONSTANTS.APP_CODE_PREVIEWER, 93 | code: BUFFOON_CONSTANTS.ERRORS.CODES.BAD_MD_STRING, 94 | userInfo: [NSLocalizedDescriptionKey: errDesc]) 95 | } 96 | } catch { 97 | // We couldn't read the file so set an appropriate error to report back 98 | reportError = setError(BUFFOON_CONSTANTS.ERRORS.CODES.FILE_WONT_OPEN) 99 | } 100 | } else { 101 | // File passed isn't readable 102 | reportError = setError(BUFFOON_CONSTANTS.ERRORS.CODES.FILE_INACCESSIBLE) 103 | } 104 | 105 | // Display the error locally in the window 106 | showError(reportError!) 107 | 108 | // Call the QLPreviewingController indicating an error (!nil) 109 | handler(nil) 110 | } 111 | 112 | 113 | func preparePreviewOfSearchableItem(identifier: String, queryString: String?, completionHandler handler: @escaping (Error?) -> Void) { 114 | 115 | // Is this ever called? 116 | NSLog("BUFFOON searchable identifier: \(identifier)") 117 | NSLog("BUFFOON searchable query: " + (queryString ?? "nil")) 118 | 119 | // Hand control back to QuickLook 120 | handler(nil) 121 | } 122 | 123 | 124 | // MARK: - Utility Functions 125 | 126 | /** 127 | Place an error message in its various outlets. 128 | 129 | - parameters: 130 | - error: The error as an NSError. 131 | */ 132 | func showError(_ error: NSError) { 133 | 134 | let errString: String = error.userInfo[NSLocalizedDescriptionKey] as! String 135 | self.errorReportField.stringValue = errString 136 | self.errorReportField.isHidden = false 137 | self.renderTextScrollView.isHidden = true 138 | self.view.display() 139 | NSLog("BUFFOON \(errString)") 140 | } 141 | 142 | 143 | /** 144 | Generate an NSError for an internal error, specified by its code. 145 | 146 | Codes are listed in `Constants.swift` 147 | 148 | - Parameters: 149 | - code: The internal error code. 150 | 151 | - Returns: The described error as an NSError. 152 | */ 153 | func setError(_ code: Int) -> NSError { 154 | 155 | // NSError generation function 156 | 157 | var errDesc: String 158 | 159 | switch(code) { 160 | case BUFFOON_CONSTANTS.ERRORS.CODES.FILE_INACCESSIBLE: 161 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.FILE_INACCESSIBLE 162 | case BUFFOON_CONSTANTS.ERRORS.CODES.FILE_WONT_OPEN: 163 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.FILE_WONT_OPEN 164 | case BUFFOON_CONSTANTS.ERRORS.CODES.BAD_TS_STRING: 165 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_TS_STRING 166 | case BUFFOON_CONSTANTS.ERRORS.CODES.BAD_MD_STRING: 167 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_MD_STRING 168 | default: 169 | errDesc = "UNKNOWN ERROR" 170 | } 171 | 172 | return NSError(domain: BUFFOON_CONSTANTS.APP_CODE_PREVIEWER, 173 | code: code, 174 | userInfo: [NSLocalizedDescriptionKey: errDesc]) 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /Markdown Previewer/Previewer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)suite.previewmarkdown 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Markdown Previewer/licences.txt: -------------------------------------------------------------------------------- 1 | PreviewMarkdown, Previewer Art and UI Design Copyright (c) 2024 Tony Smith (@smittytone) 2 | 3 | All rights reserved. 4 | 5 | ---------------------------------------- 6 | 7 | MIT Licence 8 | 9 | PreviewMarkdown, Previewer Source Code Copyright (c) 2024 Tony Smith (@smittytone) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | 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 27 | SOFTWARE. 28 | 29 | ---------------------------------------- 30 | 31 | MIT Licence 32 | 33 | SwiftyMarkdown Copyright (c) 2016-22 Simon Fairbairn 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in all 43 | copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 51 | SOFTWARE. 52 | 53 | ---------------------------------------- 54 | 55 | MIT Licence 56 | 57 | YamlSwift Copyright (c) 2015 Behrang Noruzi Niya 58 | 59 | Permission is hereby granted, free of charge, to any person obtaining a copy 60 | of this software and associated documentation files (the "Software"), to deal 61 | in the Software without restriction, including without limitation the rights 62 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 63 | copies of the Software, and to permit persons to whom the Software is 64 | furnished to do so, subject to the following conditions: 65 | 66 | The above copyright notice and this permission notice shall be included in all 67 | copies or substantial portions of the Software. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 70 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 71 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 72 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 73 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 74 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 75 | SOFTWARE. 76 | -------------------------------------------------------------------------------- /Markdown Thumbnailer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Markdown Thumbnailer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 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 | ITSAppUsesNonExemptEncryption 24 | 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSExtension 28 | 29 | NSExtensionAttributes 30 | 31 | QLSupportedContentTypes 32 | 33 | net.daringfireball 34 | net.daringfireball.markdown 35 | net.multimarkdown.text 36 | org.vim.markdown-file 37 | com.unknown.md 38 | com.foldingtext.FoldingText.document 39 | pro.writer.markdown 40 | net.ia.markdown 41 | com.nutstore.down 42 | 43 | QLThumbnailMinimumDimension 44 | 32 45 | 46 | NSExtensionPointIdentifier 47 | com.apple.quicklook.thumbnail 48 | NSExtensionPrincipalClass 49 | $(PRODUCT_MODULE_NAME).ThumbnailProvider 50 | 51 | NSHumanReadableCopyright 52 | Copyright © 2024 Tony Smith. All rights reserved. 53 | 54 | 55 | -------------------------------------------------------------------------------- /Markdown Thumbnailer/ThumbnailProvider.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ThumbnailProvider.swift 3 | * Thumbnailer 4 | * 5 | * Created by Tony Smith on 31/10/2019. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Cocoa 11 | import QuickLookThumbnailing 12 | 13 | 14 | class ThumbnailProvider: QLThumbnailProvider { 15 | 16 | // MARK: - Private Properties 17 | 18 | // FROM 1.4.0 19 | // Add possible errors returned by autorelease pool 20 | private enum ThumbnailerError: Error { 21 | case badFileLoad(String) 22 | case badFileUnreadable(String) 23 | case badFileUnsupportedEncoding(String) 24 | case badFileUnsupportedFile(String) 25 | case badGfxBitmap 26 | case badGfxDraw 27 | } 28 | 29 | 30 | // MARK: - QLThumbnailProvider Required Functions 31 | 32 | override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) { 33 | 34 | /* 35 | * This is the main entry point for the macOS thumbnailing system 36 | */ 37 | 38 | // Load the source file using a co-ordinator as we don't know what thread this function 39 | // will be executed in when it's called by macOS' QuickLook code 40 | if FileManager.default.isReadableFile(atPath: request.fileURL.path) { 41 | // Only proceed if the file is accessible from here 42 | do { 43 | // Get the file contents as a string, making sure it's not cached 44 | // as we're not going to read it again any time soon 45 | let data: Data = try Data.init(contentsOf: request.fileURL, options: [.uncached]) 46 | 47 | // FROM 1.4.3 48 | // Get the string's encoding, or fail back to .utf8 49 | let encoding: String.Encoding = data.stringEncoding ?? .utf8 50 | 51 | guard let markdownString: String = String.init(data: data, encoding: encoding) else { 52 | handler(nil, ThumbnailerError.badFileLoad(request.fileURL.path)) 53 | return 54 | } 55 | 56 | // Instantiate the common code for a thumbnail ('true') 57 | let common: Common = Common.init(true) 58 | 59 | // FROM 1.4.1 60 | // Only render the lines *likely* to appear in the thumbnail 61 | let lines: [Substring] = markdownString.split(separator: "\n", maxSplits: BUFFOON_CONSTANTS.THUMBNAIL_LINE_COUNT + 1, omittingEmptySubsequences: false) 62 | var displayString: String = "" 63 | var displayLineCount: Int = 0 64 | var gotFrontMatter: Bool = false 65 | var markdownStart: Int = 0 66 | 67 | for i in 0.. 1 { 90 | displayLineCount += (approxParagraphLineCount + 1) 91 | } else { 92 | displayLineCount += 1 93 | } 94 | 95 | // Add the paragraph to the string we'll present 96 | displayString += (String(lines[i]) + "\n") 97 | 98 | if displayLineCount >= BUFFOON_CONSTANTS.THUMBNAIL_LINE_COUNT { 99 | break 100 | } 101 | } 102 | 103 | // Set the primary NSTextView drawing frame and a base font size 104 | let markdownFrame: CGRect = NSMakeRect(CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_SIZE.ORIGIN_X), 105 | CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_SIZE.ORIGIN_Y), 106 | CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_SIZE.WIDTH), 107 | CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_SIZE.HEIGHT)) 108 | 109 | // Instantiate an NSTextField to display the NSAttributedString render of the YAML, 110 | // and extend the size of its frame 111 | let markdownTextField: NSTextField = NSTextField.init(frame: markdownFrame) 112 | markdownTextField.lineBreakMode = .byTruncatingTail 113 | markdownTextField.attributedStringValue = common.getAttributedString(displayString, true) 114 | 115 | // Generate the bitmap from the rendered markdown text view 116 | guard let bodyImageRep: NSBitmapImageRep = markdownTextField.bitmapImageRepForCachingDisplay(in: markdownFrame) else { 117 | handler(nil, ThumbnailerError.badGfxBitmap) 118 | return 119 | } 120 | 121 | // Draw the view into the bitmap 122 | markdownTextField.cacheDisplay(in: markdownFrame, to: bodyImageRep) 123 | 124 | if let image: CGImage = bodyImageRep.cgImage { 125 | if let cgImage: CGImage = image.copy() { 126 | // Set the thumbnail frame 127 | let thumbnailFrame: CGRect = NSMakeRect(0.0, 128 | 0.0, 129 | CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_SIZE.ASPECT) * request.maximumSize.height, 130 | request.maximumSize.height) 131 | 132 | let scaleFrame: CGRect = NSMakeRect(0.0, 133 | 0.0, 134 | thumbnailFrame.width * request.scale, 135 | thumbnailFrame.height * request.scale) 136 | 137 | // Pass a QLThumbnailReply and no error to the supplied handler 138 | handler(QLThumbnailReply.init(contextSize: thumbnailFrame.size) { (context) -> Bool in 139 | // `scaleFrame` and `cgImage` are immutable 140 | context.draw(cgImage, in: scaleFrame, byTiling: false) 141 | return true 142 | }, nil) 143 | return 144 | } 145 | } 146 | 147 | handler(nil, ThumbnailerError.badGfxDraw) 148 | return 149 | } catch { 150 | // NOP: fall through to error 151 | } 152 | } 153 | 154 | // We didn't draw anything because of 'can't find file' error 155 | handler(nil, ThumbnailerError.badFileUnreadable(request.fileURL.path)) 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /Markdown Thumbnailer/Thumbnailer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)suite.previewmarkdown 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Markdown Thumbnailer/licences.txt: -------------------------------------------------------------------------------- 1 | PreviewMarkdown, Thumbnailer Art and UI Design Copyright (c) 2024 Tony Smith (@smittytone) 2 | 3 | All rights reserved. 4 | 5 | ---------------------------------------- 6 | 7 | MIT License 8 | 9 | PreviewMarkdown, Thumbnailer Source Code Copyright (c) 2024 Tony Smith (@smittytone) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | 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 27 | SOFTWARE. 28 | 29 | ---------------------------------------- 30 | 31 | MIT Licence 32 | 33 | SwiftyMarkdown Copyright (c) 2016-22 Simon Fairbairn 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in all 43 | copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 51 | SOFTWARE. 52 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "d4b7333fd95b37ec75f42587185e8b651c0b8b95c242c8dd73bf7946bec5da49", 3 | "pins" : [ 4 | { 5 | "identity" : "swiftymarkdown", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/smittytone/SwiftyMarkdown.git", 8 | "state" : { 9 | "branch" : "develop", 10 | "revision" : "98ef22ff831aabfee0f6d6645b696a5db0d9f44a" 11 | } 12 | }, 13 | { 14 | "identity" : "yamlswift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/smittytone/YamlSwift.git", 17 | "state" : { 18 | "branch" : "master", 19 | "revision" : "f6da2600c6df5b316ab24397a7508a708631c11d" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/xcshareddata/xcschemes/PreviewMarkdown.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 51 | 57 | 58 | 59 | 60 | 61 | 72 | 74 | 80 | 81 | 82 | 83 | 89 | 91 | 97 | 98 | 99 | 100 | 102 | 103 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/xcshareddata/xcschemes/Previewer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /PreviewMarkdown.xcodeproj/xcshareddata/xcschemes/Thumbnailer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 83 | 87 | 88 | 92 | 93 | 94 | 95 | 99 | 100 | 101 | 102 | 110 | 112 | 118 | 119 | 120 | 121 | 123 | 124 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /PreviewMarkdown/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * AppDelegate.swift 3 | * PreviewMarkdown 4 | * 5 | * Created by Tony Smith on 31/10/2019. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Cocoa 11 | import CoreServices 12 | import WebKit 13 | 14 | 15 | @NSApplicationMain 16 | final class AppDelegate: NSObject, 17 | NSApplicationDelegate, 18 | URLSessionDelegate, 19 | URLSessionDataDelegate, 20 | WKNavigationDelegate { 21 | 22 | // MARK: - Class UI Properies 23 | 24 | // Menu Items Tab 25 | @IBOutlet var helpMenu: NSMenuItem! 26 | @IBOutlet var helpMenuSwiftyMarkdown: NSMenuItem! 27 | @IBOutlet var helpMenuAppStoreRating: NSMenuItem! 28 | // FROM 1.3.0 29 | @IBOutlet var helpMenuYamlSwift: NSMenuItem! 30 | // FROM 1.3.1 31 | @IBOutlet var helpMenuOthersPreviewYaml: NSMenuItem! 32 | // FROM 1.4.0 33 | @IBOutlet var helpMenuOthersPreviewCode: NSMenuItem! 34 | // FROM 1.4.4 35 | @IBOutlet var helpMenuOthersPreviewJson: NSMenuItem! 36 | // FROM 1.4.5 37 | @IBOutlet var helpMenuOthersPreviewText: NSMenuItem! 38 | @IBOutlet var helpMenuOnlineHelp: NSMenuItem! 39 | @IBOutlet var helpMenuReportBug: NSMenuItem! 40 | @IBOutlet var helpMenuWhatsNew: NSMenuItem! 41 | @IBOutlet var mainMenuSettings: NSMenuItem! 42 | // FROM 1.5.0 43 | @IBOutlet var mainMenuResetFinder: NSMenuItem! 44 | 45 | // Panel Items 46 | @IBOutlet var versionLabel: NSTextField! 47 | 48 | // Windows 49 | @IBOutlet weak var window: NSWindow! 50 | 51 | // FROM 1.1.1 52 | // Report Sheet 53 | @IBOutlet weak var reportWindow: NSWindow! 54 | @IBOutlet weak var feedbackText: NSTextField! 55 | @IBOutlet weak var connectionProgress: NSProgressIndicator! 56 | 57 | // FROM 1.2.0 58 | // Preferences Sheet 59 | @IBOutlet weak var preferencesWindow: NSWindow! 60 | @IBOutlet weak var fontSizeSlider: NSSlider! 61 | @IBOutlet weak var fontSizeLabel: NSTextField! 62 | @IBOutlet weak var useLightCheckbox: NSButton! 63 | @IBOutlet weak var bodyFontPopup: NSPopUpButton! 64 | @IBOutlet weak var codeFontPopup: NSPopUpButton! 65 | // FROM 1.3.0 66 | @IBOutlet weak var showFrontMatterCheckbox: NSButton! 67 | // FROM 1.4.0 68 | //@IBOutlet weak var codeColourWell: NSColorWell! 69 | @IBOutlet weak var headColourWell: NSColorWell! 70 | @IBOutlet weak var bodyStylePopup: NSPopUpButton! 71 | @IBOutlet weak var codeStylePopup: NSPopUpButton! 72 | // FROM 1.5.0 73 | @IBOutlet weak var lineSpacingPopup: NSPopUpButton! 74 | @IBOutlet weak var colourSelectionPopup: NSPopUpButton! 75 | 76 | // FROM 1.2.0 77 | // What's New Sheet 78 | @IBOutlet weak var whatsNewWindow: NSWindow! 79 | @IBOutlet weak var whatsNewWebView: WKWebView! 80 | 81 | 82 | // MARK: - Private Properies 83 | 84 | // FROM 1.1.1 85 | private var feedbackTask: URLSessionTask? = nil 86 | // FROM 1.2.0 -- stores for preferences 87 | internal var whatsNewNav: WKNavigation? = nil 88 | private var previewFontSize: CGFloat = CGFloat(BUFFOON_CONSTANTS.PREVIEW_FONT_SIZE) 89 | private var doShowLightBackground: Bool = false 90 | private var doShowTag: Bool = false 91 | var localMarkdownUTI: String = "NONE" 92 | // FROM 1.3.0 93 | private var doShowFrontMatter: Bool = false 94 | // FROM 1.4.0 95 | private var codeColourHex: String = BUFFOON_CONSTANTS.CODE_COLOUR_HEX 96 | private var headColourHex: String = BUFFOON_CONSTANTS.HEAD_COLOUR_HEX 97 | private var bodyFontName: String = BUFFOON_CONSTANTS.BODY_FONT_NAME 98 | private var codeFontName: String = BUFFOON_CONSTANTS.CODE_FONT_NAME 99 | internal var bodyFonts: [PMFont] = [] 100 | internal var codeFonts: [PMFont] = [] 101 | // FROM 1.4.6 102 | //private var havePrefsChanged: Bool = false 103 | // FROM 1.5.0 104 | private var lineSpacing: CGFloat = BUFFOON_CONSTANTS.BASE_LINE_SPACING 105 | private var linkColourHex: String = BUFFOON_CONSTANTS.LINK_COLOUR_HEX 106 | private var displayColours: [String:String] = [:] 107 | 108 | /* 109 | Replace the following string with your own team ID. This is used to 110 | identify the app suite and so share preferences set by the main app with 111 | the previewer and thumbnailer extensions. 112 | */ 113 | private var appSuiteName: String = MNU_SECRETS.PID + BUFFOON_CONSTANTS.SUITE_NAME 114 | 115 | 116 | // MARK: - Class Lifecycle Functions 117 | 118 | func applicationDidFinishLaunching(_ notification: Notification) { 119 | 120 | // FROM 1.4.0 121 | // Pre-load fonts in a separate thread 122 | let q: DispatchQueue = DispatchQueue.init(label: "com.bps.previewmarkdown.async-queue") 123 | q.async { 124 | self.asyncGetFonts() 125 | } 126 | 127 | // FROM 1.2.0 128 | // Set application group-level defaults 129 | registerPreferences() 130 | 131 | // FROM 1.2.0 132 | // Get the local UTI for markdown files 133 | self.localMarkdownUTI = getLocalFileUTI(BUFFOON_CONSTANTS.SAMPLE_UTI_FILE) 134 | 135 | // FROM 1.0.3 136 | // Add the version number to the panel 137 | let version: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 138 | let build: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String 139 | versionLabel.stringValue = "Version \(version) (\(build))" 140 | 141 | // From 1.0.4 142 | // Disable the Help menu Spotlight features 143 | let dummyHelpMenu: NSMenu = NSMenu.init(title: "Dummy") 144 | let theApp = NSApplication.shared 145 | theApp.helpMenu = dummyHelpMenu 146 | 147 | // FROM 1.0.2 148 | // Centre window and display 149 | self.window.center() 150 | self.window.makeKeyAndOrderFront(self) 151 | 152 | // FROM 1.2.0 153 | // Show 'What's New' if we need to 154 | // (and set up the WKWebBiew: no elasticity, horizontal scroller) 155 | // NOTE Has to take place at the end of the function 156 | doShowWhatsNew(self) 157 | } 158 | 159 | 160 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 161 | 162 | // When the main window closed, shut down the app 163 | return true 164 | } 165 | 166 | 167 | // MARK: - Action Functions 168 | 169 | @IBAction private func doClose(_ sender: Any) { 170 | 171 | // FROM 1.3.0 172 | // Reset the QL thumbnail cache... just in case 173 | _ = runProcess(app: "/usr/bin/qlmanage", with: ["-r", "cache"]) 174 | 175 | // FROM 1.4.6 176 | // Check for open panels 177 | if self.preferencesWindow.isVisible { 178 | if checkPrefs() { 179 | let alert: NSAlert = showAlert("You have unsaved settings", 180 | "Do you wish to cancel and save them, or quit the app anyway?", 181 | false) 182 | alert.addButton(withTitle: "Quit") 183 | alert.addButton(withTitle: "Cancel") 184 | alert.beginSheetModal(for: self.preferencesWindow) { (response: NSApplication.ModalResponse) in 185 | if response == NSApplication.ModalResponse.alertFirstButtonReturn { 186 | // The user clicked 'Quit' 187 | self.preferencesWindow.close() 188 | self.window.close() 189 | } 190 | } 191 | 192 | return 193 | } 194 | 195 | self.preferencesWindow.close() 196 | } 197 | 198 | if self.whatsNewWindow.isVisible { 199 | self.whatsNewWindow.close() 200 | } 201 | 202 | if self.reportWindow.isVisible { 203 | if self.feedbackText.stringValue.count > 0 { 204 | let alert: NSAlert = showAlert("You have unsent feedback", 205 | "Do you wish to cancel and send it, or quit the app anyway?", 206 | false) 207 | alert.addButton(withTitle: "Quit") 208 | alert.addButton(withTitle: "Cancel") 209 | alert.beginSheetModal(for: self.reportWindow) { (response: NSApplication.ModalResponse) in 210 | if response == NSApplication.ModalResponse.alertFirstButtonReturn { 211 | // The user clicked 'Quit' 212 | self.reportWindow.close() 213 | self.window.close() 214 | } 215 | } 216 | 217 | return 218 | } 219 | 220 | self.reportWindow.close() 221 | } 222 | 223 | // Close the window... which will trigger an app closure 224 | self.window.close() 225 | } 226 | 227 | 228 | @IBAction @objc private func doShowSites(sender: Any) { 229 | 230 | // Open the websites for contributors 231 | let item: NSMenuItem = sender as! NSMenuItem 232 | var path: String = BUFFOON_CONSTANTS.URL_MAIN 233 | 234 | // FROM 1.1.0 -- bypass unused items 235 | if item == self.helpMenuSwiftyMarkdown { 236 | path = "https://github.com/SimonFairbairn/SwiftyMarkdown" 237 | } else if item == self.helpMenuAppStoreRating { 238 | path = BUFFOON_CONSTANTS.APP_STORE + "?action=write-review" 239 | } else if item == self.helpMenuYamlSwift { 240 | // FROM 1.3.0 241 | path = "https://github.com/behrang/YamlSwift" 242 | } else if item == self.helpMenuOnlineHelp { 243 | // FROM 1.3.0 244 | path += "#how-to-use-previewmarkdown" 245 | } else if item == self.helpMenuOthersPreviewYaml { 246 | // FROM 1.3.1 247 | path = BUFFOON_CONSTANTS.APP_URLS.PY 248 | } else if item == self.helpMenuOthersPreviewCode { 249 | // FROM 1.4.0 250 | path = BUFFOON_CONSTANTS.APP_URLS.PC 251 | } else if item == self.helpMenuOthersPreviewJson { 252 | // FROM 1.4.4 253 | path = BUFFOON_CONSTANTS.APP_URLS.PJ 254 | } else if item == self.helpMenuOthersPreviewText { 255 | // FROM 1.4.6 256 | path = BUFFOON_CONSTANTS.APP_URLS.PT 257 | } 258 | 259 | // Open the selected website 260 | NSWorkspace.shared.open(URL.init(string:path)!) 261 | } 262 | 263 | 264 | @IBAction private func doShowPrefsHelp(sender: Any) { 265 | 266 | // FROM 1.5.0 267 | // Alternative route to help 268 | let path: String = BUFFOON_CONSTANTS.URL_MAIN + "#customise-the-preview" 269 | NSWorkspace.shared.open(URL.init(string:path)!) 270 | 271 | } 272 | 273 | 274 | @IBAction private func doOpenSysPrefs(sender: Any) { 275 | 276 | // FROM 1.1.0 277 | // Open the System Preferences app at the Extensions pane 278 | NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Library/PreferencePanes/Extensions.prefPane")) 279 | } 280 | 281 | 282 | @IBAction private func doInitiateFinderReset(sender: Any) { 283 | 284 | // FROM 1.5.0 285 | warnUserAboutReset() 286 | } 287 | 288 | 289 | // MARK: - Report Functions 290 | 291 | @IBAction @objc private func showFeedbackWindow(sender: Any?) { 292 | 293 | // FROM 1.1.1 294 | // Display a window in which the user can submit feedback 295 | 296 | // FROM 1.4.6 297 | // Disable menus we don't want used when the panel is open 298 | hidePanelGenerators() 299 | 300 | // Reset the UI 301 | self.connectionProgress.stopAnimation(self) 302 | self.feedbackText.stringValue = "" 303 | 304 | // Present the window 305 | self.window.beginSheet(self.reportWindow, completionHandler: nil) 306 | } 307 | 308 | 309 | @IBAction @objc private func doCancelReportWindow(sender: Any) { 310 | 311 | // FROM 1.1.1 312 | // User has clicked 'Cancel', so just close the sheet 313 | 314 | self.connectionProgress.stopAnimation(self) 315 | self.window.endSheet(self.reportWindow) 316 | 317 | // FROM 1.4.6 318 | // Restore menus 319 | showPanelGenerators() 320 | } 321 | 322 | 323 | @IBAction @objc private func doSendFeedback(sender: Any) { 324 | 325 | // FROM 1.1.1 326 | // User clicked 'Send' so get the message (if there is one) from the text field and send it 327 | 328 | let feedback: String = self.feedbackText.stringValue 329 | 330 | if feedback.count > 0 { 331 | // Start the connection indicator if it's not already visible 332 | self.connectionProgress.startAnimation(self) 333 | 334 | /* 335 | Add your own `func sendFeedback(_ feedback: String) -> URLSessionTask?` function 336 | */ 337 | self.feedbackTask = sendFeedback(feedback) 338 | 339 | if self.feedbackTask != nil { 340 | // We have a valid URL Session Task, so start it to send 341 | self.feedbackTask!.resume() 342 | } else { 343 | // Report the error 344 | sendFeedbackError() 345 | } 346 | 347 | return 348 | } 349 | 350 | // No feedback, so close the sheet 351 | self.window.endSheet(self.reportWindow) 352 | 353 | // FROM 1.4.6 354 | // Restore menus 355 | showPanelGenerators() 356 | 357 | // NOTE sheet closes asynchronously unless there was no feedback to send 358 | } 359 | 360 | 361 | // MARK: - Preferences Functions 362 | 363 | /** 364 | Initialise and display the **Preferences** sheet. 365 | 366 | FROM 1.2.0 367 | 368 | - Parameters: 369 | - sender: The source of the action. 370 | */ 371 | @IBAction private func doShowPreferences(sender: Any) { 372 | 373 | // FROM 1.4.6 374 | // Reset changed prefs flag 375 | //self.havePrefsChanged = false 376 | 377 | // FROM 1.4.6 378 | // Disable menus we don't want used when the panel is open 379 | hidePanelGenerators() 380 | 381 | // The suite name is the app group name, set in each extension's entitlements, and the host app's 382 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 383 | self.previewFontSize = CGFloat(defaults.float(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_SIZE)) 384 | self.doShowLightBackground = defaults.bool(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_USE_LIGHT) 385 | 386 | // FROM 1.3.0 387 | self.doShowFrontMatter = defaults.bool(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_SHOW_YAML) 388 | 389 | // FROM 1.4.0 390 | self.codeFontName = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_FONT_NAME) ?? BUFFOON_CONSTANTS.CODE_FONT_NAME 391 | self.bodyFontName = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_NAME) ?? BUFFOON_CONSTANTS.BODY_FONT_NAME 392 | 393 | // FROM 1.5.0 394 | self.lineSpacing = CGFloat(defaults.float(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINE_SPACE)) 395 | self.displayColours["heads"] = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_HEAD_COLOUR) ?? BUFFOON_CONSTANTS.HEAD_COLOUR_HEX 396 | self.displayColours["code"] = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_COLOUR) ?? BUFFOON_CONSTANTS.CODE_COLOUR_HEX 397 | self.displayColours["links"] = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINK_COLOUR) ?? BUFFOON_CONSTANTS.LINK_COLOUR_HEX 398 | self.displayColours["quote"] = defaults.string(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_QUOTE_COLOUR) ?? 399 | BUFFOON_CONSTANTS.QUOTE_COLOUR_HEX 400 | } 401 | 402 | // Get the menu item index from the stored value 403 | // NOTE The other values are currently stored as indexes -- should this be the same? 404 | let index: Int = BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS.lastIndex(of: self.previewFontSize) ?? 3 405 | self.fontSizeSlider.floatValue = Float(index) 406 | self.fontSizeLabel.stringValue = "\(Int(BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[index]))pt" 407 | self.useLightCheckbox.state = self.doShowLightBackground ? .on : .off 408 | 409 | // FROM 1.3.0 410 | self.showFrontMatterCheckbox.state = self.doShowFrontMatter ? .on : .off 411 | 412 | // FROM 1.4.0 413 | // Set the two colour wells 414 | //self.codeColourWell.color = NSColor.hexToColour(self.codeColourHex) 415 | self.headColourWell.color = NSColor.hexToColour(self.displayColours["heads"] ?? BUFFOON_CONSTANTS.HEAD_COLOUR_HEX) 416 | 417 | // FROM 1.4.0 418 | // Extend font selection to all available fonts 419 | // First, the body text font... 420 | self.bodyFontPopup.removeAllItems() 421 | self.bodyStylePopup.isEnabled = false 422 | 423 | for i: Int in 0.. Bool { 530 | 531 | var haveChanged: Bool = false 532 | 533 | // Check for a use light background change 534 | var state: Bool = self.useLightCheckbox.state == .on 535 | haveChanged = (self.doShowLightBackground != state) 536 | 537 | // Check for a show frontmatter change 538 | if !haveChanged { 539 | state = self.showFrontMatterCheckbox.state == .on 540 | haveChanged = (self.doShowFrontMatter != state) 541 | } 542 | 543 | // Check for line spacing change 544 | let lineIndex: Int = self.lineSpacingPopup.indexOfSelectedItem 545 | var lineSpacing: CGFloat = 1.0 546 | switch(lineIndex) { 547 | case 1: 548 | lineSpacing = 1.15 549 | case 2: 550 | lineSpacing = 1.5 551 | case 3: 552 | lineSpacing = 2.0 553 | default: 554 | lineSpacing = 1.0 555 | } 556 | 557 | if !haveChanged { 558 | haveChanged = (self.lineSpacing != lineSpacing) 559 | } 560 | 561 | // Check for and record font and style changes 562 | if let fontName: String = getPostScriptName(false) { 563 | if !haveChanged { 564 | haveChanged = (fontName != self.codeFontName) 565 | } 566 | } 567 | 568 | if let fontName: String = getPostScriptName(true) { 569 | if !haveChanged { 570 | haveChanged = (fontName != self.bodyFontName) 571 | } 572 | } 573 | 574 | // Check for and record a font size change 575 | if !haveChanged { 576 | haveChanged = (self.previewFontSize != BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[Int(self.fontSizeSlider.floatValue)]) 577 | } 578 | 579 | // Check for colour changes 580 | if let _ = self.displayColours["new_heads"] { 581 | haveChanged = true 582 | } 583 | 584 | if let _ = self.displayColours["new_code"] { 585 | haveChanged = true 586 | } 587 | 588 | if let _ = self.displayColours["new_links"] { 589 | haveChanged = true 590 | } 591 | 592 | if let _ = self.displayColours["new_quote"] { 593 | haveChanged = true 594 | } 595 | 596 | return haveChanged 597 | } 598 | 599 | 600 | /** 601 | Close the **Preferences** sheet and save any settings that have changed. 602 | 603 | FROM 1.2.0 604 | 605 | - Parameters: 606 | - sender: The source of the action. 607 | */ 608 | @IBAction private func doSavePreferences(sender: Any) { 609 | 610 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 611 | let newValue: CGFloat = BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[Int(self.fontSizeSlider.floatValue)] 612 | if newValue != self.previewFontSize { 613 | defaults.setValue(newValue, 614 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_SIZE) 615 | } 616 | 617 | var state: Bool = self.useLightCheckbox.state == .on 618 | if self.doShowLightBackground != state { 619 | defaults.setValue(state, 620 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_USE_LIGHT) 621 | } 622 | 623 | // FROM 1.3.0 624 | // Get the YAML checkbox value and update 625 | state = self.showFrontMatterCheckbox.state == .on 626 | if self.doShowFrontMatter != state { 627 | defaults.setValue(state, 628 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_SHOW_YAML) 629 | } 630 | 631 | // FROM 1.4.0 632 | // Get any font changes 633 | if let psname: String = getPostScriptName(false) { 634 | if psname != self.codeFontName { 635 | self.codeFontName = psname 636 | defaults.setValue(psname, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_FONT_NAME) 637 | } 638 | } 639 | 640 | if let psname = getPostScriptName(true) { 641 | if psname != self.bodyFontName { 642 | self.bodyFontName = psname 643 | defaults.setValue(psname, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_NAME) 644 | } 645 | } 646 | 647 | // FROM 1.5.0 648 | // Save the selected line spacing 649 | let lineIndex: Int = self.lineSpacingPopup.indexOfSelectedItem 650 | var lineSpacing: CGFloat = 1.0 651 | switch(lineIndex) { 652 | case 1: 653 | lineSpacing = 1.15 654 | case 2: 655 | lineSpacing = 1.5 656 | case 3: 657 | lineSpacing = 2.0 658 | default: 659 | lineSpacing = 1.0 660 | } 661 | 662 | if (self.lineSpacing != lineSpacing) { 663 | self.lineSpacing = lineSpacing 664 | defaults.setValue(lineSpacing, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINE_SPACE) 665 | } 666 | 667 | if let newColour: String = self.displayColours["new_heads"] { 668 | defaults.setValue(newColour, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_HEAD_COLOUR) 669 | } 670 | 671 | if let newColour: String = self.displayColours["new_code"] { 672 | defaults.setValue(newColour, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_COLOUR) 673 | } 674 | 675 | if let newColour: String = self.displayColours["new_links"] { 676 | defaults.setValue(newColour, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINK_COLOUR) 677 | } 678 | 679 | if let newColour: String = self.displayColours["new_quote"] { 680 | defaults.setValue(newColour, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_QUOTE_COLOUR) 681 | } 682 | } 683 | 684 | closePrefsWindow() 685 | } 686 | 687 | 688 | /** 689 | Called when the user selects a font from either list. 690 | 691 | FROM 1.4.0 692 | 693 | - Parameters: 694 | - sender: The source of the action. 695 | */ 696 | @IBAction private func doUpdateFonts(sender: Any) { 697 | 698 | let item: NSPopUpButton = sender as! NSPopUpButton 699 | setStylePopup(item == self.bodyFontPopup) 700 | //self.havePrefsChanged = true 701 | } 702 | 703 | 704 | /** 705 | When the font size slider is moved and released, this function updates the font size readout. 706 | 707 | FROM 1.2.0 708 | 709 | - Parameters: 710 | - sender: The source of the action. 711 | */ 712 | @IBAction private func doMoveSlider(sender: Any) { 713 | 714 | let index: Int = Int(self.fontSizeSlider.floatValue) 715 | self.fontSizeLabel.stringValue = "\(Int(BUFFOON_CONSTANTS.FONT_SIZE_OPTIONS[index]))pt" 716 | //self.havePrefsChanged = true 717 | } 718 | 719 | 720 | /** 721 | Generic IBAction for any Prefs control to register it has been used. 722 | 723 | - Parameters: 724 | - sender: The source of the action. 725 | */ 726 | @IBAction private func controlClicked(sender: Any) { 727 | 728 | //self.havePrefsChanged = true 729 | } 730 | 731 | 732 | /** 733 | Update the colour preferences dictionary with a value from the 734 | colour well when a colour is chosen. 735 | FROM 1.5.0 736 | 737 | - Parameters: 738 | - sender: The source of the action. 739 | */ 740 | @objc @IBAction private func colourSelected(sender: Any) { 741 | 742 | let keys: [String] = ["heads", "code", "links", "quote"] 743 | let key: String = "new_" + keys[self.colourSelectionPopup.indexOfSelectedItem] 744 | self.displayColours[key] = self.headColourWell.color.hexString 745 | //self.havePrefsChanged = true 746 | } 747 | 748 | 749 | /** 750 | Update the colour well with the stored colour: either a new one, previously 751 | chosen, or the loaded preference. 752 | FROM 1.5.0 753 | 754 | - Parameters: 755 | - sender: The source of the action. 756 | */ 757 | @IBAction private func doChooseColourType(sender: Any) { 758 | 759 | let keys: [String] = ["heads", "code", "links", "quote"] 760 | let key: String = keys[self.colourSelectionPopup.indexOfSelectedItem] 761 | 762 | // If there's no `new_xxx` key, the next line will evaluate to false 763 | if let colour: String = self.displayColours["new_" + key] { 764 | if colour.count != 0 { 765 | // Set the colourwell with the updated colour and exit 766 | self.headColourWell.color = NSColor.hexToColour(colour) 767 | return 768 | } 769 | } 770 | 771 | // Set the colourwell with the stored colour 772 | if let colour: String = self.displayColours[key] { 773 | self.headColourWell.color = NSColor.hexToColour(colour) 774 | } 775 | } 776 | 777 | 778 | /** 779 | Zap any temporary colour values. 780 | FROM 1.5.0 781 | 782 | */ 783 | private func clearNewColours() { 784 | 785 | let keys: [String] = ["heads", "code", "links", "quote"] 786 | for key in keys { 787 | if let _: String = self.displayColours["new_" + key] { 788 | self.displayColours["new_" + key] = nil 789 | } 790 | } 791 | } 792 | 793 | 794 | 795 | 796 | // MARK: - What's New Functions 797 | /** 798 | Show the **What's New** sheet. 799 | 800 | If we're on a new, non-patch version, of the user has explicitly 801 | asked to see it with a menu click See if we're coming from a menu click 802 | (`sender != self`) or directly in code from *appDidFinishLoading()* 803 | (`sender == self`) 804 | 805 | - Parameters: 806 | - sender: The source of the action. 807 | */ 808 | @IBAction private func doShowWhatsNew(_ sender: Any) { 809 | 810 | // See if we're coming from a menu click (sender != self) or 811 | // directly in code from 'appDidFinishLoading()' (sender == self) 812 | var doShowSheet: Bool = type(of: self) != type(of: sender) 813 | 814 | if !doShowSheet { 815 | // We are coming from the 'appDidFinishLoading()' so check 816 | // if we need to show the sheet by the checking the prefs 817 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 818 | // Get the version-specific preference key 819 | let key: String = BUFFOON_CONSTANTS.PREFS_IDS.MAIN_WHATS_NEW + getVersion() 820 | doShowSheet = defaults.bool(forKey: key) 821 | } 822 | } 823 | 824 | // Configure and show the sheet 825 | if doShowSheet { 826 | // FROM 1.4.6 827 | // Disable menus we don't want used when the panel is open 828 | hidePanelGenerators() 829 | 830 | // First, get the folder path 831 | let htmlFolderPath = Bundle.main.resourcePath! + "/new" 832 | 833 | // Set WebView properties: limit scrollers and elasticity 834 | self.whatsNewWebView.enclosingScrollView?.hasHorizontalScroller = false 835 | self.whatsNewWebView.enclosingScrollView?.horizontalScrollElasticity = .none 836 | self.whatsNewWebView.enclosingScrollView?.verticalScrollElasticity = .none 837 | self.whatsNewWebView.configuration.suppressesIncrementalRendering = true 838 | 839 | // Just in case, make sure we can load the file 840 | if FileManager.default.fileExists(atPath: htmlFolderPath) { 841 | let htmlFileURL = URL.init(fileURLWithPath: htmlFolderPath + "/new.html") 842 | let htmlFolderURL = URL.init(fileURLWithPath: htmlFolderPath) 843 | self.whatsNewNav = self.whatsNewWebView.loadFileURL(htmlFileURL, allowingReadAccessTo: htmlFolderURL) 844 | } 845 | } 846 | } 847 | 848 | 849 | @IBAction private func doCloseWhatsNew(_ sender: Any) { 850 | 851 | // FROM 1.2.0 852 | // Close the 'What's New' sheet, making sure we clear the preference flag for this minor version, 853 | // so that the sheet is not displayed next time the app is run (unless the version changes) 854 | 855 | // Close the sheet 856 | self.window.endSheet(self.whatsNewWindow) 857 | 858 | // Scroll the web view back to the top 859 | self.whatsNewWebView.evaluateJavaScript("window.scrollTo(0,0)", completionHandler: nil) 860 | 861 | // Set this version's preference 862 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 863 | let key: String = BUFFOON_CONSTANTS.PREFS_IDS.MAIN_WHATS_NEW + getVersion() 864 | defaults.setValue(false, forKey: key) 865 | 866 | #if DEBUG 867 | print("\(key) reset back to true") 868 | defaults.setValue(true, forKey: key) 869 | #endif 870 | } 871 | 872 | // FROM 1.4.6 873 | // Restore menus 874 | showPanelGenerators() 875 | } 876 | 877 | 878 | // MARK: - Misc Functions 879 | 880 | /** 881 | Configure the app's preferences with default values. 882 | 883 | FROM 1.2.0 884 | */ 885 | private func registerPreferences() { 886 | 887 | if let defaults = UserDefaults(suiteName: self.appSuiteName) { 888 | // Check if each preference value exists -- set if it doesn't 889 | // Preview body font size, stored as a CGFloat 890 | // Default: 16.0 891 | let bodyFontSizeDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_SIZE) 892 | if bodyFontSizeDefault == nil { 893 | defaults.setValue(CGFloat(BUFFOON_CONSTANTS.PREVIEW_FONT_SIZE), 894 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_SIZE) 895 | } 896 | 897 | // Thumbnail view base font size, stored as a CGFloat, not currently used 898 | // Default: 14.0 899 | let thumbFontSizeDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.THUMB_FONT_SIZE) 900 | if thumbFontSizeDefault == nil { 901 | defaults.setValue(CGFloat(BUFFOON_CONSTANTS.THUMBNAIL_FONT_SIZE), 902 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.THUMB_FONT_SIZE) 903 | } 904 | 905 | // Use light background even in dark mode, stored as a bool 906 | // Default: false 907 | let useLightDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_USE_LIGHT) 908 | if useLightDefault == nil { 909 | defaults.setValue(false, 910 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_USE_LIGHT) 911 | } 912 | 913 | // Show the What's New sheet 914 | // Default: true 915 | // This is a version-specific preference suffixed with, eg, '-2-3'. Once created 916 | // this will persist, but with each new major and/or minor version, we make a 917 | // new preference that will be read by 'doShowWhatsNew()' to see if the sheet 918 | // should be shown this run 919 | let key: String = BUFFOON_CONSTANTS.PREFS_IDS.MAIN_WHATS_NEW + getVersion() 920 | let showNewDefault: Any? = defaults.object(forKey: key) 921 | if showNewDefault == nil { 922 | defaults.setValue(true, forKey: key) 923 | } 924 | 925 | // FROM 1.3.0 926 | // Show any YAML front matter, if present 927 | // Default: true 928 | let showFrontMatterDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_SHOW_YAML) 929 | if showFrontMatterDefault == nil { 930 | defaults.setValue(true, forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_SHOW_YAML) 931 | } 932 | 933 | // FROM 1.4.0 934 | // Colour of links in the preview, stored as hex string 935 | let linkColourDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINK_COLOUR) 936 | if linkColourDefault == nil { 937 | defaults.setValue(BUFFOON_CONSTANTS.LINK_COLOUR_HEX, 938 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINK_COLOUR) 939 | } 940 | 941 | // FROM 1.4.0 942 | // Colour of code blocks in the preview, stored as hex string 943 | let codeColourDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_COLOUR) 944 | if codeColourDefault == nil { 945 | defaults.setValue(BUFFOON_CONSTANTS.CODE_COLOUR_HEX, 946 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_COLOUR) 947 | } 948 | 949 | // FROM 1.4.0 950 | // Colour of headings in the preview, stored as hex string 951 | let headColourDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_HEAD_COLOUR) 952 | if headColourDefault == nil { 953 | defaults.setValue(BUFFOON_CONSTANTS.HEAD_COLOUR_HEX, 954 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_HEAD_COLOUR) 955 | } 956 | 957 | // FROM 1.4.0 958 | // Font for body test in the preview, stored as a PostScript name 959 | let bodyFontDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_NAME) 960 | if bodyFontDefault == nil { 961 | defaults.setValue(BUFFOON_CONSTANTS.BODY_FONT_NAME, 962 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_BODY_FONT_NAME) 963 | } 964 | 965 | // FROM 1.4.0 966 | // Font for code blocks in the preview, stored as a PostScript name 967 | let codeFontDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_FONT_NAME) 968 | if codeFontDefault == nil { 969 | defaults.setValue(BUFFOON_CONSTANTS.CODE_FONT_NAME, 970 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_CODE_FONT_NAME) 971 | } 972 | 973 | // FROM 1.5.0 974 | // Store the preview line spacing value 975 | let lineSpacingDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINE_SPACE) 976 | if lineSpacingDefault == nil { 977 | defaults.setValue(BUFFOON_CONSTANTS.BASE_LINE_SPACING, 978 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_LINE_SPACE) 979 | } 980 | 981 | // The blockquote colour, stored as hex string 982 | let quoteColourDefault: Any? = defaults.object(forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_QUOTE_COLOUR) 983 | if quoteColourDefault == nil { 984 | defaults.setValue(BUFFOON_CONSTANTS.QUOTE_COLOUR_HEX, 985 | forKey: BUFFOON_CONSTANTS.PREFS_IDS.PREVIEW_QUOTE_COLOUR) 986 | } 987 | } 988 | } 989 | 990 | } 991 | 992 | -------------------------------------------------------------------------------- /PreviewMarkdown/Constants.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Constants.swift 3 | * PreviewMarkdown 4 | * 5 | * Created by Tony Smith on 12/08/2020. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | // Combine the app's various constants into a struct 11 | import Foundation 12 | 13 | 14 | struct BUFFOON_CONSTANTS { 15 | 16 | struct ERRORS { 17 | 18 | struct CODES { 19 | static let NONE = 0 20 | static let FILE_INACCESSIBLE = 400 21 | static let FILE_WONT_OPEN = 401 22 | static let BAD_MD_STRING = 402 23 | static let BAD_TS_STRING = 403 24 | } 25 | 26 | struct MESSAGES { 27 | static let NO_ERROR = "No error" 28 | static let FILE_INACCESSIBLE = "Can't access file" 29 | static let FILE_WONT_OPEN = "Can't open file" 30 | static let BAD_MD_STRING = "Can't get markdown data" 31 | static let BAD_TS_STRING = "Can't access NSTextView's TextStorage" 32 | } 33 | } 34 | 35 | struct THUMBNAIL_SIZE { 36 | 37 | static let ORIGIN_X = 0 38 | static let ORIGIN_Y = 0 39 | static let WIDTH = 768 40 | static let HEIGHT = 1024 41 | static let ASPECT = 0.75 42 | static let TAG_HEIGHT = 204.8 43 | static let FONT_SIZE = 130.0 44 | } 45 | 46 | static let PREVIEW_FONT_SIZE = 16.0 47 | static let THUMBNAIL_FONT_SIZE: Float = 18.0 48 | static let SPACES_FOR_A_TAB = 4 49 | 50 | // FROM 1.2.0 51 | static let CODE_COLOUR_INDEX = 0 52 | static let LINK_COLOUR_INDEX = 2 53 | static let CODE_FONT_INDEX = 0 54 | static let BODY_FONT_INDEX = 0 55 | static let FONT_SIZE_OPTIONS: [CGFloat] = [10.0, 12.0, 14.0, 16.0, 18.0, 24.0, 28.0] 56 | 57 | // FROM 1.3.0 58 | static let YAML_INDENT = 2 59 | 60 | // FROM 1.3.1 61 | static let URL_MAIN = "https://smittytone.net/previewmarkdown/index.html" 62 | static let APP_STORE = "https://apps.apple.com/us/app/previewmarkdown/id1492280469" 63 | static let SUITE_NAME = ".suite.previewmarkdown" 64 | 65 | static let TAG_TEXT_SIZE = 180 //124 66 | static let TAG_TEXT_MIN_SIZE = 118 67 | 68 | // FROM 1.4.0 69 | static let HEAD_COLOUR_HEX = "941751FF" 70 | static let CODE_COLOUR_HEX = "00FF00FF" 71 | static let BODY_FONT_NAME = "System" 72 | // FROM 1.5.0 -- Change default font: Courier not included with macOS now 73 | static let CODE_FONT_NAME = "AndaleMono" 74 | static let LINK_COLOUR_HEX = "0096FFFF" 75 | static let QUOTE_COLOUR_HEX = "22528EFF" 76 | 77 | static let SAMPLE_UTI_FILE = "sample.md" 78 | 79 | // FROM 1.4.1 80 | static let THUMBNAIL_LINE_COUNT = 40 81 | 82 | // FROM 1.4.3 83 | static let APP_CODE_PREVIEWER = "com.bps.PreviewMarkdown.Previewer" 84 | 85 | // FROM 1.4.6 86 | struct APP_URLS { 87 | 88 | static let PM = "https://apps.apple.com/us/app/previewmarkdown/id1492280469?ls=1" 89 | static let PC = "https://apps.apple.com/us/app/previewcode/id1571797683?ls=1" 90 | static let PY = "https://apps.apple.com/us/app/previewyaml/id1564574724?ls=1" 91 | static let PJ = "https://apps.apple.com/us/app/previewjson/id6443584377?ls=1" 92 | static let PT = "https://apps.apple.com/us/app/previewtext/id1660037028?ls=1" 93 | } 94 | 95 | // FROM 1.5.0 96 | static let SYS_LAUNCH_SERVICES = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" 97 | static let BASE_LINE_SPACING = 1.0 98 | 99 | struct PREFS_IDS { 100 | 101 | static let MAIN_WHATS_NEW = "com-bps-previewmarkdown-do-show-whats-new-" 102 | 103 | static let PREVIEW_BODY_FONT_SIZE = "com-bps-previewmarkdown-base-font-size" 104 | static let PREVIEW_BODY_FONT_NAME = "com-bps-previewmarkdown-body-font-name" 105 | static let PREVIEW_CODE_FONT_NAME = "com-bps-previewmarkdown-code-font-name" 106 | static let PREVIEW_USE_LIGHT = "com-bps-previewmarkdown-do-use-light" 107 | static let PREVIEW_SHOW_YAML = "com-bps-previewmarkdown-do-show-front-matter" 108 | static let PREVIEW_LINK_COLOUR = "com-bps-previewmarkdown-link-colour-hex" 109 | static let PREVIEW_CODE_COLOUR = "com-bps-previewmarkdown-code-colour-hex" 110 | static let PREVIEW_HEAD_COLOUR = "com-bps-previewmarkdown-head-colour-hex" 111 | static let PREVIEW_QUOTE_COLOUR = "com-bps-previewmarkdown-quote-colour-hex" 112 | static let PREVIEW_LINE_SPACE = "com-bps-previewmarkdown-line-spacing" 113 | 114 | static let THUMB_FONT_SIZE = "com-bps-previewmarkdown-thumb-font-size" 115 | static let THUMB_SHOW_TAG = "com-bps-previewmarkdown-do-show-tag" 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /PreviewMarkdown/GenericColorExtensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * GenericColorExtension.swift 3 | * PreviewApps 4 | * 5 | * Created by Tony Smith on 18/06/2021. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Foundation 11 | import Cocoa 12 | 13 | 14 | extension NSColor { 15 | 16 | /** 17 | Convert a colour's internal representation into an RGB+A hex string. 18 | */ 19 | var hexString: String { 20 | 21 | guard let rgbColour = usingColorSpace(.sRGB) else { 22 | return BUFFOON_CONSTANTS.CODE_COLOUR_HEX 23 | } 24 | 25 | let red: Int = Int(round(rgbColour.redComponent * 0xFF)) 26 | let green: Int = Int(round(rgbColour.greenComponent * 0xFF)) 27 | let blue: Int = Int(round(rgbColour.blueComponent * 0xFF)) 28 | let alpha: Int = Int(round(rgbColour.alphaComponent * 0xFF)) 29 | 30 | let hexString: NSString = NSString(format: "%02X%02X%02X%02X", red, green, blue, alpha) 31 | return hexString as String 32 | } 33 | 34 | 35 | /** 36 | Generate a new NSColor from an RGB+A hex string.. 37 | 38 | - Parameters: 39 | - hex: The RGB+A hex string, eg.`AABBCCFF`. 40 | 41 | - Returns: An NSColor instance. 42 | */ 43 | static func hexToColour(_ hex: String) -> NSColor { 44 | 45 | if hex.count != 8 { 46 | return NSColor.red 47 | } 48 | 49 | func hexToFloat(_ hs: String) -> CGFloat { 50 | return CGFloat(UInt8(hs, radix: 16) ?? 0) 51 | } 52 | 53 | let hexns: NSString = hex as NSString 54 | let red: CGFloat = hexToFloat(hexns.substring(with: NSRange.init(location: 0, length: 2))) / 255 55 | let green: CGFloat = hexToFloat(hexns.substring(with: NSRange.init(location: 2, length: 2))) / 255 56 | let blue: CGFloat = hexToFloat(hexns.substring(with: NSRange.init(location: 4, length: 2))) / 255 57 | let alpha: CGFloat = hexToFloat(hexns.substring(with: NSRange.init(location: 6, length: 2))) / 255 58 | return NSColor.init(srgbRed: red, green: green, blue: blue, alpha: alpha) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /PreviewMarkdown/GenericExtensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * GenericExtensions.swift 3 | * PreviewApps 4 | * 5 | * These functions can be used by all PreviewApps 6 | * 7 | * Created by Tony Smith on 18/06/2021. 8 | * Copyright © 2024 Tony Smith. All rights reserved. 9 | */ 10 | 11 | 12 | import Foundation 13 | import Cocoa 14 | import WebKit 15 | import UniformTypeIdentifiers 16 | 17 | 18 | extension AppDelegate { 19 | 20 | // MARK: - Process Handling Functions 21 | 22 | /** 23 | Generic macOS process creation and run function. 24 | 25 | Make sure we clear the preference flag for this minor version, so that 26 | the sheet is not displayed next time the app is run (unless the version changes) 27 | 28 | - Parameters: 29 | - app: The location of the app. 30 | - with: Array of arguments to pass to the app. 31 | 32 | - Returns: `true` if the operation was successful, otherwise `false`. 33 | */ 34 | internal func runProcess(app path: String, with args: [String]) -> Bool { 35 | 36 | let task: Process = Process() 37 | task.executableURL = URL.init(fileURLWithPath: path) 38 | task.arguments = args 39 | 40 | // Pipe out the output to avoid putting it in the log 41 | let outputPipe = Pipe() 42 | task.standardOutput = outputPipe 43 | task.standardError = outputPipe 44 | 45 | do { 46 | try task.run() 47 | } catch { 48 | return false 49 | } 50 | 51 | // Block until the task has completed (short tasks ONLY) 52 | task.waitUntilExit() 53 | 54 | if !task.isRunning { 55 | if (task.terminationStatus != 0) { 56 | // Command failed -- collect the output if there is any 57 | let outputHandle = outputPipe.fileHandleForReading 58 | var outString: String = "" 59 | if let line = String(data: outputHandle.availableData, encoding: String.Encoding.utf8) { 60 | outString = line 61 | } 62 | 63 | if outString.count > 0 { 64 | print("\(outString)") 65 | } else { 66 | print("Error", "Exit code \(task.terminationStatus)") 67 | } 68 | return false 69 | } 70 | } 71 | 72 | return true 73 | } 74 | 75 | 76 | // MARK: - Finder Database Reset Functions 77 | 78 | internal func warnUserAboutReset() { 79 | 80 | // Hide panel-opening menus 81 | self.hidePanelGenerators() 82 | 83 | // Warn the user about the risks (minor) 84 | let alert: NSAlert = showAlert("Are you sure you wish to reset Finder’s UTI database?", 85 | "Resetting Finder’s Uniform Type Identifier (UTI) database may result in unexpected associations between files and apps, but it can also fix situations where previews are not being shown after you have first logged out of your Mac. Logging out of your Mac fixes most issues where previews are not being correctly shown and should be tried first.", 86 | false) 87 | alert.addButton(withTitle: "Go Back") 88 | alert.addButton(withTitle: "Continue") 89 | 90 | // Show the alert 91 | alert.beginSheetModal(for: self.window) { (resp) in 92 | 93 | // Close alert and restore menus 94 | alert.window.close() 95 | self.showPanelGenerators() 96 | 97 | // If the user wants to continue, perform the reset 98 | if resp == .alertSecondButtonReturn { 99 | // Perform the reset 100 | self.doResetFinderDatabase() 101 | } 102 | } 103 | } 104 | 105 | 106 | /** 107 | Reset Finder's launch services database using a sub-process. 108 | */ 109 | internal func doResetFinderDatabase() { 110 | 111 | // Perform the Finder reset 112 | // NOTE Cannot access the system domain from within the Sandbox 113 | let success: Bool = runProcess(app: BUFFOON_CONSTANTS.SYS_LAUNCH_SERVICES, 114 | with: ["-kill", "-f", "-r", "-domain", "user", "-domain", "local"]) 115 | if !success { 116 | let alert: NSAlert = showAlert("Sorry, the operation failed", "The Finder database could not be reset at this time") 117 | alert.alertStyle = .critical 118 | alert.beginSheetModal(for: self.window) 119 | } else { 120 | let alert: NSAlert = showAlert("Finder’s database was reset", "") 121 | alert.beginSheetModal(for: self.window) 122 | } 123 | } 124 | 125 | 126 | // MARK: - Misc Functions 127 | 128 | /** 129 | Present an error message specific to sending feedback. 130 | 131 | This is called from multiple locations: if the initial request can't be created, 132 | there was a send failure, or a server error. 133 | */ 134 | internal func sendFeedbackError() { 135 | 136 | let alert: NSAlert = showAlert("Feedback Could Not Be Sent", 137 | "Unfortunately, your comments could not be send at this time. Please try again later.") 138 | alert.beginSheetModal(for: self.reportWindow) { (resp) in 139 | self.window.endSheet(self.reportWindow) 140 | self.showPanelGenerators() 141 | } 142 | } 143 | 144 | 145 | /** 146 | Generic alert generator. 147 | 148 | - Parameters: 149 | - head: The alert's title. 150 | - message: The alert's message. 151 | - addOkButton: Should we add an OK button? 152 | 153 | - Returns: The NSAlert. 154 | */ 155 | internal func showAlert(_ head: String, _ message: String, _ addOkButton: Bool = true) -> NSAlert { 156 | 157 | let alert: NSAlert = NSAlert() 158 | alert.messageText = head 159 | alert.informativeText = message 160 | if addOkButton { alert.addButton(withTitle: "OK") } 161 | return alert 162 | } 163 | 164 | 165 | /** 166 | Build a basic 'major-manor' version string for prefs usage. 167 | 168 | - Returns: The version string. 169 | */ 170 | internal func getVersion() -> String { 171 | 172 | let version: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 173 | let parts: [String] = (version as NSString).components(separatedBy: ".") 174 | return parts[0] + "-" + parts[1] 175 | } 176 | 177 | 178 | /** 179 | Build a date string string for feedback usage. 180 | 181 | - Returns: The date string. 182 | */ 183 | internal func getDateForFeedback() -> String { 184 | 185 | let date: Date = Date() 186 | let dateFormatter: DateFormatter = DateFormatter() 187 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 188 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 189 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 190 | return dateFormatter.string(from: date) 191 | } 192 | 193 | 194 | /** 195 | Build a user-agent string string for feedback usage. 196 | 197 | - Returns: The user-agent string. 198 | */ 199 | internal func getUserAgentForFeedback() -> String { 200 | 201 | // Refactor code out into separate function for clarity 202 | 203 | let sysVer: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion 204 | let bundle: Bundle = Bundle.main 205 | let app: String = bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as! String 206 | let version: String = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 207 | let build: String = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as! String 208 | return "\(app)/\(version)-\(build) (macOS/\(sysVer.majorVersion).\(sysVer.minorVersion).\(sysVer.patchVersion))" 209 | } 210 | 211 | 212 | /** 213 | Read back the host system's registered UTI for the specified file. 214 | 215 | This is not PII. It used solely for debugging purposes 216 | 217 | - Parameters: 218 | - filename: The file we'll use to get the UTI. 219 | 220 | - Returns: The file's UTI. 221 | */ 222 | internal func getLocalFileUTI(_ filename: String) -> String { 223 | 224 | var localUTI: String = "NONE" 225 | let samplePath = Bundle.main.resourcePath! + "/" + filename 226 | 227 | if FileManager.default.fileExists(atPath: samplePath) { 228 | // Create a URL reference to the sample file 229 | let sampleURL = URL.init(fileURLWithPath: samplePath) 230 | 231 | do { 232 | // Read back the UTI from the URL 233 | // Use Big Sur's UTType API 234 | if #available(macOS 11, *) { 235 | if let uti: UTType = try sampleURL.resourceValues(forKeys: [.contentTypeKey]).contentType { 236 | localUTI = uti.identifier 237 | } 238 | } else { 239 | // NOTE '.typeIdentifier' yields an optional 240 | if let uti: String = try sampleURL.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier { 241 | localUTI = uti 242 | } 243 | } 244 | } catch { 245 | // NOP 246 | } 247 | } 248 | 249 | return localUTI 250 | } 251 | 252 | 253 | /** 254 | Disable all panel-opening menu items. 255 | */ 256 | internal func hidePanelGenerators() { 257 | 258 | self.helpMenuReportBug.isEnabled = false 259 | self.helpMenuWhatsNew.isEnabled = false 260 | self.mainMenuSettings.isEnabled = false 261 | self.mainMenuResetFinder.isEnabled = false 262 | } 263 | 264 | 265 | /** 266 | Enable all panel-opening menu items. 267 | */ 268 | internal func showPanelGenerators() { 269 | 270 | self.helpMenuReportBug.isEnabled = true 271 | self.helpMenuWhatsNew.isEnabled = true 272 | self.mainMenuSettings.isEnabled = true 273 | self.mainMenuResetFinder.isEnabled = true 274 | } 275 | 276 | 277 | /** 278 | Determine whether the host Mac is in light mode. 279 | 280 | - Returns: `true` if the Mac is in light mode, otherwise `false`. 281 | */ 282 | internal func isMacInLightMode() -> Bool { 283 | 284 | let appearNameString: String = NSApp.effectiveAppearance.name.rawValue 285 | return (appearNameString == "NSAppearanceNameAqua") 286 | } 287 | 288 | 289 | // MARK: - URLSession Delegate Functions 290 | 291 | func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { 292 | 293 | // Some sort of connection error - report it 294 | self.connectionProgress.stopAnimation(self) 295 | sendFeedbackError() 296 | } 297 | 298 | 299 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 300 | 301 | // The operation to send the comment completed 302 | self.connectionProgress.stopAnimation(self) 303 | if let _ = error { 304 | // An error took place - report it 305 | sendFeedbackError() 306 | } else { 307 | // The comment was submitted successfully 308 | let alert: NSAlert = showAlert("Thanks For Your Feedback!", 309 | "Your comments have been received and we’ll take a look at them shortly.") 310 | alert.beginSheetModal(for: self.reportWindow) { (resp) in 311 | // Close the feedback window when the modal alert returns 312 | let _: Timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { timer in 313 | self.window.endSheet(self.reportWindow) 314 | self.showPanelGenerators() 315 | } 316 | } 317 | } 318 | } 319 | 320 | 321 | // MARK: - WKWebNavigation Delegate Functions 322 | 323 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 324 | 325 | // Asynchronously show the sheet once the HTML has loaded 326 | // (triggered by delegate method) 327 | 328 | if let nav = self.whatsNewNav { 329 | if nav == navigation { 330 | // Display the sheet 331 | Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { timer in 332 | timer.invalidate() 333 | self.window.beginSheet(self.whatsNewWindow, completionHandler: nil) 334 | } 335 | } 336 | } 337 | } 338 | } 339 | 340 | 341 | extension NSApplication { 342 | 343 | func isMacInLightMode() -> Bool { 344 | 345 | return (self.effectiveAppearance.name.rawValue == "NSAppearanceNameAqua") 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /PreviewMarkdown/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 | ITSAppUsesNonExemptEncryption 24 | 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | Copyright © 2024 Tony Smith. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | NSSupportsAutomaticTermination 36 | 37 | NSSupportsSuddenTermination 38 | 39 | UTImportedTypeDeclarations 40 | 41 | 42 | UTTypeConformsTo 43 | 44 | public.data 45 | public.content 46 | public.text 47 | public.plain-text 48 | public.markdown 49 | 50 | UTTypeDescription 51 | Markdown 52 | UTTypeIcons 53 | 54 | UTTypeIdentifier 55 | net.daringfireball.markdown 56 | UTTypeTagSpecification 57 | 58 | public.filename-extension 59 | 60 | md 61 | mdown 62 | markdown 63 | mdwn 64 | mdml 65 | multimarkdown 66 | mkd 67 | mmd 68 | ft 69 | 70 | public.mime-type 71 | 72 | text/markdown 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /PreviewMarkdown/PMFont.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * PMFont.swift 3 | * PreviewApps 4 | * 5 | * Created by Tony Smith on 02/07/2021. 6 | * Copyright © 2024 Tony Smith. All rights reserved. 7 | */ 8 | 9 | 10 | import Foundation 11 | 12 | /** 13 | Internal font record structure. 14 | */ 15 | 16 | struct PMFont { 17 | 18 | var postScriptName: String = "" 19 | var displayName: String = "" 20 | var styleName: String = "" 21 | var traits: UInt = 0 22 | var styles: [PMFont]? = nil 23 | } 24 | -------------------------------------------------------------------------------- /PreviewMarkdown/PMFontExtensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * GenericExtensions.swift 3 | * PreviewMarkdown 4 | * 5 | * These functions can be used by all PreviewApps 6 | * 7 | * Created by Tony Smith on 18/06/2021. 8 | * Copyright © 2024 Tony Smith. All rights reserved. 9 | */ 10 | 11 | 12 | import Foundation 13 | import Cocoa 14 | import WebKit 15 | import UniformTypeIdentifiers 16 | 17 | 18 | extension AppDelegate { 19 | 20 | // MARK: - Font Management 21 | 22 | /** 23 | Build a list of available fonts. 24 | 25 | Should be called asynchronously. Two sets created: monospace fonts and regular fonts. 26 | Requires 'bodyFonts' and 'codeFonts' to be set as instance properties. 27 | Comment out either of these, as required. 28 | 29 | The final font lists each comprise pairs of strings: the font's PostScript name 30 | then its display name. 31 | */ 32 | internal func asyncGetFonts() { 33 | 34 | var cf: [PMFont] = [] 35 | var bf: [PMFont] = [] 36 | 37 | let mono: UInt = NSFontTraitMask.fixedPitchFontMask.rawValue 38 | let bold: UInt = NSFontTraitMask.boldFontMask.rawValue 39 | let ital: UInt = NSFontTraitMask.italicFontMask.rawValue 40 | let symb: UInt = NSFontTraitMask.nonStandardCharacterSetFontMask.rawValue 41 | 42 | let fm: NSFontManager = NSFontManager.shared 43 | 44 | let families: [String] = fm.availableFontFamilies 45 | for family in families { 46 | // Remove known unwanted fonts 47 | if family.hasPrefix(".") || family.hasPrefix("Apple Braille") || family == "Apple Color Emoji" { 48 | continue 49 | } 50 | 51 | var isCodeFont: Bool = true 52 | 53 | // For each family, examine its fonts for suitable ones 54 | if let fonts: [[Any]] = fm.availableMembers(ofFontFamily: family) { 55 | // This will hold a font family: individual fonts will be added to 56 | // the 'styles' array 57 | var familyRecord: PMFont = PMFont.init() 58 | familyRecord.displayName = family 59 | 60 | for font: [Any] in fonts { 61 | let psname: String = font[0] as! String 62 | let traits: UInt = font[3] as! UInt 63 | var doUseFont: Bool = false 64 | 65 | if mono & traits != 0 { 66 | doUseFont = true 67 | } else if traits & bold == 0 && traits & ital == 0 && traits & symb == 0 { 68 | isCodeFont = false 69 | doUseFont = true 70 | } 71 | 72 | if doUseFont { 73 | // The font is good to use, so add it to the list 74 | var fontRecord: PMFont = PMFont.init() 75 | fontRecord.postScriptName = psname 76 | fontRecord.styleName = font[1] as! String 77 | fontRecord.traits = traits 78 | 79 | if familyRecord.styles == nil { 80 | familyRecord.styles = [] 81 | } 82 | 83 | familyRecord.styles!.append(fontRecord) 84 | } 85 | } 86 | 87 | if familyRecord.styles != nil && familyRecord.styles!.count > 0 { 88 | if isCodeFont { 89 | cf.append(familyRecord) 90 | } else { 91 | bf.append(familyRecord) 92 | } 93 | } 94 | } 95 | } 96 | 97 | DispatchQueue.main.async { 98 | self.bodyFonts = bf 99 | self.codeFonts = cf 100 | } 101 | } 102 | 103 | 104 | /** 105 | Build and enable the font style popup. 106 | 107 | - Parameters: 108 | - isBody: Whether we're handling body text font styles (`true`) or code font styles (`false`). Default: `true`. 109 | - styleName: The name of the selected style. Default: `nil`. 110 | */ 111 | internal func setStylePopup(_ isBody: Bool = true, _ styleName: String? = nil) { 112 | 113 | let selectedFamily: String = isBody ? self.bodyFontPopup.titleOfSelectedItem! : self.codeFontPopup.titleOfSelectedItem! 114 | let familyList: [PMFont] = isBody ? self.bodyFonts : self.codeFonts 115 | let targetPopup: NSPopUpButton = isBody ? self.bodyStylePopup : self.codeStylePopup 116 | targetPopup.removeAllItems() 117 | 118 | for family: PMFont in familyList { 119 | if selectedFamily == family.displayName { 120 | if let styles: [PMFont] = family.styles { 121 | targetPopup.isEnabled = true 122 | for style: PMFont in styles { 123 | targetPopup.addItem(withTitle: style.styleName) 124 | } 125 | 126 | if styleName != nil { 127 | targetPopup.selectItem(withTitle: styleName!) 128 | } 129 | 130 | break 131 | } 132 | } 133 | } 134 | 135 | // FROM 1.5.0 136 | // Select a style if none selected 137 | if targetPopup.selectedItem == nil { 138 | targetPopup.selectItem(at: 0); 139 | } 140 | } 141 | 142 | 143 | /** 144 | Select the font popup using the stored PostScript name 145 | of the user's chosen font. 146 | 147 | - Parameters: 148 | - postScriptName: The PostScript name of the font. 149 | - isBody: Whether we're handling body text font styles (`true`) or code font styles (`false`). 150 | */ 151 | internal func selectFontByPostScriptName(_ postScriptName: String, _ isBody: Bool) { 152 | 153 | let familyList: [PMFont] = isBody ? self.bodyFonts : self.codeFonts 154 | let targetPopup: NSPopUpButton = isBody ? self.bodyFontPopup : self.codeFontPopup 155 | 156 | for family: PMFont in familyList { 157 | if let styles: [PMFont] = family.styles { 158 | for style: PMFont in styles { 159 | if style.postScriptName == postScriptName { 160 | targetPopup.selectItem(withTitle: family.displayName) 161 | setStylePopup(isBody, style.styleName) 162 | break 163 | } 164 | } 165 | } 166 | } 167 | 168 | // FROM 1.5.0 169 | // Select a font if none selected 170 | if targetPopup.selectedItem == nil { 171 | targetPopup.selectItem(at: 0); 172 | } 173 | } 174 | 175 | 176 | /** 177 | Get the PostScript name from the selected family and style. 178 | 179 | - Parameters: 180 | - isBody: Whether we're handling body text font styles (`true`) or code font styles (`false`). 181 | 182 | - Returns: The PostScript name as a string, or nil. 183 | */ 184 | internal func getPostScriptName(_ isBody: Bool) -> String? { 185 | 186 | let familyList: [PMFont] = isBody ? self.bodyFonts : self.codeFonts 187 | let fontPopup: NSPopUpButton = isBody ? self.bodyFontPopup : self.codeFontPopup 188 | let stylePopup: NSPopUpButton = isBody ? self.bodyStylePopup : self.codeStylePopup 189 | 190 | if let selectedFont: String = fontPopup.titleOfSelectedItem { 191 | let selectedStyle: Int = stylePopup.indexOfSelectedItem 192 | 193 | for family: PMFont in familyList { 194 | if family.displayName == selectedFont { 195 | if let styles: [PMFont] = family.styles { 196 | let font: PMFont = styles[selectedStyle] 197 | return font.postScriptName 198 | } 199 | } 200 | } 201 | } 202 | 203 | return nil 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /PreviewMarkdown/PreviewMarkdown.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)suite.previewmarkdown 10 | 11 | com.apple.security.automation.apple-events 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.network.client 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /PreviewMarkdown/sample.md: -------------------------------------------------------------------------------- 1 | # H1 # 2 | 3 | Some body text. 4 | 5 | ## H2 ## 6 | 7 | *Some body text in italic.* 8 | 9 | ### H3 ### 10 | 11 | **Some body text in bold** 12 | 13 | #### H4 #### 14 | 15 | **Some body text in bold** and then not bold **and then bold again**. 16 | 17 | ``` 18 | code 19 | ``` 20 | 21 | ##### H5 ##### 22 | 23 | 1. A list 24 | 1. A list 25 | 1. A list 26 | 27 | ###### H6 ###### 28 | 29 | - A bullet list 30 | - A bullet list 31 | - A bullet list 32 | * A sub bullet list 33 | * A sub bullet list 34 | 35 | #### H4 Again 36 | 37 | * Point One 38 | * Point Two 39 | 40 | ## H2 Again ## 41 | 42 | Some `preformatted text` in `this line`. 43 | 44 | | A | B | C | 45 | | :-- | :-: | --: | 46 | | 1 | 2 | 3 | 47 | 48 | ### Code ### 49 | 50 | func showError(_ errString: String) { 51 | 52 | // Relay an error message to its various outlets 53 | 54 | NSLog("BUFFOON " + errString) 55 | self.errorReportField.stringValue = errString 56 | self.errorReportField.isHidden = false 57 | } 58 | -------------------------------------------------------------------------------- /PreviewMarkdownTests/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 | -------------------------------------------------------------------------------- /PreviewMarkdownTests/PreviewMarkdownTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewMarkdownTests.swift 3 | // PreviewMarkdownTests 4 | // 5 | // Created by Tony Smith on 18/09/2020. 6 | // Copyright © 2024 Tony Smith. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import AppKit 11 | 12 | @testable import PreviewMarkdown 13 | @testable import Previewer 14 | 15 | 16 | class PreviewMarkdownTests: XCTestCase { 17 | 18 | let appDelegate = NSApplication.shared.delegate as! AppDelegate 19 | let pvc: PreviewViewController = PreviewViewController() 20 | let cmn: Common = Common(false) 21 | 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | 28 | func testProcessSymbols() throws { 29 | 30 | var markdownString = "**™ ±  " 31 | var expectedString = "**™ ± " 32 | XCTAssert(cmn.processSymbols(markdownString) == expectedString) 33 | 34 | markdownString = "® ©" 35 | expectedString = "® ©" 36 | XCTAssert(cmn.processSymbols(markdownString) == expectedString) 37 | 38 | markdownString = "² > &trad." 39 | expectedString = "² > &trad." 40 | XCTAssert(cmn.processSymbols(markdownString) == expectedString) 41 | } 42 | 43 | 44 | func testProcessCode() throws { 45 | 46 | let markdownString = """ 47 | This is some text. 48 | 49 | ``` 50 | This is some code. 51 | ``` 52 | More text. 53 | """ 54 | 55 | let expectedString = """ 56 | This is some text. 57 | 58 | This is some code. 59 | More text. 60 | """ 61 | 62 | //print(processCodeTags(markdownString)) 63 | XCTAssert(cmn.processCodeTags(markdownString) == expectedString) 64 | } 65 | 66 | 67 | func testConvertSpaces() throws { 68 | 69 | var markdownString = """ 70 | This is some text. 71 | 1. Tab 72 | * Tab 73 | - Tab 74 | \t* Tab2 75 | Done 76 | """ 77 | 78 | var expectedString = """ 79 | This is some text. 80 | 1. Tab 81 | \t* Tab 82 | \t\t- Tab 83 | \t* Tab2 84 | Done 85 | """ 86 | 87 | //print(convertSpaces(markdownString)) 88 | XCTAssert(cmn.convertSpaces(markdownString) == expectedString) 89 | 90 | markdownString = " 11. Something" 91 | expectedString = "\t11. Something" 92 | XCTAssert(cmn.convertSpaces(markdownString) == expectedString) 93 | } 94 | 95 | 96 | func testProcessCheckboxes() throws { 97 | 98 | // Negative cases 99 | var markdownString = "[p]" 100 | XCTAssert(cmn.processCheckboxes(markdownString) == "[p]") 101 | 102 | markdownString = "[x]()" 103 | XCTAssert(cmn.processCheckboxes(markdownString) == "[x]()") 104 | 105 | // Positive cases 106 | markdownString = "[X]" 107 | XCTAssert(cmn.processCheckboxes(markdownString) == "✅") 108 | 109 | markdownString = "[x]" 110 | XCTAssert(cmn.processCheckboxes(markdownString) == "✅") 111 | 112 | markdownString = "[]" 113 | XCTAssert(cmn.processCheckboxes(markdownString) == "❎") 114 | 115 | markdownString = "[ ]" 116 | XCTAssert(cmn.processCheckboxes(markdownString) == "❎") 117 | } 118 | 119 | 120 | func testGetFrontMatter() throws { 121 | 122 | var markdownString = """ 123 | --- 124 | title: ASCII 125 | description: A macOS tool to help you design glyphs for 8x8 LED matrix displays 126 | slug: ascii 127 | logo: true 128 | preview: images/ascii/preview.png 129 | --- 130 | # Next 131 | """ 132 | 133 | var expectedString = """ 134 | title: ASCII 135 | description: A macOS tool to help you design glyphs for 8x8 LED matrix displays 136 | slug: ascii 137 | logo: true 138 | preview: images/ascii/preview.png 139 | 140 | """ 141 | XCTAssert(cmn.getFrontMatter(markdownString, #"^(-)+"#) == expectedString) 142 | 143 | markdownString = """ 144 | ------------ 145 | title: ASCII 146 | description: A macOS tool to help you design glyphs for 8x8 LED matrix displays 147 | slug: ascii 148 | logo: true 149 | preview: images/ascii/preview.png 150 | ------------ 151 | # Next 152 | """ 153 | 154 | XCTAssert(cmn.getFrontMatter(markdownString, #"^(-)+"#) == expectedString) 155 | 156 | markdownString = """ 157 | +++ 158 | title: ASCII 159 | description: A macOS tool to help you design glyphs for 8x8 LED matrix displays 160 | slug: ascii 161 | logo: true 162 | preview: images/ascii/preview.png 163 | +++ 164 | # Next 165 | """ 166 | 167 | XCTAssert(cmn.getFrontMatter(markdownString, #"^(\+)+"#) == expectedString) 168 | 169 | markdownString = """ 170 | 171 | 172 | 173 | 174 | ------------ 175 | title: ASCII 176 | description: A macOS tool to help you design glyphs for 8x8 LED matrix displays 177 | slug: ascii 178 | logo: true 179 | preview: images/ascii/preview.png 180 | ------------ 181 | # Next 182 | """ 183 | 184 | XCTAssert(cmn.getFrontMatter(markdownString, #"^(-)+"#) == expectedString) 185 | 186 | markdownString = """ 187 | # Next 188 | """ 189 | 190 | expectedString = "" 191 | 192 | XCTAssert(cmn.getFrontMatter(markdownString, #"^(-)+"#) == expectedString) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PreviewMarkdown 1.5.3 2 | 3 | This app provides [Markdown](https://daringfireball.net/projects/markdown/syntax) file preview and thumbnailing extensions for Catalina and later versions of macOS. 4 | 5 | ![PreviewMarkdown App Store QR code](qr-code.jpg) 6 | 7 | ## Installation and Usage 8 | 9 | Just run the host app once to register the extensions — you can quit the app as soon as it has launched. We recommend logging out of your Mac and back in again at this point. Now you can preview markdown documents using QuickLook (select an icon and hit Space), and Finder’s preview pane and **Info** panels. 10 | 11 | You can disable and re-enable the Previewer and Thumbnailer extensions at any time in **System Preferences > Extensions > Quick Look**. 12 | 13 | ### Adjusting the Preview 14 | 15 | You can alter some of the key elements of the preview by using the **Preferences** panel: 16 | 17 | - The base body font and text size. 18 | - The monospaced code font. 19 | - The colour of headlines, code, blockquotes and link text. 20 | - Whether YAML front matter should be displayed too. 21 | - Whether preview should be display white-on-black even in Dark Mode. 22 | 23 | Changing these settings will affect previews immediately, but may not affect thumbnails until you open a folder that has not been previously opened in the current login session. 24 | 25 | For more information on the background to this app, please see this [blog post](https://smittytone.wordpress.com/2019/11/07/create_previews_macos_catalina/). 26 | 27 | ### YAML Front Matter 28 | 29 | *PreviewMarkdown* supports rendering YAML front matter in Markdown files. To enable it, go to **Preview Markdown > Preferences...** and check the **Show YAML front matter** checkbox. YAML will appear in QuickLook previews only, not thumbnails. 30 | 31 | YAML front matter can be delimited with both `---` and `---`, and `---` and `...` start and end markers. 32 | 33 | ## Known Issues 34 | 35 | Users of Markdown editing tools like OneMarkdown and Marked may not see PreviewMarkdown-produced previews. This is because those apps claim ownership of key Markdown file UTIs which may cause Finder to pre-empt PreviewMarkdown. There is no workaround at this time. 36 | 37 | ## Source Code 38 | 39 | This repository contains the primary source code for *PreviewMarkdown*. Certain graphical assets, code components and data files are not included. To build *PreviewMarkdown* from scratch, you will need to add these files yourself or remove them from your fork. 40 | 41 | This includes minor changes made to the SwiftyMarkdown source — specifically to unset NSAttributedString’s `.link` attribute for links in order to prevent NSAttributedString applying its own link text formatting. 42 | 43 | The files `REPLACE_WITH_YOUR_FUNCTIONS` and `REPLACE_WITH_YOUR_CODES` must be replaced with your own files. The former will contain your `sendFeedback(_ feedback: String) -> URLSessionTask?` function. The latter your Developer Team ID, used as the App Suite identifier prefix. 44 | 45 | You will need to generate your own `Assets.xcassets` file containing the app icon. 46 | 47 | You will need to create your own `new` directory containing your own `new.html` file. 48 | 49 | ## Acknowledgements 50 | 51 | PreviewMarkdown’s app extensions contain [SwiftyMarkdown](https://github.com/SimonFairbairn/SwiftyMarkdown) by Simon Fairbairn and other contributors, and [YamlSwift](https://github.com/behrang/YamlSwift) by Behrang Noruzi Niya and other contributors. 52 | 53 | ## Release Notes 54 | 55 | - 1.5.3 *7 September 2024* 56 | - Improve settings change checking. 57 | - Correctly render the YAML frontmatter separator line: revert NSTextViews to TextKit 1 (previously bumped to TextKit 2 by Xcode). 58 | - 1.5.2 *13 May 2024* 59 | - Revise thumbnailer to improve memory utilization and efficiency. 60 | - 1.5.1 *2 November 2023* 61 | - Support the emerging `public.markdown` UTI. 62 | - Support YAML front matter that uses the `...` end marker (Thanks, anonymous). 63 | - Better **What’s New** dialog presentation in dark mode. 64 | - 1.5.0 *1 October 2023* 65 | - Use *PreviewApps*’ new preview element colour selection UI. 66 | - Allow link colours to be changed. 67 | - Allow blockquote colours to be changed. 68 | - Add line-spacing setting for previews. 69 | - Add link to help on **Preferences** panel. 70 | - Add experimental Finder UTI database reset option. 71 | - Rename extensions `Markdown Previewer` and `Markdown Thumbnailer`. 72 | - Improve font edge-case handling. 73 | - Fix link text formatting. 74 | - Remove dynamic UTIs. 75 | - 1.4.6 *21 January 2023* 76 | - Add link to [PreviewText](https://smittytone.net/previewtext/index.html). 77 | - Better menu handling when panels are visible. 78 | - Better app exit management. 79 | - 1.4.5 *23 December 2022* 80 | - Add UTI `com.nutstore.down`. 81 | - 1.4.4 *2 October 2022* 82 | - Fix UTI generation. 83 | - Add link to [PreviewJson](https://smittytone.net/previewjson/index.html). 84 | - 1.4.3 *26 August 2022* 85 | - Initial support for non-utf8 source code file encodings. 86 | - 1.4.2 *7 August 2022* 87 | - Upgrade to SwiftyMarkdown 1.2.4. 88 | - Support checkboxes (`[x]`, `[ ]`). 89 | - 1.4.1 *20 November 2021* 90 | - Disable selection of thumbnail tags under macOS 12 Monterey to avoid clash with system-added tags. 91 | - 1.4.0 *28 July 2021* 92 | - Allow any installed font to be selected. 93 | - Allow the heading colour to be selected. 94 | - Allow any colour to be chosen using macOS’ colour picker. 95 | - Tighten the thumbnailer code. 96 | - Fixed a rare bug in the previewer error reporting code. 97 | - 1.3.1 *18 June 2021* 98 | - Add links to other PreviewApps. 99 | - Support macOS 11 Big Sur’s UTType API. 100 | - Stability improvements. 101 | - 1.3.0 *9 May 2021* 102 | - Add optional presentation of YAML front matter to previews. 103 | - Recode Thumbnailer to make it thread safe: this should prevent crashes leading to generic or editor-specific thumbnail icons being seen. 104 | - Update user-agent string. 105 | - Minor code and UI improvements. 106 | - 1.2.0 *4 February 2021* 107 | - Add preview display preferences (requested by various anonymous feedback senders) 108 | - Add file type ident tag to thumbnails (requested by @chamiu). 109 | - Add **What’s New** sheet to be shown with new major/minor versions. 110 | - Include local markdown UTI with user-submitted feedback. 111 | - Add link for app reviews. 112 | - 1.1.4 *16 January 2021* 113 | - Add UTI `net.ia.markdown`. 114 | - 1.1.3 *14 January 2021* 115 | - Add UTI `pro.writer.markdown`. 116 | - 1.1.2 *18 November 2020* 117 | - Apple Silicon version included. 118 | - 1.1.1 *1 October 2020* 119 | - Add report bugs/send feedback mechanism. 120 | - Add usage advice to main window. 121 | - Handle markdown formatting not yet rendered by SwiftyMarkdown: three-tick code blocks, HTML symbols, space-inset lists. 122 | - 1.1.0 *25 September 2020* 123 | - Add macOS Big Sur support. 124 | - Better macOS dark/light mode support. 125 | - Migrate engine to [SwiftyMarkdown 1.2.3](https://github.com/SimonFairbairn/SwiftyMarkdown). 126 | - 1.0.5 *9 April 2020* 127 | - App Store release version. 128 | - 1.0.4 *Unreleased* 129 | - Minor cosmetic changes to app menus. 130 | - 1.0.3 *10 December 2019* 131 | - Add version number to app’s info panel. 132 | - 1.0.2 *4 December 2019* 133 | - Fix random crash (`string index out of range` in SwiftyMarkdown). 134 | - 1.0.1 *20 November 2019* 135 | - Correct thumbnailer styles. 136 | - 1.0.0 *8 November 2019* 137 | - Initial public release. 138 | 139 | ## Copyright and Credits ## 140 | 141 | Primary app code and UI design © 2024, Tony Smith. 142 | 143 | Code portions © 2022 Simon Fairbairn. Code portions © 2021 Behrang Noruzi Niya. 144 | -------------------------------------------------------------------------------- /RenderDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RenderDemo 4 | // 5 | // Created by Tony Smith on 10/07/2023. 6 | // Copyright © 2024 Tony Smith. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @main 12 | class AppDelegate: NSObject, 13 | NSApplicationDelegate, 14 | NSOpenSavePanelDelegate { 15 | 16 | // MARK: - Class UI Properies 17 | 18 | @IBOutlet var window: NSWindow! 19 | @IBOutlet var mainView: NSView! 20 | @IBOutlet var previewTextView: NSTextView! 21 | @IBOutlet var previewScrollView: NSScrollView! 22 | @IBOutlet var modeButton: NSButton! 23 | 24 | 25 | // MARK: - Private Properies 26 | 27 | private var openDialog: NSOpenPanel? = nil 28 | private var currentURL: URL? = nil 29 | private var renderAsDark: Bool = true 30 | private var renderIndents: Bool = false 31 | private var common: Common = Common.init(false) 32 | 33 | 34 | // MARK: - Class Lifecycle Functions 35 | 36 | func applicationDidFinishLaunching(_ notification: Notification) { 37 | 38 | // Set the mode button 39 | self.modeButton.state = self.renderAsDark ? .on : .off 40 | 41 | // Centre the main window and display 42 | self.window.center() 43 | self.window.makeKeyAndOrderFront(self) 44 | } 45 | 46 | 47 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 48 | 49 | // When the main window closed, shut down the app 50 | return true 51 | } 52 | 53 | 54 | // MARK: - Action Functions 55 | 56 | @IBAction private func doLoadFile(_ sender: Any) { 57 | 58 | self.openDialog = NSOpenPanel.init() 59 | self.openDialog!.canChooseFiles = true 60 | self.openDialog!.canChooseDirectories = false 61 | self.openDialog!.allowsMultipleSelection = false 62 | self.openDialog!.delegate = self 63 | self.openDialog!.directoryURL = URL.init(fileURLWithPath: "") 64 | 65 | if self.openDialog!.runModal() == .OK { 66 | self.currentURL = self.openDialog!.url 67 | let possibleError: NSError? = renderContent(self.openDialog!.url) 68 | if possibleError != nil { 69 | let errorAlert: NSAlert = NSAlert.init(error: possibleError!) 70 | errorAlert.beginSheetModal(for: self.window) 71 | } 72 | } 73 | 74 | self.openDialog = nil 75 | } 76 | 77 | 78 | @IBAction private func doSwitchMode(_ sender: Any) { 79 | 80 | self.renderAsDark = self.modeButton.state == .on 81 | doReRenderFile(self) 82 | } 83 | 84 | 85 | @IBAction private func doReRenderFile(_ sender: Any) { 86 | 87 | let possibleError: NSError? = renderContent(self.currentURL) 88 | if possibleError != nil { 89 | // Pop up an alert 90 | let errorAlert: NSAlert = NSAlert.init(error: possibleError!) 91 | errorAlert.beginSheetModal(for: self.window) 92 | } 93 | } 94 | 95 | 96 | // MARK: - Rendering Functions 97 | 98 | func renderContent(_ fileToRender: URL?) -> NSError? { 99 | 100 | var reportError: NSError? = nil 101 | 102 | do { 103 | if let mdUrl: URL = fileToRender { 104 | self.window.title = mdUrl.absoluteString 105 | 106 | // Get the file contents as a string 107 | let data: Data = try Data.init(contentsOf: mdUrl, options: [.uncached]) 108 | 109 | // Get the string's encoding, or fail back to .utf8 110 | let encoding: String.Encoding = data.stringEncoding ?? .utf8 111 | 112 | if let mdString: String = String.init(data: data, encoding: encoding) { 113 | common.doShowLightBackground = !self.renderAsDark 114 | 115 | // Get the key string first 116 | let mdAttString: NSAttributedString = common.getAttributedString(mdString, false) 117 | 118 | self.previewTextView.backgroundColor = common.doShowLightBackground ? NSColor.init(white: 1.0, alpha: 0.9) : NSColor.textBackgroundColor 119 | self.previewScrollView.scrollerKnobStyle = common.doShowLightBackground ? .dark : .light 120 | 121 | if let renderTextStorage: NSTextStorage = self.previewTextView.textStorage { 122 | renderTextStorage.beginEditing() 123 | renderTextStorage.setAttributedString(mdAttString) 124 | renderTextStorage.endEditing() 125 | return nil 126 | } 127 | 128 | // We can't access the preview NSTextView's NSTextStorage 129 | reportError = setError(BUFFOON_CONSTANTS.ERRORS.CODES.BAD_TS_STRING) 130 | } else { 131 | // We couldn't convert to data to a valid encoding 132 | let errDesc: String = "\(BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_TS_STRING) \(encoding)" 133 | reportError = NSError(domain: BUFFOON_CONSTANTS.APP_CODE_PREVIEWER, 134 | code: BUFFOON_CONSTANTS.ERRORS.CODES.BAD_MD_STRING, 135 | userInfo: [NSLocalizedDescriptionKey: errDesc]) 136 | } 137 | } else { 138 | // No file selected 139 | let errDesc: String = "No file selected to render" 140 | reportError = NSError(domain: BUFFOON_CONSTANTS.APP_CODE_PREVIEWER, 141 | code: BUFFOON_CONSTANTS.ERRORS.CODES.BAD_MD_STRING, 142 | userInfo: [NSLocalizedDescriptionKey: errDesc]) 143 | } 144 | } catch { 145 | // We couldn't read the file so set an appropriate error to report back 146 | reportError = setError(BUFFOON_CONSTANTS.ERRORS.CODES.FILE_WONT_OPEN) 147 | } 148 | 149 | return reportError 150 | } 151 | 152 | 153 | /** 154 | Generate an NSError for an internal error, specified by its code. 155 | 156 | Codes are listed in `Constants.swift` 157 | 158 | - Parameters: 159 | - code: The internal error code. 160 | 161 | - Returns: The described error as an NSError. 162 | */ 163 | func setError(_ code: Int) -> NSError { 164 | 165 | var errDesc: String 166 | 167 | switch(code) { 168 | case BUFFOON_CONSTANTS.ERRORS.CODES.FILE_INACCESSIBLE: 169 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.FILE_INACCESSIBLE 170 | case BUFFOON_CONSTANTS.ERRORS.CODES.FILE_WONT_OPEN: 171 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.FILE_WONT_OPEN 172 | case BUFFOON_CONSTANTS.ERRORS.CODES.BAD_TS_STRING: 173 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_TS_STRING 174 | case BUFFOON_CONSTANTS.ERRORS.CODES.BAD_MD_STRING: 175 | errDesc = BUFFOON_CONSTANTS.ERRORS.MESSAGES.BAD_MD_STRING 176 | default: 177 | errDesc = "UNKNOWN ERROR" 178 | } 179 | 180 | return NSError(domain: BUFFOON_CONSTANTS.APP_CODE_PREVIEWER, 181 | code: code, 182 | userInfo: [NSLocalizedDescriptionKey: errDesc]) 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /RenderDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RenderDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RenderDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RenderDemo/Base.lproj/MainMenu.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 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 187 | 188 | 189 | 190 | 191 | 192 | 206 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /RenderDemo/RenderDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)suite.previewmarkdown 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /qr-code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smittytone/PreviewMarkdown/c08da106683f8e493ab7427f8e7b664b689754af/qr-code.jpg --------------------------------------------------------------------------------