Skip to content

Commit 6c9c510

Browse files
committed
Command line interface (CLI) tool.
1 parent ce9b67d commit 6c9c510

File tree

11 files changed

+571
-27
lines changed

11 files changed

+571
-27
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22
=======
33

4+
### 1.0.3 (28)
5+
New features:
6+
- Command line interface (CLI) tool.
7+
48
### 1.0.2 (27)
59
Bugfix:
610
- Fenced code block highlighted even when rmd syntax is used (language name inside a curly brace).

QLMarkdown.xcodeproj/project.pbxproj

+296-11
Large diffs are not rendered by default.

QLMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

QLMarkdown/Settings.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Settings {
3636

3737
static let shared = Settings()
3838
static let factorySettings = Settings(noInitFromDefault: true)
39+
static var appBundleUrl: URL?
3940

4041
@objc var autoLinkExtension: Bool = true
4142
@objc var checkboxExtension: Bool = false
@@ -112,12 +113,12 @@ class Settings {
112113
}
113114

114115
@objc func handleSettingsChanged(_ notification: NSNotification) {
115-
print("settings changed")
116+
// print("settings changed")
116117
self.initFromDefaults()
117118
}
118119

119120
func initFromDefaults() {
120-
print("Shared preferences stored in \(Settings.applicationSupportUrl?.path ?? "??").")
121+
// print("Shared preferences stored in \(Settings.applicationSupportUrl?.path ?? "??").")
121122

122123
let defaults = UserDefaults.standard
123124
// let d = UserDefaults(suiteName: Settings.Domain)
@@ -331,7 +332,9 @@ class Settings {
331332
/// Get the Bundle with the resources.
332333
/// For the host app return the main Bundle. For the appex return the bundle of the hosting app.
333334
func getResourceBundle() -> Bundle {
334-
if Bundle.main.bundlePath.hasSuffix(".appex") {
335+
if let url = Settings.appBundleUrl, let appBundle = Bundle(url: url) {
336+
return appBundle
337+
} else if Bundle.main.bundlePath.hasSuffix(".appex") {
335338
// this is an app extension
336339
let url = Bundle.main.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
337340

@@ -429,7 +432,7 @@ class Settings {
429432
return renderYaml(yaml)
430433
}
431434
} catch {
432-
print(error)
435+
// print(error)
433436
}
434437
}
435438
// Embed the header inside a yaml block.
@@ -924,6 +927,7 @@ table.debug td {
924927
if self.syntaxHighlightExtension, let ext = cmark_find_syntax_extension("syntaxhighlight"), cmark_syntax_extension_highlight_get_rendered_count(ext) > 0 {
925928
let theme = String(cString: cmark_syntax_extension_highlight_get_theme_name(ext))
926929
if !theme.isEmpty, let p = cmark_syntax_extension_get_style(ext) {
930+
// Embed the theme style.
927931
let font = self.syntaxFontFamily
928932
css_highlight = String(cString: p)
929933
if font != "" {

README.md

+33-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
# QLMarkdown
66

7-
QLMarkdown is a macOS Quick Look extension to preview Markdown files. It can also preview textbundle packages and rmarkdown (`.rmd`) files.
8-
9-
RMarkdown files are rendered as normal markdown _without_ evaluating `r` code. The header is rendered as a `yaml` code block.
7+
QLMarkdown is a macOS Quick Look extension to preview Markdown files. It can also preview textbundle packages and rmarkdown (`.rmd`) files (_without_ evaluating `r` code).
108

119
> **Please note that this software is provided "as is", without any warranty of any kind.**
1210
@@ -30,7 +28,9 @@ You can download the last compiled release (as universal binary) from [this link
3028
- [Table](#table)
3129
- [Tag filter](#tag-filter)
3230
- [Task list](#task-list)
31+
- [YAML header](#yaml-header)
3332
- [Themes](#themes)
33+
- [Command line interface](#command-line-interface)
3434
- [Build from source](#build-from-source)
3535
- [Dependency](#dependency)
3636
- [Note about security](#note-about-security)
@@ -224,6 +224,35 @@ The custom style is appended after the CSS used for the source code. In this way
224224

225225
Syntax highlighting extension allow to customize the appearance of the code blocks.
226226

227+
228+
## Command line interface
229+
230+
A `qlmarkdown_cli` command line interface (CLI) is available to perform batch conversion of markdown files.
231+
232+
The tool is located inside the `QLMarkdown.app/Contents/Resources` folder (and should not be moved outside).
233+
234+
```
235+
Usage: qlmarkdown_cli [--app <path>] [-o <file|dir>] <file> [..]
236+
237+
Arguments:
238+
-h Show this help and exit.
239+
-o <file|dir> Destination output. If you pass a directory, a new file is
240+
created with the name of the processed source with html extension.
241+
The destination file is always overwritten.
242+
If this argument is not provided, the output will be printed to the
243+
stdout.
244+
-v Verbose mode. Valid only with the -o option.
245+
246+
To handle multiple files at time you need to pass the -o arguments with a destination folder.
247+
```
248+
249+
The CLI interface uses the same settings as the Quick Look extension.
250+
251+
The CLI honors the inline image option _only_ for image defined with the markdown syntax.
252+
253+
Any relative paths (for example in the `src` attribute of an `<img>` tag) inside raw HTML fragments are not updated according to the destination folder.
254+
255+
227256
## Build from source
228257

229258
When you clone this repository, remember to fetch also the submodule with `git submodule update --init`.
@@ -248,7 +277,7 @@ On Big Sur there is a bug in the Quick Look engine and WebKit that cause the imm
248277

249278
## Note about the developer
250279

251-
I am not primarily an application developer, and I have no particular experience in programming in Swift and much less in C/C++. There may be possible bugs in the code, be patient.
280+
I am not primarily an application developer. There may be possible bugs in the code, be patient.
252281
Also, I am not a native English speaker :sweat_smile:.
253282

254283
Thanks to [hazarek](https://github.com/hazarek) for the app icon and the CSS style.

TODO.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
- [ ] Check inline images on network / mounted disk
77
- [ ] Investigate font family override for fanced blocks
88
- [ ] Localization support
9-
- [X] Emoji extension: better code that parse the single placeholder and generate nodes inside the AST (this would avoid the CMARK_OPT_UNSAFE option for emojis as images)
9+
- [ ] embed inline image for `<img>` raw tag without using javascript/callbacks.
10+
- [x] Emoji extension: better code that parse the single placeholder and generate nodes inside the AST (this would avoid the CMARK_OPT_UNSAFE option for emojis as images)
1011
- [x] Investigate CMARK_OPT_UNSAFE for inline images
1112
- [x] Application screenshot in the docs
1213
- [x] Extension to generate anchor link for heads

highlight-wrapper/wrapper_highlight.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ const char *highlight_get_current_theme(void) {
164164

165165
int highlight_set_current_theme(const char *theme) {
166166
string themePath;
167+
if (strlen(theme) == 0) {
168+
return EXIT_FAILURE;
169+
}
167170
if (Platform::fileExists(theme)) {
168171
themePath = theme;
169172
} else {
@@ -186,7 +189,7 @@ int highlight_set_current_theme(const char *theme) {
186189
return EXIT_FAILURE;
187190
} else {
188191
os_log_debug(sLog, "Using theme `%{public}s`.", themePath.c_str());
189-
printf("using theme %s\n", themePath.c_str());
192+
// printf("using theme %s\n", themePath.c_str());
190193
}
191194
return EXIT_SUCCESS;
192195
}

qlmarkdown_cli/main.swift

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//
2+
// main.swift
3+
// qlmarkdown_cli
4+
//
5+
// Created by Sbarex on 18/10/21.
6+
//
7+
8+
import Cocoa
9+
import OSLog
10+
11+
let cliUrl = URL(fileURLWithPath: CommandLine.arguments[0])
12+
13+
var standardError = FileHandle.standardError
14+
15+
extension FileHandle : TextOutputStream {
16+
public func write(_ string: String) {
17+
guard let data = string.data(using: .utf8) else { return }
18+
self.write(data)
19+
}
20+
}
21+
22+
func usage(exitCode: Int = -1) {
23+
let name = cliUrl.lastPathComponent
24+
print("\(name)")
25+
print("Usage: \(name) [--app <path>] [-o <file|dir>] [-v] <file> [..]")
26+
print("\nArguments:")
27+
print(" -h\tShow this help and exit.")
28+
print(" -o\t<file|dir> Destination output. If you pass a directory, a new file is created with the name of the processed source with html extension. \n \tThe destination file is always overwritten. If this argument is not provided, the output will be printed to the stdout.")
29+
print(" -v\tVerbose mode. Valid only with the -o option.")
30+
// print(" --app\t<path> Set the path of \"QLMarkdown.app\" otherwise assume that \(name) is called from the Contents/Resources of the app bundle.")
31+
print("\nTo handle multiple files at time you need to pass the -o arguments with a destination folder.")
32+
print("\nPlease use the main app to customize the rendering settings.")
33+
34+
if exitCode >= 0 {
35+
exit(Int32(exitCode))
36+
}
37+
}
38+
39+
var appUrl: URL!
40+
var files: [URL] = []
41+
var dest: URL?
42+
var verbose = false
43+
44+
var i = 1
45+
while i < Int(CommandLine.argc) {
46+
var arg = CommandLine.arguments[i]
47+
if arg.hasPrefix("-") {
48+
if arg.hasPrefix("--") {
49+
// process a --arg
50+
switch arg {
51+
case "--help":
52+
usage(exitCode: 0)
53+
case "--app":
54+
let u = CommandLine.arguments[i+1]
55+
i += 1
56+
appUrl = URL(fileURLWithPath: u)
57+
default:
58+
print("\(cliUrl.lastPathComponent): illegal option -\(arg)\n", to: &standardError)
59+
usage(exitCode: 1)
60+
}
61+
} else {
62+
// process a -arg
63+
arg.removeFirst()
64+
for (j, arg1) in arg.enumerated() {
65+
switch arg1 {
66+
case "h":
67+
usage(exitCode: 0)
68+
case "o":
69+
if j + 1 == arg.count {
70+
dest = URL(fileURLWithPath: CommandLine.arguments[i+1])
71+
i += 1
72+
} else {
73+
print("\(cliUrl.lastPathComponent): option -\(arg1) require a destination path\n", to: &standardError)
74+
usage(exitCode: 1)
75+
}
76+
case "v":
77+
verbose = true
78+
default:
79+
print("\(cliUrl.lastPathComponent): illegal option -\(arg1)\n", to: &standardError)
80+
usage(exitCode: 1)
81+
}
82+
}
83+
}
84+
} else {
85+
files.append(URL(fileURLWithPath: arg))
86+
}
87+
/*
88+
switch arg {
89+
case "--help", "-h":
90+
usage()
91+
exit(0)
92+
case "--app":
93+
let u = CommandLine.arguments[i+1]
94+
i += 1
95+
appUrl = URL(fileURLWithPath: u)
96+
case "-o":
97+
dest = URL(fileURLWithPath: CommandLine.arguments[i+1])
98+
i += 1
99+
case "-v":
100+
verbose = true
101+
default:
102+
if arg.hasPrefix("-") {
103+
print("\(cliUrl.lastPathComponent): illegal option \(arg)", to: &standardError)
104+
usage()
105+
exit(1)
106+
}
107+
files.append(URL(fileURLWithPath: arg))
108+
}
109+
*/
110+
i += 1
111+
}
112+
113+
verbose = verbose && dest != nil
114+
115+
if appUrl == nil {
116+
appUrl = cliUrl.deletingLastPathComponent().deletingLastPathComponent()
117+
}
118+
119+
let appBundleUrl = appUrl.appendingPathComponent("Contents/Resources")
120+
121+
122+
if files.count > 1 {
123+
var isDir: ObjCBool = false
124+
if let dest = dest {
125+
FileManager.default.fileExists(atPath: dest.path, isDirectory: &isDir)
126+
}
127+
if !isDir.boolValue {
128+
print("Error: to process multiple files you must use the -o arguments with a folder path!", to: &standardError)
129+
exit(1)
130+
}
131+
}
132+
133+
var n = 0
134+
defer {
135+
if verbose {
136+
print(n != 1 ? "Processed \(n) files." : "Processed 1 file.")
137+
}
138+
}
139+
140+
Settings.appBundleUrl = appBundleUrl
141+
let settings = Settings.shared
142+
143+
let type = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light"
144+
145+
if verbose {
146+
print("\(cliUrl.lastPathComponent):")
147+
}
148+
149+
for url in files {
150+
let markdown_url: URL
151+
if let typeIdentifier = (try? url.resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier, typeIdentifier == "org.textbundle.package" {
152+
if FileManager.default.fileExists(atPath: url.appendingPathComponent("text.md").path) {
153+
markdown_url = url.appendingPathComponent("text.md")
154+
} else {
155+
markdown_url = url.appendingPathComponent("text.markdown")
156+
}
157+
} else {
158+
markdown_url = url
159+
}
160+
161+
162+
do {
163+
if !FileManager.default.isReadableFile(atPath: markdown_url.path) {
164+
print("Unable to read the file \(markdown_url.path)", to: &standardError)
165+
exit(127)
166+
}
167+
if verbose {
168+
print("- processing \(markdown_url.path) ...")
169+
}
170+
let text = try settings.render(file: markdown_url, forAppearance: type == "Light" ? .light : .dark, baseDir: markdown_url.deletingLastPathComponent().path, log: nil)
171+
172+
let html = settings.getCompleteHTML(title: url.lastPathComponent, body: text)
173+
174+
var output: URL?
175+
if let dest = dest {
176+
var isDir: ObjCBool = false
177+
FileManager.default.fileExists(atPath: dest.path, isDirectory: &isDir)
178+
if isDir.boolValue {
179+
output = dest.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension("html")
180+
} else {
181+
output = dest
182+
}
183+
/*
184+
if !(output?.pathExtension.lowercased().hasPrefix("htm") ?? false) {
185+
output?.appendPathExtension("html")
186+
}
187+
*/
188+
}
189+
190+
if let output = output {
191+
try html.write(to: output, atomically: true, encoding: .utf8)
192+
if verbose {
193+
print(" ... stored in \(output.path)")
194+
}
195+
n += 1
196+
} else {
197+
FileHandle.standardOutput.write(html)
198+
n += 1
199+
}
200+
} catch {
201+
print("Error processing \(url.path): \(error.localizedDescription)", to: &standardError)
202+
exit(1)
203+
}
204+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<false/>
7+
<key>com.apple.security.application-groups</key>
8+
<array>
9+
<string>org.sbarex.qlmarkdown</string>
10+
</array>
11+
<key>com.apple.security.temporary-exception.shared-preference.read-only</key>
12+
<array>
13+
<string>org.sbarex.qlmarkdown</string>
14+
</array>
15+
<key>com.apple.security.cs.disable-library-validation</key>
16+
<true/>
17+
</dict>
18+
</plist>

0 commit comments

Comments
 (0)