Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pique/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct ContentView: View {
("Shell", "terminal", Color.mint),
("Python", "chevron.left.forwardslash.chevron.right", Color.cyan),
("HCL", "doc.text", Color.indigo),
("Log", "doc.text.below.ecg", Color.gray),
]

var body: some View {
Expand Down
19 changes: 19 additions & 0 deletions Pique/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Log File</string>
<key>UTTypeIdentifier</key>
<string>io.macadmins.pique.log</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions Pique/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct SettingsView: View {
("JavaScript", "chevron.left.forwardslash.chevron.right", .yellow),
("Markdown", "doc.richtext", .gray),
("HCL", "doc.text", .indigo),
("Log", "doc.text.below.ecg", .gray),
]

@State private var overrides: [String: AppearanceOverride] = [:]
Expand Down
25 changes: 25 additions & 0 deletions PiquePreview/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>io.macadmins.pique.log</string>
<key>UTTypeDescription</key>
<string>Log File</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.plain-text</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
</dict>
</dict>
</array>
<key>NSExtension</key>
<dict>
Expand Down Expand Up @@ -146,6 +165,9 @@
<string>io.macadmins.pique.hcl</string>
<string>io.macadmins.pique.recipe</string>
<string>com.apple.terminal.shell-script</string>
<string>io.macadmins.pique.log</string>
<string>com.apple.log</string>
<string>public.log</string>
</array>
<key>QLFileExtensions</key>
<array>
Expand Down Expand Up @@ -184,6 +206,9 @@
<string>tfvars</string>
<string>hcl</string>
<string>recipe</string>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
<key>QLPreviewWidth</key>
<real>1024</real>
Expand Down
1 change: 1 addition & 0 deletions PiquePreview/PreviewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class PreviewProvider: NSViewController, QLPreviewingController {
case "js", "jsx", "ts", "tsx", "mjs", "cjs": return "JavaScript"
case "md", "markdown", "adoc": return "Markdown"
case "tf", "tfvars", "hcl": return "HCL"
case "log", "out", "err": return "Log"
default: return ext
}
}
Expand Down
7 changes: 7 additions & 0 deletions PiqueTests/FileFormatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,20 @@ final class FileFormatTests: XCTestCase {
}
}

func testLog() {
for ext in ["log", "out", "err"] {
XCTAssertEqual(FileFormat(pathExtension: ext), .log, "Expected .log for .\(ext)")
}
}

// MARK: - Case insensitivity

func testCaseInsensitive() {
XCTAssertEqual(FileFormat(pathExtension: "JSON"), .json)
XCTAssertEqual(FileFormat(pathExtension: "YAML"), .yaml)
XCTAssertEqual(FileFormat(pathExtension: "Toml"), .toml)
XCTAssertEqual(FileFormat(pathExtension: "SH"), .shell)
XCTAssertEqual(FileFormat(pathExtension: "LOG"), .log)
}

// MARK: - Unknown / empty
Expand Down
113 changes: 113 additions & 0 deletions PiqueTests/HighlightIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,117 @@ final class HighlightIntegrationTests: XCTestCase {
let html = body(SyntaxHighlighter.highlight(hcl, format: .hcl))
XCTAssertTrue(html.contains(#"class="string""#))
}

// MARK: - Log files: heuristic highlighting

func testLogSeverityLevels() {
let log = "ERROR something failed\nWARN low disk\nINFO started\nDEBUG tick"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logError">ERROR</span>"#))
XCTAssertTrue(html.contains(#"<span class="logWarn">WARN</span>"#))
XCTAssertTrue(html.contains(#"<span class="logInfo">INFO</span>"#))
XCTAssertTrue(html.contains(#"<span class="logDebug">DEBUG</span>"#))
}

func testLogISOTimestamp() {
let log = "2026-03-28T14:05:33Z INFO ready"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logTimestamp">2026-03-28T14:05:33Z</span>"#))
}

func testLogSyslogTimestamp() {
let log = "Mar 28 14:05:33 myhost syslogd: restart"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logTimestamp">Mar 28 14:05:33</span>"#))
}

func testLogIPv4Address() {
let log = "connection from 192.168.1.100 accepted"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="number">192.168.1.100</span>"#))
}

func testLogHTTPMethodAndStatusCode() {
let log = #"GET /api/health 200"#
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="keyword">GET</span>"#))
XCTAssertTrue(html.contains(#"<span class="variable">/api/health</span>"#))
}

func testLogQuotedString() {
let log = #"message: "disk space low""#
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="string">&quot;disk space low&quot;</span>"#))
}

func testLogCriticalSeverity() {
let log = "FATAL out of memory\nCRITICAL disk failure"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logError">FATAL</span>"#))
XCTAssertTrue(html.contains(#"<span class="logError">CRITICAL</span>"#))
}

func testLogApostropheNotTreatedAsString() {
let log = "INFO: Successfully validated the received JWT's signature..."
let html = body(SyntaxHighlighter.highlight(log, format: .log))
// The apostrophe in JWT's must NOT cause text to be swallowed into a string span
XCTAssertFalse(html.contains(#"<span class="string">'s signature...'"#),
"Apostrophe in JWT's should not start a quoted string")
Comment thread
neilmartin83 marked this conversation as resolved.
Outdated
XCTAssertTrue(html.contains("JWT"))
XCTAssertTrue(html.contains("signature"))
}

func testLogSeverityWithColon() {
let log = "2025-09-16 06:20:56 +0100 – INFO: Notifying that Docker has been installed"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logInfo">INFO:</span>"#))
}

func testLogMultiLinePreservesAllLines() {
let log = "INFO line one\nWARN line two\nERROR line three"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="logInfo">INFO</span>"#))
XCTAssertTrue(html.contains(#"<span class="logWarn">WARN</span>"#))
XCTAssertTrue(html.contains(#"<span class="logError">ERROR</span>"#))
XCTAssertTrue(html.contains("line one"))
XCTAssertTrue(html.contains("line two"))
XCTAssertTrue(html.contains("line three"))
}

func testLogHTTPStatusCodeColoring() {
let log = "status 200\nstatus 404\nstatus 500"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
// 2xx → .logInfo (blue)
XCTAssertTrue(html.contains(#"<span class="logInfo">200</span>"#))
// 4xx → .logWarn (orange)
XCTAssertTrue(html.contains(#"<span class="logWarn">404</span>"#))
// 5xx → .logError (bold red)
XCTAssertTrue(html.contains(#"<span class="logError">500</span>"#))
}

func testLogFilePath() {
let log = "loading /usr/local/etc/config.yaml"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="variable">/usr/local/etc/config.yaml</span>"#))
}

// MARK: - Truncation

func testTruncationAppliedForLargeInput() {
// Generate a string larger than 512KB
let line = "2026-03-28T09:00:00Z INFO This is a log line for testing truncation purposes.\n"
let count = (512_001 / line.count) + 1
let bigLog = String(repeating: line, count: count)
XCTAssertGreaterThan(bigLog.count, 512_000, "Test input should exceed the limit")

let html = SyntaxHighlighter.highlight(bigLog, format: .log)
XCTAssertTrue(html.contains("Preview truncated"), "Large input should show truncation notice")
XCTAssertTrue(html.contains("lines shown"), "Truncation notice should mention line counts")
}
Comment thread
neilmartin83 marked this conversation as resolved.
Outdated

Comment thread
neilmartin83 marked this conversation as resolved.
Outdated
func testNoTruncationForSmallInput() {
let log = "INFO all good"
let html = SyntaxHighlighter.highlight(log, format: .log)
XCTAssertFalse(html.contains("Preview truncated"), "Small input should not be truncated")
}
}
Loading
Loading