├── .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 |
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 |
--------------------------------------------------------------------------------
/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 | 
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 |
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 |
184 |
185 |
186 |
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
--------------------------------------------------------------------------------