commit f9d3a5e397166e49ce6869d3ebd37b341d8b143d Author: Shadowfacts Date: Mon May 30 11:29:44 2022 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ce42ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f231ed7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "splash-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5366b21 --- /dev/null +++ b/build.rs @@ -0,0 +1,93 @@ +fn main() { + link_swift(); + link_swift_package("highlight-swift", "./highlight-swift/"); + + let target = get_swift_target_info(); + // on linux we need to tell it to link against the swift stdlib, + // because the --static-swift-stdlib on swift build doesn't work for static libraries + // on macos, it all magically works + if target.target.unversioned_triple.contains("linux") { + // include the swift runtime library path in the rpath + target.paths.runtime_library_paths.iter().for_each(|path| { + println!("cargo:rustc-link-arg-Wl,-rpath={}", path); + }); + // need to link against libswiftCore and libFoundation + println!("cargo:rustc-link-lib=dylib=swiftCore"); + println!("cargo:rustc-link-lib=dylib=Foundation"); + } +} + +// Copied from https://github.com/Brendonovich/swift-rs +// With get_swift_target_info changed to print the current target +// rather than always trying for the macosx target. + +use std::{env, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwiftTargetInfo { + pub unversioned_triple: String, + #[serde(rename = "librariesRequireRPath")] + pub libraries_require_rpath: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwiftPaths { + pub runtime_library_paths: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct SwiftTarget { + pub target: SwiftTargetInfo, + pub paths: SwiftPaths, +} + +pub fn get_swift_target_info() -> SwiftTarget { + let swift_target_info_str = Command::new("swift") + .args(&["-print-target-info"]) + .output() + .unwrap() + .stdout; + + serde_json::from_slice(&swift_target_info_str).unwrap() +} + +pub fn link_swift() { + let swift_target_info = get_swift_target_info(); + if swift_target_info.target.libraries_require_rpath { + panic!("Libraries require RPath! Change minimum MacOS value to fix.") + } + + swift_target_info + .paths + .runtime_library_paths + .iter() + .for_each(|path| { + println!("cargo:rustc-link-search=native={}", path); + }); +} + +pub fn link_swift_package(package_name: &str, package_root: &str) { + let profile = env::var("PROFILE").unwrap(); + + if !Command::new("swift") + .args(&["build", "-c", &profile]) + .current_dir(package_root) + .status() + .unwrap() + .success() + { + panic!("Failed to compile swift package {}", package_name); + } + + let swift_target_info = get_swift_target_info(); + + println!( + "cargo:rustc-link-search=native={}.build/{}/{}", + package_root, swift_target_info.target.unversioned_triple, profile + ); + println!("cargo:rustc-link-lib=static={}", package_name); +} diff --git a/highlight-swift/.gitignore b/highlight-swift/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/highlight-swift/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/highlight-swift/Package.resolved b/highlight-swift/Package.resolved new file mode 100644 index 0000000..55695dd --- /dev/null +++ b/highlight-swift/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "splash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/Splash.git", + "state" : { + "revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8", + "version" : "0.16.0" + } + } + ], + "version" : 2 +} diff --git a/highlight-swift/Package.swift b/highlight-swift/Package.swift new file mode 100644 index 0000000..37aa78a --- /dev/null +++ b/highlight-swift/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "highlight-swift", + platforms: [ + .macOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "highlight-swift", + type: .static, + targets: ["highlight-swift"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/JohnSundell/Splash.git", .upToNextMinor(from: "0.16.0")), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "highlight-swift", + dependencies: ["Splash"]), + ] +) diff --git a/highlight-swift/README.md b/highlight-swift/README.md new file mode 100644 index 0000000..6c93af1 --- /dev/null +++ b/highlight-swift/README.md @@ -0,0 +1,3 @@ +# highlight-swift + +A description of this package. diff --git a/highlight-swift/Sources/highlight-swift/highlight_swift.swift b/highlight-swift/Sources/highlight-swift/highlight_swift.swift new file mode 100644 index 0000000..2ed3c7e --- /dev/null +++ b/highlight-swift/Sources/highlight-swift/highlight_swift.swift @@ -0,0 +1,120 @@ +import Foundation +import Splash + +@_cdecl("highlight_swift") +public func highlight(codePtr: UnsafeRawPointer, codeLen: UInt64, outPtr: UnsafeMutableRawPointer, maxLen: UInt64) -> UInt64 { + // don't free, the underlying data is owned by rust + let code = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: codePtr), length: Int(codeLen), encoding: .utf8, freeWhenDone: false)! + + let highligher = SyntaxHighlighter(format: MyOutputFormat()) + var html = highligher.highlight(code) + precondition(html.utf8.count <= maxLen) + return html.withUTF8 { buf in + buf.copyBytes(to: UnsafeMutableRawBufferPointer(start: outPtr, count: Int(maxLen))) + return UInt64(buf.count) + } +} + +struct MyOutputFormat: OutputFormat { + func makeBuilder() -> Builder { + return Builder() + } + + struct Builder: OutputBuilder { + typealias Output = String + + private var html = "" + private var pendingToken: (string: String, type: TokenType)? + private var pendingWhitespace: String? + + mutating func addToken(_ token: String, ofType type: TokenType) { + if var pendingToken = pendingToken { + guard pendingToken.type != type else { + pendingWhitespace.map { pendingToken.string += $0 } + pendingWhitespace = nil + pendingToken.string += token + self.pendingToken = pendingToken + return + } + } + + appendPending() + pendingToken = (token, type) + } + + mutating func addPlainText(_ text: String) { + appendPending() + html.append(contentsOf: text.escapingHTMLEntities()) + } + + mutating func addWhitespace(_ whitespace: String) { + if pendingToken != nil { + pendingWhitespace = (pendingWhitespace ?? "") + whitespace + } else { + html.append(whitespace) + } + } + + mutating func build() -> String { + appendPending() + return html + } + + private mutating func appendPending() { + if let pendingToken = pendingToken { + let cls: String + switch pendingToken.type { + case .keyword: + cls = "hl-kw" + case .string: + cls = "hl-str" + case .type: + cls = "hl-type" + case .call: + cls = "hl-fn" + case .number: + cls = "hl-num" + case .comment: + cls = "hl-cmt" + case .property: + cls = "hl-var" + case .dotAccess: + cls = "hl-prop" + case .preprocessing: + cls = "" + case .custom(_): + cls = "" + } + html.append(""" + \(pendingToken.string.escapingHTMLEntities()) + """) + + self.pendingToken = nil + } + + if let pendingWhitespace = pendingWhitespace { + html.append(pendingWhitespace) + self.pendingWhitespace = nil + } + } + } +} + +// copied from Splash, where it's internal +extension StringProtocol { + func escapingHTMLEntities() -> String { + return String(flatMap { character -> String in + switch character { + case "&": + return "&" + case "<": + return "<" + case ">": + return ">" + default: + return String(character) + } + }) + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7454119 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +pub fn highlight(code: &str) -> String { + unsafe { + let mut buf = Vec::new(); + buf.reserve( + 2usize + .pow(1 + (code.len() as f32).log2().ceil() as u32) + .max(1024), + ); + let used = highlight_swift( + code.as_ptr(), + code.len() as u64, + buf.as_mut_ptr(), + buf.capacity() as u64, + ); + buf.set_len(used as usize); + String::from_utf8_unchecked(buf) + } +} + +extern "C" { + fn highlight_swift(code_ptr: *const u8, code_len: u64, out_ptr: *mut u8, max_len: u64) -> u64; +} + +#[cfg(test)] +mod tests { + #[test] + fn test_highlight() { + let result = super::highlight("1+1"); + assert_eq!( + result, + r#"1+1"# + ); + } +}