feat(ios): intelligent Switch Markdown View & Ephemeral Action View (#9823)

Co-authored-by: EYHN <cneyhn@gmail.com>
This commit is contained in:
Lakr
2025-03-24 17:47:42 +08:00
committed by GitHub
parent 4d3eee3bad
commit 447b23f25f
192 changed files with 43960 additions and 980 deletions

1
.gitignore vendored
View File

@@ -84,3 +84,4 @@ packages/frontend/core/public/static/templates
# script
af
af.cmd
*.resolved

View File

@@ -33,6 +33,7 @@ packages/common/native/fixtures/**
packages/common/graphql/src/graphql/index.ts
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
packages/frontend/apps/android/App/app/build/**
packages/frontend/apps/android/App/**
packages/frontend/apps/ios/App/**
tests/blocksuite/snapshots
blocksuite/docs/api/**

View File

@@ -34,7 +34,8 @@
"packages/common/graphql/src/graphql/index.ts",
"packages/frontend/native/index.d.ts",
"packages/frontend/native/index.js",
"packages/frontend/apps/android/App/app/build/**",
"packages/frontend/apps/android/App/**",
"packages/frontend/apps/ios/App/**",
"tests/blocksuite/snapshots",
"blocksuite/docs/api/**"
],

View File

@@ -14,6 +14,8 @@
50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; };
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; };
50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; };
50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; };
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */; };
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
@@ -44,6 +46,9 @@
50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; };
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
50FEBD7B2D3F7719002847B5 /* ChidoriMenu */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ChidoriMenu; sourceTree = "<group>"; };
50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationBridgedWindowScript.swift; sourceTree = "<group>"; };
50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AffineViewController+AIButton.swift"; sourceTree = "<group>"; };
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = "<group>"; };
9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPlugin.swift; sourceTree = "<group>"; };
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
@@ -130,6 +135,7 @@
isa = PBXGroup;
children = (
5039CC962D1D42C700874F32 /* AffineGraphQL */,
50FEBD7B2D3F7719002847B5 /* ChidoriMenu */,
50802D5E2D112F7D00694021 /* Intelligents */,
);
path = Packages;
@@ -186,6 +192,8 @@
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
507513692D1924C600AD60C0 /* RootViewController.swift */,
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */,
50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */,
50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */,
9D90BE1D2CCB9876006677DB /* Assets.xcassets */,
9D90BE1E2CCB9876006677DB /* capacitor.config.json */,
9D90BE1F2CCB9876006677DB /* config.xml */,
@@ -358,12 +366,14 @@
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
9DAE9BD92D8D1AB0000C1D5A /* AppConfigManager.swift in Sources */,
50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */,
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */,
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */,
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */,
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */,

View File

@@ -3,37 +3,19 @@
{
"identity" : "apollo-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apollographql/apollo-ios.git",
"location" : "https://github.com/apollographql/apollo-ios",
"state" : {
"revision" : "9aa748d6f0526a744d49d59a2383dc7fdf9d645b",
"version" : "1.18.0"
}
},
{
"identity" : "msdisplaylink",
"identity" : "splash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MSDisplayLink",
"location" : "https://github.com/JohnSundell/Splash",
"state" : {
"revision" : "c2fcd28cb99300d83acc30860ce252ef97c20b61",
"version" : "1.1.1"
}
},
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "springinterpolation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/SpringInterpolation",
"state" : {
"revision" : "f9d1ee3d2466bdb00fd0ade7f256ed20229c8413",
"version" : "1.3.0"
"revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8",
"version" : "0.16.0"
}
},
{
@@ -46,12 +28,12 @@
}
},
{
"identity" : "swift-cmark",
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version" : "0.5.0"
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
@@ -62,15 +44,6 @@
"revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814",
"version" : "3.3.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 2

View File

@@ -10,30 +10,12 @@
}
},
{
"identity" : "msdisplaylink",
"identity" : "splash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MSDisplayLink",
"location" : "https://github.com/JohnSundell/Splash",
"state" : {
"revision" : "c2fcd28cb99300d83acc30860ce252ef97c20b61",
"version" : "1.1.1"
}
},
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "springinterpolation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/SpringInterpolation",
"state" : {
"revision" : "f9d1ee3d2466bdb00fd0ade7f256ed20229c8413",
"version" : "1.3.0"
"revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8",
"version" : "0.16.0"
}
},
{
@@ -46,12 +28,12 @@
}
},
{
"identity" : "swift-cmark",
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version" : "0.5.0"
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
@@ -62,15 +44,6 @@
"revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814",
"version" : "3.3.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 2

View File

@@ -0,0 +1,127 @@
//
// File.swift
// App
//
// Created by on 2025/1/8.
//
import UIKit
import Intelligents
import ChidoriMenu
extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate {
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
guard let webView else {
assertionFailure() // ? wdym ?
return
}
button.beginProgress()
let group = DispatchGroup()
group.enter()
webView.evaluateScript(.getCurrentServerBaseUrl) { result in
self.baseUrl = result as? String
print("[*] setting baseUrl: \(self.baseUrl ?? "")")
group.leave()
}
group.enter()
webView.evaluateScript(.getCurrentDocId) { result in
self.documentID = result as? String
print("[*] setting documentID: \(self.documentID ?? "")")
group.leave()
}
group.enter()
webView.evaluateScript(.getCurrentWorkspaceId) { result in
self.workspaceID = result as? String
print("[*] setting workspaceID: \(self.workspaceID ?? "")")
group.leave()
}
group.enter()
webView.evaluateScript(.getCurrentDocContentInMarkdown) { input in
self.documentContent = input as? String
print("[*] setting documentContent: \(self.documentContent?.count ?? 0) chars")
group.leave()
}
DispatchQueue.global().asyncAfter(deadline: .now()) {
group.wait()
DispatchQueue.main.async {
button.stopProgress()
webView.resignFirstResponder()
self.openIntelligentsSheet()
}
}
}
@discardableResult
func openIntelligentsSheet() -> IntelligentsFocusApertureView? {
dismissIntelligentsButton()
view.resignFirstResponder()
// stop scroll on webview
if let contentOffset = webView?.scrollView.contentOffset {
webView?.scrollView.contentOffset = contentOffset
}
let focus = IntelligentsFocusApertureView()
focus.prepareAnimationWith(
capturingTargetContentView: webView ?? .init(),
coveringRootViewController: self
)
focus.delegate = self
focus.executeAnimationKickIn()
dismissIntelligentsButton()
return focus
}
func openSimpleChat() {
let targetController = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: targetController)
}
func focusApertureRequestAction(
from view: IntelligentsFocusApertureView,
actionType: IntelligentsFocusApertureViewActionType
) {
switch actionType {
case .translateTo:
var actions: [UIAction] = []
for lang in IntelligentsEphemeralActionController.EphemeralAction.Language.allCases {
actions.append(.init(title: lang.rawValue) { [weak self] _ in
guard let self else { return }
let controller = IntelligentsEphemeralActionController(
action: .translate(to: lang)
)
controller.workspaceID = workspaceID ?? ""
controller.documentID = documentID ?? ""
controller.documentContent = documentContent ?? ""
controller.configure(previewImage: view.capturedImage ?? .init())
presentIntoCurrentContext(withTargetController: controller)
})
}
view.present(menu: .init(children: actions)) { menu in
menu.overrideUserInterfaceStyle = .dark
}
case .summary:
let controller = IntelligentsEphemeralActionController(
action: .summarize
)
controller.configure(previewImage: view.capturedImage ?? .init())
controller.workspaceID = workspaceID ?? ""
controller.documentID = documentID ?? ""
controller.documentContent = documentContent ?? ""
presentIntoCurrentContext(withTargetController: controller)
case .chatWithAI:
let controller = IntelligentsChatController()
controller.metadata[.documentID] = documentID
controller.metadata[.workspaceID] = workspaceID
controller.metadata[.content] = documentContent
presentIntoCurrentContext(withTargetController: controller)
case .dismiss:
presentIntelligentsButton()
}
}
}

View File

@@ -3,6 +3,13 @@ import Intelligents
import UIKit
class AFFiNEViewController: CAPBridgeViewController {
var baseUrl: String? {
didSet { Intelligents.setUpstreamEndpoint(baseUrl ?? "") }
}
var documentID: String?
var workspaceID: String?
var documentContent: String?
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
@@ -39,84 +46,6 @@ class AFFiNEViewController: CAPBridgeViewController {
super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
}
extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate {
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
guard let webView else {
assertionFailure() // ? wdym ?
return
}
button.beginProgress()
let upstreamReaderScript = "window.getCurrentServerBaseUrl();"
webView.evaluateJavaScript(upstreamReaderScript) { result, _ in
if let baseUrl = result as? String {
Intelligents.setUpstreamEndpoint(baseUrl)
}
let script = "return await window.getCurrentDocContentInMarkdown();"
webView.callAsyncJavaScript(
script,
arguments: [:],
in: nil,
in: .page
) { result in
button.stopProgress()
webView.resignFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if case let .success(content) = result,
let res = content as? String
{
print("[*] \(self) received document with \(res.count) characters")
self.openIntelligentsSheet(withContext: res)
} else {
self.openSimpleChat()
}
}
}
}
}
func openIntelligentsSheet(withContext context: String) {
guard let view = webView?.subviews.first else {
assertionFailure()
return
}
assert(view is UIScrollView)
_ = context
let focus = IntelligentsFocusApertureView()
focus.prepareAnimationWith(
capturingTargetContentView: view,
coveringRootViewController: self
)
focus.delegate = self
focus.executeAnimationKickIn()
dismissIntelligentsButton()
}
func openSimpleChat() {
let targetController = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: targetController)
}
func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType) {
switch actionType {
case .translateTo:
fatalError("not implemented")
case .summary:
fatalError("not implemented")
case .chatWithAI:
let controller = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: controller)
case .dismiss:
presentIntelligentsButton()
}
}
}

View File

@@ -0,0 +1,47 @@
//
// ApplicationBridgedWindowScript.swift
// App
//
// Created by on 2025/1/8.
//
import Foundation
import WebKit
enum ApplicationBridgedWindowScript: String {
case getCurrentDocContentInMarkdown = "return await window.getCurrentDocContentInMarkdown();"
case getCurrentServerBaseUrl = "window.getCurrentServerBaseUrl()"
case getCurrentWorkspaceId = "window.getCurrentWorkspaceId();"
case getCurrentDocId = "window.getCurrentDocId();"
var requiresAsyncContext: Bool {
switch self {
case .getCurrentDocContentInMarkdown: return true
default: return false
}
}
}
extension WKWebView {
func evaluateScript(_ script: ApplicationBridgedWindowScript, callback: @escaping (Any?) -> ()) {
if script.requiresAsyncContext {
callAsyncJavaScript(
script.rawValue,
arguments: [:],
in: nil,
in: .page
) { result in
switch result {
case .success(let input):
callback(input)
case .failure:
callback(nil)
}
}
} else {
evaluateJavaScript(script.rawValue) { output, _ in callback(output) }
}
}
}

View File

@@ -497,7 +497,7 @@ fileprivate struct FfiConverterString: FfiConverter {
public protocol DocStoragePoolProtocol: AnyObject {
public protocol DocStoragePoolProtocol: AnyObject, Sendable {
func clearClocks(universalId: String) async throws
@@ -573,6 +573,9 @@ open class DocStoragePool: DocStoragePoolProtocol, @unchecked Sendable {
// TODO: We'd like this to be `private` but for Swifty reasons,
// we can't implement `FfiConverter` without making this `required` and we can't
// make it `required` without making it `public`.
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
self.pointer = pointer
}
@@ -621,7 +624,7 @@ open func clearClocks(universalId: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -641,7 +644,7 @@ open func connect(universalId: String, path: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -658,7 +661,7 @@ open func deleteBlob(universalId: String, key: String, permanently: Bool)async t
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -675,7 +678,7 @@ open func deleteDoc(universalId: String, docId: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -692,7 +695,7 @@ open func disconnect(universalId: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -709,7 +712,7 @@ open func getBlob(universalId: String, key: String)async throws -> Blob? {
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeBlob.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -726,7 +729,7 @@ open func getBlobUploadedAt(universalId: String, peer: String, blobId: String)as
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionInt64.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -743,7 +746,7 @@ open func getDocClock(universalId: String, docId: String)async throws -> DocClo
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -760,7 +763,7 @@ open func getDocClocks(universalId: String, after: Int64?)async throws -> [DocC
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -777,7 +780,7 @@ open func getDocSnapshot(universalId: String, docId: String)async throws -> Doc
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeDocRecord.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -794,7 +797,7 @@ open func getDocUpdates(universalId: String, docId: String)async throws -> [Doc
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeDocUpdate.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -811,7 +814,7 @@ open func getPeerPulledRemoteClock(universalId: String, peer: String, docId: Str
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -828,7 +831,7 @@ open func getPeerPulledRemoteClocks(universalId: String, peer: String)async thro
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -845,7 +848,7 @@ open func getPeerPushedClock(universalId: String, peer: String, docId: String)as
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -862,7 +865,7 @@ open func getPeerPushedClocks(universalId: String, peer: String)async throws ->
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -879,7 +882,7 @@ open func getPeerRemoteClock(universalId: String, peer: String, docId: String)as
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterOptionTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -896,7 +899,7 @@ open func getPeerRemoteClocks(universalId: String, peer: String)async throws ->
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeDocClock.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -913,7 +916,7 @@ open func listBlobs(universalId: String)async throws -> [ListedBlob] {
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer,
liftFunc: FfiConverterSequenceTypeListedBlob.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -930,7 +933,7 @@ open func markUpdatesMerged(universalId: String, docId: String, updates: [Int64]
completeFunc: ffi_affine_mobile_native_rust_future_complete_u32,
freeFunc: ffi_affine_mobile_native_rust_future_free_u32,
liftFunc: FfiConverterUInt32.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -947,7 +950,7 @@ open func pushUpdate(universalId: String, docId: String, update: String)async th
completeFunc: ffi_affine_mobile_native_rust_future_complete_i64,
freeFunc: ffi_affine_mobile_native_rust_future_free_i64,
liftFunc: FfiConverterInt64.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -964,7 +967,7 @@ open func releaseBlobs(universalId: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -981,7 +984,7 @@ open func setBlob(universalId: String, blob: SetBlob)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -998,7 +1001,7 @@ open func setBlobUploadedAt(universalId: String, peer: String, blobId: String, u
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1015,7 +1018,7 @@ open func setDocSnapshot(universalId: String, snapshot: DocRecord)async throws
completeFunc: ffi_affine_mobile_native_rust_future_complete_i8,
freeFunc: ffi_affine_mobile_native_rust_future_free_i8,
liftFunc: FfiConverterBool.lift,
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1032,7 +1035,7 @@ open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: Str
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1049,7 +1052,7 @@ open func setPeerPushedClock(universalId: String, peer: String, docId: String, c
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1066,7 +1069,7 @@ open func setPeerRemoteClock(universalId: String, peer: String, docId: String, c
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1083,7 +1086,7 @@ open func setSpaceId(universalId: String, spaceId: String)async throws {
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
errorHandler: FfiConverterTypeUniffiError_lift
)
}
@@ -1627,7 +1630,7 @@ public func FfiConverterTypeSetBlob_lower(_ value: SetBlob) -> RustBuffer {
}
public enum UniffiError {
public enum UniffiError: Swift.Error {

View File

@@ -14,7 +14,7 @@ let package = Package(
.library(name: "AffineGraphQL", targets: ["AffineGraphQL"]),
],
dependencies: [
.package(url: "https://github.com/apollographql/apollo-ios", exact: "1.18.0"),
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0"),
],
targets: [
.target(

View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,410 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
503E58F02D3C0D6B007BD8C6 /* ChidoriMenu in Frameworks */ = {isa = PBXBuildFile; productRef = 503E58EF2D3C0D6B007BD8C6 /* ChidoriMenu */; };
503E58F32D3C10BF007BD8C6 /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 503E58F22D3C10BF007BD8C6 /* LookinServer */; };
503E591E2D3D0F1C007BD8C6 /* SPIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 503E591D2D3D0F1C007BD8C6 /* SPIndicator */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
505109962D3C0D1000F62A71 /* ChidoriMenuExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChidoriMenuExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
505109982D3C0D1000F62A71 /* ChidoriMenuExample */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ChidoriMenuExample;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
505109932D3C0D1000F62A71 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
503E58F02D3C0D6B007BD8C6 /* ChidoriMenu in Frameworks */,
503E58F32D3C10BF007BD8C6 /* LookinServer in Frameworks */,
503E591E2D3D0F1C007BD8C6 /* SPIndicator in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5051098D2D3C0D1000F62A71 = {
isa = PBXGroup;
children = (
505109982D3C0D1000F62A71 /* ChidoriMenuExample */,
505109972D3C0D1000F62A71 /* Products */,
);
sourceTree = "<group>";
};
505109972D3C0D1000F62A71 /* Products */ = {
isa = PBXGroup;
children = (
505109962D3C0D1000F62A71 /* ChidoriMenuExample.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
505109952D3C0D1000F62A71 /* ChidoriMenuExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = 505109A42D3C0D1100F62A71 /* Build configuration list for PBXNativeTarget "ChidoriMenuExample" */;
buildPhases = (
503E59202D3D34C8007BD8C6 /* Format Source */,
505109922D3C0D1000F62A71 /* Sources */,
505109932D3C0D1000F62A71 /* Frameworks */,
505109942D3C0D1000F62A71 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
505109982D3C0D1000F62A71 /* ChidoriMenuExample */,
);
name = ChidoriMenuExample;
packageProductDependencies = (
503E58EF2D3C0D6B007BD8C6 /* ChidoriMenu */,
503E58F22D3C10BF007BD8C6 /* LookinServer */,
503E591D2D3D0F1C007BD8C6 /* SPIndicator */,
);
productName = ChidoriMenuExample;
productReference = 505109962D3C0D1000F62A71 /* ChidoriMenuExample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
5051098E2D3C0D1000F62A71 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
505109952D3C0D1000F62A71 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = 505109912D3C0D1000F62A71 /* Build configuration list for PBXProject "ChidoriMenuExample" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 5051098D2D3C0D1000F62A71;
minimizedProjectReferenceProxies = 1;
packageReferences = (
503E58F12D3C10BF007BD8C6 /* XCRemoteSwiftPackageReference "LookinServer" */,
503E591C2D3D0F1C007BD8C6 /* XCRemoteSwiftPackageReference "SPIndicator" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 505109972D3C0D1000F62A71 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
505109952D3C0D1000F62A71 /* ChidoriMenuExample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
505109942D3C0D1000F62A71 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
503E59202D3D34C8007BD8C6 /* Format Source */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Format Source";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "swiftformat . --swiftversion 6.0 --indent 4 || true\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
505109922D3C0D1000F62A71 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
505109A22D3C0D1100F62A71 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
505109A32D3C0D1100F62A71 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
505109A52D3C0D1100F62A71 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ChidoriMenuExample/ChidoriMenuExample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 964G86XT2P;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.ChidoriMenuExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
505109A62D3C0D1100F62A71 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ChidoriMenuExample/ChidoriMenuExample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 964G86XT2P;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.ChidoriMenuExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
505109912D3C0D1000F62A71 /* Build configuration list for PBXProject "ChidoriMenuExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
505109A22D3C0D1100F62A71 /* Debug */,
505109A32D3C0D1100F62A71 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
505109A42D3C0D1100F62A71 /* Build configuration list for PBXNativeTarget "ChidoriMenuExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
505109A52D3C0D1100F62A71 /* Debug */,
505109A62D3C0D1100F62A71 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
503E58F12D3C10BF007BD8C6 /* XCRemoteSwiftPackageReference "LookinServer" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/QMUI/LookinServer/";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.8;
};
};
503E591C2D3D0F1C007BD8C6 /* XCRemoteSwiftPackageReference "SPIndicator" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ivanvorobei/SPIndicator";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.6.5;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
503E58EF2D3C0D6B007BD8C6 /* ChidoriMenu */ = {
isa = XCSwiftPackageProductDependency;
productName = ChidoriMenu;
};
503E58F22D3C10BF007BD8C6 /* LookinServer */ = {
isa = XCSwiftPackageProductDependency;
package = 503E58F12D3C10BF007BD8C6 /* XCRemoteSwiftPackageReference "LookinServer" */;
productName = LookinServer;
};
503E591D2D3D0F1C007BD8C6 /* SPIndicator */ = {
isa = XCSwiftPackageProductDependency;
package = 503E591C2D3D0F1C007BD8C6 /* XCRemoteSwiftPackageReference "SPIndicator" */;
productName = SPIndicator;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 5051098E2D3C0D1000F62A71 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:ChidoriMenuExample.xcodeproj">
</FileRef>
<FileRef
location = "group:../">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,394 @@
//
// App.swift
// ChidoriMenuExample
//
// Created by on 1/19/25.
//
import ChidoriMenu
import SPIndicator
import SwiftUI
@main
struct ChidoriMenuExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
Content()
.ignoresSafeArea()
.navigationTitle("Chidori Menu")
}
.navigationViewStyle(.stack)
}
}
struct Content: UIViewControllerRepresentable {
class ContentController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = FooterButton()
view.addSubview(tableView)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
tableView.frame = view.bounds
}
struct Menu {
let title: String
let menu: UIMenu
}
var firstMenu: Menu = .init(title: "Show Menu Set", menu: .init(children: [
UIAction(
title: "Copy",
image: UIImage(systemName: "doc.on.doc"),
state: .on
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled,
state: .off
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete",
image: UIImage(systemName: "trash"),
attributes: .destructive,
state: .mixed
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]))
var secondMenu: Menu = .init(title: "Show Nested Menu Set", menu: .init(title: "Root Menu", children: [
UIAction(
title: "Copy",
image: UIImage(systemName: "doc.on.doc"),
state: .on
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIMenu(
title: "Child Menu",
image: .init(systemName: "menucard"),
children: [
UIAction(
title: "Copy",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIMenu(
title: "Child Menu",
image: .init(systemName: "menucard"),
children: [
UIAction(
title: "Copy",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
]
),
]
),
UIMenu(
title: "Inline Menu",
image: UIImage(systemName: "arrow.right"),
options: [.displayInline],
children: [
UIAction(
title: "Copy",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
]))
var veryLongMenu: Menu = .init(title: "Show Looong Menu Set", menu: .init(title: "Root Menu", children: [
UIAction(
title: "Copy 1",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 1",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 1",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
UIMenu(
options: [.displayInline],
children: [
UIAction(
title: "Copy 2",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 2",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 2",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
UIMenu(
options: [.displayInline],
children: [
UIAction(
title: "Copy 3",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 3",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 3",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
UIMenu(
title: "Submenu Here",
children: [
UIAction(
title: "Copy 4",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 4",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 4",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
UIMenu(
title: "Hello World",
options: [.displayInline],
children: [
UIAction(
title: "Copy 5",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 5",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 5",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
UIMenu(
options: [.displayInline],
children: [
UIAction(
title: "Copy 6",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 6",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 6",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
UIMenu(
options: [.displayInline],
children: [
UIAction(
title: "Copy 7",
image: UIImage(systemName: "doc.on.doc")
) { _ in
SPIndicatorView(title: "Copied", preset: .done).present()
},
UIAction(
title: "Paste 7",
image: UIImage(systemName: "doc.on.doc"),
attributes: .disabled
) { _ in
SPIndicatorView(title: "Pasted", preset: .done).present()
},
UIAction(
title: "Delete 7",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { _ in
SPIndicatorView(title: "Delete", preset: .done).present()
},
]
),
]))
var menuList: [Menu] { [
firstMenu,
secondMenu,
veryLongMenu,
] }
func numberOfSections(in _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
menuList.count
}
func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
let menu = menuList[indexPath.row]
cell.textLabel?.text = menu.title
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let cell = tableView.cellForRow(at: indexPath) else { return }
let anchorView = UIView()
cell.addSubview(anchorView)
anchorView.frame = .init(
x: cell.bounds.midX,
y: cell.bounds.midY,
width: 0,
height: 0
)
let menu = menuList[indexPath.row].menu
anchorView.present(menu: menu)
anchorView.removeFromSuperview()
}
func tableView(_ tableView: UITableView, viewForHeaderInSection _: Int) -> UIView? {
let view = UIView()
view.frame = .init(x: 0, y: 0, width: tableView.bounds.width, height: 20)
return view
}
}
func makeUIViewController(context _: Context) -> ContentController {
ContentController()
}
func updateUIViewController(_: ContentController, context _: Context) {}
}
class FooterButton: UIButton {
init() {
super.init(frame: .init(x: 0, y: 0, width: 200, height: 44))
setTitle("Test Menu", for: .normal)
setTitleColor(.systemBlue, for: .normal)
titleLabel?.font = .preferredFont(forTextStyle: .footnote)
interactions = [UIContextMenuInteraction(delegate: self)]
addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func buttonTapped() {
presentMenu()
}
override func contextMenuInteraction(_: UIContextMenuInteraction, configurationForMenuAtLocation _: CGPoint) -> UIContextMenuConfiguration? {
.init(identifier: nil, previewProvider: nil) { items in
.init(title: "Hello World", children: items + [UIAction(title: "Action A") { _ in
SPIndicator.present(title: "Action A", haptic: .success)
}])
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Christian Selig & Lakr Aream
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,18 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ChidoriMenu",
platforms: [.iOS(.v15), .macCatalyst(.v15)],
products: [
.library(
name: "ChidoriMenu",
targets: ["ChidoriMenu"]
),
],
targets: [
.target(name: "ChidoriMenu"),
]
)

View File

@@ -0,0 +1,46 @@
# ChidoriMenu 🐦⚡️
A seamless drop-in replacement for UIMenu & UIAction, featuring nested menu support and dark mode compatibility.
The inspiration behind this project stems from Apple's lack of a unified interface across iOS and Mac Catalyst apps, as well as the absence of a built-in menu presentation method. To address these gaps—and to enable greater customization—we developed ChidoriMenu.
This project draws heavily on code from [ChidoriMenu](https://github.com/christianselig/ChidoriMenu), and as such, we adhere to the same license.
## Preview
![Screenshot](./Resources/IMG_4262.JPG)
## Features
- [x] Added support for UIMenu & UIAction
- [x] Added drop-in replacement for `_presentMenuAtLocation:` (not recommended for general use)
- [x] Added support for nested menus in child elements
- [x] Added support for the `.displayInline` menu option
- [x] Added compatibility with dark mode
- [x] Fixed scrolling issues during selection
- [x] Fixed multiple actions triggering simultaneously
- [ ] Added support for UIDeferredMenuElement (Contributions Welcome!)
## Requirements
- iOS 15.0 or later
- macCatalyst 15.0 or later
## Usage
Getting started is straightforward. While the interface differs slightly from Apple's menu implementation, the core principles remain the same. Mac Catalyst is fully supported, though it does not bridge to AppKit menus—it functions identically to iOS.
```swift
UIButton.presentMenu()
UIView.present(menu: menu)
```
For detailed examples, check out the example project included in the repository.
## License
ChidoriMenu is available under the MIT license. See the LICENSE file for more info.
---
2025.1.20 - Made with ❤️ by Lakr233

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,114 @@
//
// ChidoriMenu+Cell.swift
// Chidori
//
// Created by Christian Selig on 2021-02-16.
//
import UIKit
extension ChidoriMenu {
class Cell: UITableViewCell {
var menuTitle: String = "" {
didSet { textLabel?.text = menuTitle }
}
var isDestructive: Bool = false {
didSet {
let color: UIColor = isDestructive ? .systemRed : .label
textLabel?.textColor = color
imageView?.tintColor = color
}
}
var iconImage: UIImage? {
didSet { imageView?.image = iconImage }
}
override var accessibilityHint: String? {
get { super.accessibilityHint ?? menuTitle }
set { super.accessibilityHint = newValue }
}
let sep = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
selectionStyle = .none
accessibilityTraits = [.button]
textLabel?.textColor = .label
imageView?.contentMode = .scaleAspectFit
imageView?.tintColor = .label
accessoryView?.isUserInteractionEnabled = false
preservesSuperviewLayoutMargins = false
separatorInset = UIEdgeInsets.zero
layoutMargins = UIEdgeInsets.zero
sep.backgroundColor = ChidoriMenu.dimmingSectionSepratorColor
contentView.addSubview(sep)
}
@available(*, unavailable)
required init?(coder _: NSCoder) { fatalError() }
override func layoutSubviews() {
super.layoutSubviews()
sep.frame = .init(
x: 0,
y: 0,
width: bounds.width,
height: 1
)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
backgroundColor = selected ? Self.highlightCoverColor : .clear
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setSelected(highlighted, animated: animated)
backgroundColor = highlighted ? Self.highlightCoverColor : .clear
}
}
class HeaderCell: UIView {
let titleLabel: UILabel = .init()
init() {
super.init(frame: .zero)
titleLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
titleLabel.numberOfLines = 1
titleLabel.textColor = .secondaryLabel
titleLabel.textAlignment = .left
addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(
equalTo: leadingAnchor,
constant: 8
),
titleLabel.trailingAnchor.constraint(
equalTo: trailingAnchor,
constant: -8
),
titleLabel.centerYAnchor.constraint(
equalTo: centerYAnchor,
constant: -ChidoriMenu.sectionTopPadding / 2
),
])
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
}

View File

@@ -0,0 +1,26 @@
//
// ChidoriMenu+Constant.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
extension ChidoriMenu {
static let width: CGFloat = 256
static let cornerRadius: CGFloat = 14
static let shadowRadius: CGFloat = cornerRadius
static let sectionTopPadding: CGFloat = 6
static let offsetY: CGFloat = 10
static let dimmingBackgroundColor = UIColor.black.withAlphaComponent(0.2)
static let dimmingSectionSepratorColor = UIColor.black.withAlphaComponent(0.2)
static let dimmingSectionSepratorHeight: CGFloat = 4
static let stackScaleFactor: CGFloat = 0.05
}
extension ChidoriMenu.Cell {
static let highlightCoverColor = UIColor.black.withAlphaComponent(0.1)
}

View File

@@ -0,0 +1,125 @@
//
// ChidoriMenu+DataSource.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import Foundation
import UIKit
extension ChidoriMenu {
struct MenuSection: Identifiable, Hashable {
let id = UUID()
let title: String
}
struct MenuContent: Identifiable, Hashable {
let id = UUID()
let content: Content
enum Content {
case action(UIAction)
case submenu(UIMenu)
// case deferred(([UIMenuElement]) -> ())
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: ChidoriMenu.MenuContent, rhs: ChidoriMenu.MenuContent) -> Bool {
lhs.hashValue == rhs.hashValue
}
}
}
extension ChidoriMenu.MenuContent {
var description: String {
switch content {
case let .action(action):
"action: \(action.title)"
case let .submenu(menu):
"submenu: \(menu.title)"
}
}
}
extension ChidoriMenu {
typealias DataSource = UITableViewDiffableDataSource<MenuSection, MenuContent>
typealias Snapshot = NSDiffableDataSourceSnapshot<MenuSection, MenuContent>
func updateSnapshot() {
var snapshot = Snapshot()
let contents = flatMap(menu: menu)
for (section, items) in contents {
snapshot.appendSections([section])
snapshot.appendItems(items, toSection: section)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
func flatMap(menu: UIMenu) -> [(MenuSection, [MenuContent])] {
var result: [(MenuSection, [MenuContent])] = []
var sectionTitle: String = menu.title
var sectionBuilder: [MenuContent] = []
let sectionBuilderCommit = {
defer {
sectionTitle = ""
sectionBuilder = []
}
guard !sectionBuilder.isEmpty else { return }
let section = MenuSection(title: sectionTitle)
result.append((section, sectionBuilder))
}
for element in menu.children {
if let action = element as? UIAction {
sectionBuilder.append(.init(content: .action(action)))
continue
}
if let childMenu = element as? UIMenu {
if childMenu.options.contains(.displayInline) {
sectionBuilderCommit()
for (section, items) in flatMap(menu: childMenu) {
result.append((section, items))
}
continue
}
sectionBuilder.append(.init(content: .submenu(childMenu)))
continue
}
if let deferred = element as? UIDeferredMenuElement {
// TODO: IMPL
assertionFailure("current \(deferred) unsupported")
}
assertionFailure("unknown menu element")
}
sectionBuilderCommit()
return result
}
func executeAction(_ indexPath: IndexPath) {
guard let action = dataSource.itemIdentifier(for: indexPath) else { return }
guard let cell = tableView.cellForRow(at: indexPath) else { return }
for indexPath in tableView.indexPathsForSelectedRows ?? [] {
tableView.deselectRow(at: indexPath, animated: true)
}
let content = action.content
switch content {
case let .action(action):
action.execute()
presentingParent?.dismiss(animated: true)
case let .submenu(menu):
cell.present(menu: menu, anchorPoint: .init(
x: cell.convert(cell.bounds, to: cell.window ?? .init()).midX,
y: cell.convert(.zero, to: cell.window ?? .init()).minY - ChidoriMenu.offsetY
))
iterateMenusInStack {
$0.menuStackScaleFactor *= 1 - ChidoriMenu.stackScaleFactor
}
}
}
}

View File

@@ -0,0 +1,31 @@
//
// ChidoriMenu+Gesture.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
extension ChidoriMenu {
@objc func panned(panGestureRecognizer: UIPanGestureRecognizer) {
let offsetInTableView = panGestureRecognizer.location(in: tableView)
guard let indexPath = tableView.indexPathForRow(at: offsetInTableView) else {
// If we pan outside the table and there's a cell selected, unselect it
for indexPath in tableView.indexPathsForSelectedRows ?? [] {
tableView.deselectRow(at: indexPath, animated: false)
}
return
}
if panGestureRecognizer.state == .ended {
// Treat is as a tap
for indexPath in tableView.indexPathsForSelectedRows ?? [] {
tableView.deselectRow(at: indexPath, animated: false)
}
executeAction(indexPath)
} else {
// This API always confuses me, it does not *select* the cell in a way that would call `didSelectRowAtIndexPath`, this just visually highlights it!
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
}

View File

@@ -0,0 +1,78 @@
//
// ChidoriMenu+TableView.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
extension ChidoriMenu: UITableViewDelegate {
func cell(forRowAtIndex indexPath: IndexPath, dataSource: DataSource) -> UITableViewCell? {
guard let section = dataSource.sectionIdentifier(for: indexPath.section),
let item = dataSource.itemIdentifier(for: indexPath)
else { return nil }
let cell = tableView.dequeueReusableCell(
withIdentifier: String(describing: Cell.self),
for: indexPath
) as! Cell
switch item.content {
case let .action(action):
cell.menuTitle = action.title
cell.iconImage = action.image
cell.isDestructive = action.attributes.contains(.destructive)
switch action.state {
case .on: cell.accessoryType = .checkmark
case .mixed: cell.accessoryType = .detailButton
default: cell.accessoryType = .none
}
cell.accessoryView?.tintColor = .label
case let .submenu(menu):
cell.menuTitle = menu.title
cell.iconImage = menu.image
cell.isDestructive = false
cell.accessoryType = .disclosureIndicator
cell.accessoryView?.tintColor = .label
}
if section.title.isEmpty, indexPath.row == 0, indexPath.section == 0 {
cell.sep.isHidden = true
} else {
cell.sep.isHidden = false
}
return cell
}
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if section == tableView.numberOfSections - 1 { return nil }
let footerView = UIView(frame: .zero)
footerView.backgroundColor = ChidoriMenu.dimmingSectionSepratorColor
return footerView
}
public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
if section == tableView.numberOfSections - 1 { return 0 }
return ChidoriMenu.dimmingSectionSepratorHeight
}
public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
executeAction(indexPath)
}
public func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = dataSource.sectionIdentifier(for: section) else {
return 0
}
if section.title.isEmpty { return 0 }
return UIFont.preferredFont(forTextStyle: .footnote).lineHeight + 8
}
public func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let section = dataSource.sectionIdentifier(for: section) else {
return nil
}
if section.title.isEmpty { return nil }
let cell = HeaderCell()
cell.titleLabel.text = section.title
return cell
}
}

View File

@@ -0,0 +1,51 @@
//
// ChidoriMenu+Transition.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
extension ChidoriMenu: UIViewControllerTransitioningDelegate {
public func animationController(
forPresented _: UIViewController,
presenting _: UIViewController,
source _: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
transitionController = ChidoriAnimationController(type: .presentation)
return transitionController
}
public func interactionControllerForPresentation(
using _: UIViewControllerAnimatedTransitioning
) -> UIViewControllerInteractiveTransitioning? {
transitionController
}
public func animationController(
forDismissed _: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
ChidoriAnimationController(type: .dismissal)
}
public func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source _: UIViewController
) -> UIPresentationController? {
let controller = ChidoriPresentationController(
presentedViewController: presented,
presenting: presenting
)
controller.transitionDelegate = self
return controller
}
}
extension ChidoriMenu: ChidoriPresentationController.Delegate {
func didTapOverlayView(_: ChidoriPresentationController) {
transitionController?.cancelTransition()
dismiss(animated: true)
}
}

View File

@@ -0,0 +1,189 @@
//
// ChidoriMenu.swift
// Chidori
//
// Created by Christian Selig on 2021-02-15.
//
import UIKit
public class ChidoriMenu: UIViewController {
let tableView: UITableView
var dataSource: DataSource!
let menu: UIMenu
let anchorPoint: CGPoint
let backgroundView = UIView()
let shadowView = UIView()
let panGestureRecognizer: UIPanGestureRecognizer = .init()
var transitionController: ChidoriAnimationController?
var height: CGFloat {
tableView.sizeThatFits(
CGSize(
width: ChidoriMenu.width,
height: CGFloat.greatestFiniteMagnitude
)
).height.rounded(.up)
}
var presentingParent: UIViewController? {
var parent: UIViewController? = presentingViewController
while let superMenu = parent as? ChidoriMenu {
parent = superMenu.presentingViewController
}
return parent
}
var menuStackScaleFactor: CGFloat = 1.0 {
didSet { UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) { [self] in
let factor = menuStackScaleFactor
view.transform = CGAffineTransform(scaleX: factor, y: factor)
view.layoutIfNeeded()
} }
}
required init(menu: UIMenu, anchorPoint: CGPoint) {
self.menu = menu
self.anchorPoint = anchorPoint
tableView = TableView(frame: .zero, style: .plain)
tableView.register(Cell.self, forCellReuseIdentifier: String(describing: Cell.self))
tableView.dataSource = dataSource
super.init(nibName: nil, bundle: nil)
dataSource = DataSource(
tableView: tableView
) { [weak self] _, indexPath, _ -> UITableViewCell? in
guard let self else { return nil }
return cell(forRowAtIndex: indexPath, dataSource: dataSource)
}
modalPresentationStyle = .custom
transitioningDelegate = self
}
@available(*, unavailable)
public required init?(coder _: NSCoder) { fatalError() }
override public func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
view.layer.masksToBounds = false
shadowView.backgroundColor = .systemBackground
shadowView.layer.shadowColor = UIColor { provider in
switch provider.userInterfaceStyle {
case .dark: UIColor.black.withAlphaComponent(0.25)
default: UIColor.black.withAlphaComponent(0.1)
}
}.cgColor
shadowView.layer.shadowOpacity = 1
shadowView.layer.shadowOffset = .zero
shadowView.layer.shadowRadius = 8
shadowView.layer.cornerRadius = ChidoriMenu.cornerRadius
view.addSubview(shadowView)
backgroundView.backgroundColor = .init(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
backgroundView.layer.masksToBounds = true
backgroundView.layer.cornerRadius = ChidoriMenu.cornerRadius
backgroundView.layer.cornerCurve = .continuous
view.addSubview(backgroundView)
tableView.separatorInset = .zero
tableView.contentInset = .zero
tableView.delegate = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.backgroundColor = .clear
tableView.allowsMultipleSelection = false
tableView.selectionFollowsFocus = true
tableView.allowsSelection = true
tableView.allowsFocus = false
tableView.sectionHeaderTopPadding = Self.sectionTopPadding
tableView.verticalScrollIndicatorInsets = UIEdgeInsets(
top: ChidoriMenu.cornerRadius,
left: 0.0,
bottom: ChidoriMenu.cornerRadius,
right: 0.0
)
backgroundView.addSubview(tableView)
tableView.estimatedSectionHeaderHeight = 0.0
tableView.estimatedRowHeight = 0.0
tableView.estimatedSectionFooterHeight = 0.0
panGestureRecognizer.addTarget(
self,
action: #selector(panned(panGestureRecognizer:))
)
panGestureRecognizer.cancelsTouchesInView = true
tableView.addGestureRecognizer(panGestureRecognizer)
updateSnapshot()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
transitionController = nil
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
backgroundView.frame = view.bounds
let contentFrame = backgroundView.bounds
tableView.separatorStyle = .none
tableView.frame = .init(
x: contentFrame.minX,
y: contentFrame.minY,
width: contentFrame.width,
height: contentFrame.height
)
shadowView.layer.shadowPath = UIBezierPath(
roundedRect: .init(
x: 0,
y: 0,
width: ChidoriMenu.width,
height: tableView.frame.height
),
cornerRadius: ChidoriMenu.cornerRadius
).cgPath
let isTableViewNotFullyVisible = tableView.contentSize.height > view.bounds.height
tableView.isScrollEnabled = isTableViewNotFullyVisible
panGestureRecognizer.isEnabled = !isTableViewNotFullyVisible
}
override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
let superController = presentingViewController as? ChidoriMenu
superController?.menuStackScaleFactor = 1.0
super.dismiss(animated: flag, completion: completion)
}
func dismissIfEmpty() {
var count = 0
for idx in 0 ..< dataSource.numberOfSections(in: tableView) {
count += dataSource.tableView(tableView, numberOfRowsInSection: idx)
}
guard count <= 0 else { return }
dismiss(animated: true)
}
func iterateMenusInStack(_ executing: @escaping (ChidoriMenu) -> Void) {
var parent: ChidoriMenu? = self
while let currentParent = parent {
executing(currentParent)
parent = currentParent.presentingViewController as? ChidoriMenu
}
}
}

View File

@@ -0,0 +1,60 @@
//
// Present+UIButton.swift
//
//
// Created by QAQ on 2023/9/16.
//
import UIKit
public extension UIButton {
func presentMenu() {
guard let menu = retrieveMenu() else { return }
present(menu: menu)
}
}
extension UIButton {
private func retrieveMenu() -> UIMenu? {
for interaction in interactions {
if let menuInteraction = interaction as? UIContextMenuInteraction,
let menuConfig = menuInteraction.delegate?.contextMenuInteraction(
menuInteraction,
configurationForMenuAtLocation: .zero
),
let menu = menuConfig.retrieveMenu()
{
return menu
}
}
if let menuConfig = contextMenuInteraction(
.init(delegate: RetrieveMenuDelegate.shared),
configurationForMenuAtLocation: .zero
), let menu = menuConfig.retrieveMenu() {
return menu
}
return nil
}
}
private class RetrieveMenuDelegate: NSObject, UIContextMenuInteractionDelegate {
static let shared = RetrieveMenuDelegate()
func contextMenuInteraction(
_: UIContextMenuInteraction,
configurationForMenuAtLocation _: CGPoint
) -> UIContextMenuConfiguration? {
nil
}
}
private extension UIContextMenuConfiguration {
func retrieveMenu() -> UIMenu? {
guard responds(to: NSSelectorFromString(["Provider", "action"].reversed().joined())),
let actionProvider = value(forKey: ["Provider", "action", "_"].reversed().joined())
else { return nil }
typealias ActionProviderBlock = @convention(block) ([UIMenuElement]) -> (UIMenu?)
let blockPtr = UnsafeRawPointer(Unmanaged<AnyObject>.passUnretained(actionProvider as AnyObject).toOpaque())
let handler = unsafeBitCast(blockPtr, to: ActionProviderBlock.self)
return handler([])
}
}

View File

@@ -0,0 +1,25 @@
//
// Present+UIView.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
public extension UIView {
func present(menu: UIMenu, anchorPoint: CGPoint? = nil, onMenuCreated: (ChidoriMenu) -> Void = { _ in }) {
guard let presenter = parentViewController else { return }
let chidoriMenu = ChidoriMenu(
menu: menu,
anchorPoint: anchorPoint ?? convert(.init(
x: bounds.midX,
y: bounds.midY
), to: window)
)
onMenuCreated(chidoriMenu)
presenter.present(chidoriMenu, animated: true) {
chidoriMenu.dismissIfEmpty()
}
}
}

View File

@@ -0,0 +1,20 @@
//
// UIAction.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
extension UIAction {
func execute() {
guard responds(to: NSSelectorFromString("handler")),
let handler = value(forKey: "_handler")
else { return }
typealias ActionBlock = @convention(block) (UIAction) -> Void
let blockPtr = UnsafeRawPointer(Unmanaged<AnyObject>.passUnretained(handler as AnyObject).toOpaque())
let block = unsafeBitCast(blockPtr, to: ActionBlock.self)
return block(self)
}
}

View File

@@ -0,0 +1,21 @@
//
// UIView.swift
//
//
// Created by QAQ on 2023/9/16.
//
import UIKit
extension UIView {
var parentViewController: UIViewController? {
weak var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}

View File

@@ -0,0 +1,139 @@
//
// ChidoriAnimationController.swift
// Chidori
//
// Created by Christian Selig on 2021-02-15.
//
import UIKit
private typealias ChidoriDelegateProtocol = AnyObject
& UIViewControllerAnimatedTransitioning
& UIViewControllerInteractiveTransitioning
class ChidoriAnimationController: NSObject, ChidoriDelegateProtocol {
enum AnimationControllerType { case presentation, dismissal }
let type: AnimationControllerType
var animator: UIViewPropertyAnimator?
weak var context: UIViewControllerContextTransitioning?
init(type: AnimationControllerType) {
self.type = type
}
func transitionDuration(
using _: UIViewControllerContextTransitioning?
) -> TimeInterval {
switch type {
case .presentation:
0.5
case .dismissal:
0.35
}
}
func startInteractiveTransition(
_ transitionContext: UIViewControllerContextTransitioning
) {
context = transitionContext
animateTransition(using: transitionContext)
}
func cancelTransition() {
guard let context, let animator else { return }
context.cancelInteractiveTransition()
animator.isReversed = true
animator.startAnimation()
guard type == .presentation else { return }
if let presentingController = context.viewController(forKey: .from) {
presentingController.view.tintAdjustmentMode = .automatic
}
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let interruptableAnimator = interruptibleAnimator(using: transitionContext)
switch type {
case .presentation:
if let menu = transitionContext.viewController(
forKey: .to
) as? ChidoriMenu { transitionContext.containerView.addSubview(menu.view) }
if let presentingController = transitionContext.viewController(
forKey: .from
) { presentingController.view.tintAdjustmentMode = .dimmed }
case .dismissal:
if let presentingController = transitionContext.viewController(
forKey: .to
) { presentingController.view.tintAdjustmentMode = .automatic }
}
interruptableAnimator.startAnimation()
}
func interruptibleAnimator(
using context: UIViewControllerContextTransitioning
) -> UIViewImplicitlyAnimating {
if let animator { return animator }
let duration = transitionDuration(using: context)
let propertyAnimator = UIViewPropertyAnimator(
duration: duration,
timingParameters: UISpringTimingParameters(
dampingRatio: 0.8,
initialVelocity: .init(dx: 0.8, dy: 0.8)
)
)
propertyAnimator.isInterruptible = true
propertyAnimator.isUserInteractionEnabled = true
let isPresenting = type == .presentation
guard let menu = (
isPresenting
? context.viewController(forKey: .to)
: context.viewController(forKey: .from)
) as? ChidoriMenu else {
preconditionFailure()
}
let finalFrame = context.finalFrame(for: menu)
menu.view.frame = finalFrame
let translationRequired: CGVector = .init(
dx: 0,
dy: menu.view.frame.height * -0.1
)
let initialAlpha: CGFloat = isPresenting ? 0.0 : 1.0
let finalAlpha: CGFloat = isPresenting ? 1.0 : 0.0
let transform = CGAffineTransform(
translationX: translationRequired.dx,
y: translationRequired.dy
).scaledBy(x: 0.9, y: 0.9)
let initialTransform = isPresenting ? transform : .identity
let finalTransform = isPresenting ? .identity : transform
menu.view.transform = initialTransform
.concatenating(menu.view.transform)
menu.view.alpha = initialAlpha
propertyAnimator.addAnimations {
menu.view.transform = finalTransform
menu.view.alpha = finalAlpha
}
propertyAnimator.addCompletion { _ in
context.completeTransition(!context.transitionWasCancelled)
self.animator = nil
}
animator = propertyAnimator
return propertyAnimator
}
}

View File

@@ -0,0 +1,121 @@
//
// ChidoriPresentationController.swift
// Chidori
//
// Created by Christian Selig on 2021-02-15.
//
import UIKit
class ChidoriPresentationController: UIPresentationController {
let dimmView: UIView = .init()
var minimalEdgeInset: CGFloat = 10
protocol Delegate: AnyObject {
func didTapOverlayView(_ chidoriPresentationController: ChidoriPresentationController)
}
weak var transitionDelegate: Delegate?
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
guard let containerView else { return }
dimmView.translatesAutoresizingMaskIntoConstraints = false
dimmView.isUserInteractionEnabled = true
dimmView.isAccessibilityElement = true
dimmView.accessibilityTraits = .button
dimmView.accessibilityHint = NSLocalizedString("Close Menu", comment: "")
dimmView.backgroundColor = ChidoriMenu.dimmingBackgroundColor
dimmView.alpha = 0.0
presentingViewController.view.tintAdjustmentMode = .dimmed
containerView.addSubview(dimmView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: dimmView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: dimmView.trailingAnchor),
containerView.topAnchor.constraint(equalTo: dimmView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: dimmView.bottomAnchor),
])
let tapGesture = UITapGestureRecognizer(target: nil, action: nil)
tapGesture.addTarget(self, action: #selector(dimmViewTapped))
dimmView.addGestureRecognizer(tapGesture)
if let transitionCoordinator = presentingViewController.transitionCoordinator {
transitionCoordinator.animate { _ in self.dimmView.alpha = 1.0 }
}
}
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
presentingViewController.view.tintAdjustmentMode = .automatic
if let transitionCoordinator = presentingViewController.transitionCoordinator {
transitionCoordinator.animate { _ in self.dimmView.alpha = 0.0 }
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed { dimmView.removeFromSuperview() }
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let menu = presentedViewController as? ChidoriMenu else {
return .zero
}
var height = menu.height
if let heightLimit = containerView?.bounds.height {
height = min(height, heightLimit * 0.75)
}
let menuSize = CGSize(width: ChidoriMenu.width, height: height)
let originatingPoint = calculateOriginatingPoint(
anchorPoint: menu.anchorPoint,
menuSize: menuSize
)
return CGRect(origin: originatingPoint, size: menuSize)
}
private func calculateOriginatingPoint(
anchorPoint: CGPoint,
menuSize: CGSize
) -> CGPoint {
guard let containerView else { return .zero }
let x: CGFloat = {
let requiredMinX = anchorPoint.x - ChidoriMenu.width / 2
let maxPossibleX = minimalEdgeInset
+ containerView.safeAreaInsets.left
let rightMostPermissableXPosition = containerView.bounds.width
- minimalEdgeInset
- containerView.safeAreaInsets.right
- menuSize.width
return min(
rightMostPermissableXPosition,
max(requiredMinX, maxPossibleX)
)
}()
let y: CGFloat = {
let maxY = anchorPoint.y + menuSize.height + minimalEdgeInset + ChidoriMenu.offsetY
let allowedY = containerView.bounds.height - containerView.safeAreaInsets.bottom
if maxY < allowedY { return anchorPoint.y + ChidoriMenu.offsetY /* move below a little bit */ }
// if not, iOS tries to keep as much in the bottom half of the screen as possible
// to be closer to where the thumb normally is, presumably
return containerView.bounds.height
- minimalEdgeInset
- containerView.safeAreaInsets.bottom
- menuSize.height
}()
return CGPoint(x: x, y: y)
}
@objc func dimmViewTapped() {
transitionDelegate?.didTapOverlayView(self)
}
}

View File

@@ -0,0 +1,15 @@
//
// TableView.swift
// ChidoriMenu
//
// Created by on 1/19/25.
//
import UIKit
class TableView: UITableView {
// - (BOOL)allowsHeaderViewsToFloat;
// - (BOOL)allowsFooterViewsToFloat;
@objc var allowsHeaderViewsToFloat: Bool { false }
@objc var allowsFooterViewsToFloat: Bool { false }
}

View File

@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MSDisplayLink",
"state" : {
"revision" : "c2fcd28cb99300d83acc30860ce252ef97c20b61",
"version" : "1.1.1"
"revision" : "6e92b5513e3473e064685e64056c4ac46470e7b0",
"version" : "2.0.3"
}
},
{

View File

@@ -14,21 +14,21 @@ let package = Package(
.library(name: "Intelligents", targets: ["Intelligents"]),
],
dependencies: [
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
.package(url: "https://github.com/Lakr233/SpringInterpolation", from: "1.3.0"),
.package(url: "https://github.com/Lakr233/MSDisplayLink", from: "1.1.1"),
.package(path: "../AffineGraphQL"),
.package(path: "../ChidoriMenu"),
.package(path: "../MarkdownView"),
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"),
.package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", from: "3.3.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
],
targets: [
.target(name: "Intelligents", dependencies: [
"AffineGraphQL",
"SpringInterpolation",
"MSDisplayLink",
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
"ChidoriMenu",
"MarkdownView",
.product(name: "Apollo", package: "apollo-ios"),
.product(name: "LDSwiftEventSource", package: "swift-eventsource"),
.product(name: "OrderedCollections", package: "swift-collections"),
]),
]
)

View File

@@ -37,14 +37,16 @@ public extension UIViewController {
}
func presentError(_ error: Error, onDismiss: @escaping () -> Void = {}) {
let alert = UIAlertController(
title: "Error".localized(),
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK".localized(), style: .default) { _ in
onDismiss()
})
present(alert, animated: true)
DispatchQueue.main.async { [self] in
let alert = UIAlertController(
title: "Error".localized(),
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK".localized(), style: .default) { _ in
onDismiss()
})
present(alert, animated: true)
}
}
}

View File

@@ -57,13 +57,23 @@ public extension UIViewController {
button.transform = .identity
button.setNeedsLayout()
self.view.layoutIfNeeded()
} completion: { _ in
button.isUserInteractionEnabled = true
}
}
func dismissIntelligentsButton() {
func dismissIntelligentsButton(animated: Bool = true) {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.isUserInteractionEnabled = false
if !animated {
button.stopProgress()
button.isHidden = true
return
}
button.stopProgress()
button.setNeedsLayout()
view.layoutIfNeeded()

View File

@@ -43,6 +43,7 @@ public class IntelligentsButton: UIView {
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue),
].forEach { $0.isActive = true }
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
addSubview(activityIndicator)
[
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
@@ -74,17 +75,25 @@ public class IntelligentsButton: UIView {
layer.cornerRadius = bounds.width / 2
}
private var allowedTap = true
@objc func tapped() {
guard allowedTap else { return }
delegate?.onIntelligentsButtonTapped(self)
}
public func beginProgress() {
allowedTap = false
activityIndicator.startAnimating()
activityIndicator.isHidden = false
image.isHidden = true
bringSubviewToFront(activityIndicator)
}
public func stopProgress() {
allowedTap = true
activityIndicator.stopAnimating()
activityIndicator.isHidden = true
image.isHidden = false
}
}

View File

@@ -1,100 +0,0 @@
//
// ChatTableView+BaseCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
private let initialInsetValue: CGFloat = 24
extension ChatTableView {
class BaseCell: UITableViewCell {
var inset: UIEdgeInsets { // available for overrides
.init(
top: initialInsetValue / 2,
left: initialInsetValue,
bottom: initialInsetValue / 2,
right: initialInsetValue
)
}
let containerView = UIView()
let roundedBackgroundView = UIView()
var viewModel: AnyObject? {
didSet { update(via: viewModel) }
}
enum BackgroundColorType {
case clear
case highlight
case warning
case lightGray
}
var backgroundColorType: BackgroundColorType = .clear {
didSet {
roundedBackgroundView.backgroundColor = backgroundColorType.color
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
roundedBackgroundView.clipsToBounds = true
roundedBackgroundView.layer.cornerRadius = 8
roundedBackgroundView.layer.masksToBounds = true
contentView.addSubview(roundedBackgroundView)
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
[ // inset half of the container view
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left / 2),
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right / 2),
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top / 2),
roundedBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom / 2),
].forEach { $0.isActive = true }
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
[
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right),
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func prepareForReuse() {
super.prepareForReuse()
viewModel = nil
}
func update(via object: AnyObject?) {
_ = object
}
}
}
extension ChatTableView.BaseCell.BackgroundColorType {
var color: UIColor {
switch self {
case .clear:
.clear
case .highlight:
.accent
case .warning:
.systemRed.withAlphaComponent(0.1)
case .lightGray:
.systemGray.withAlphaComponent(0.1)
}
}
}

View File

@@ -1,124 +0,0 @@
//
// ChatTableView+ChatCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import MarkdownUI
import UIKit
extension ChatTableView {
class ChatCell: BaseCell {
let avatarView = CircleImageView()
let titleLabel = UILabel()
let markdownContainer = UIView()
var markdownView: UIView?
var removableConstraints: [NSLayoutConstraint] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let spacingElement: CGFloat = 12
let avatarSize: CGFloat = 24
containerView.addSubview(avatarView)
avatarView.translatesAutoresizingMaskIntoConstraints = false
[
avatarView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
avatarView.topAnchor.constraint(equalTo: containerView.topAnchor),
avatarView.widthAnchor.constraint(equalToConstant: avatarSize),
avatarView.heightAnchor.constraint(equalToConstant: avatarSize),
].forEach { $0.isActive = true }
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .bold)
containerView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
[
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: spacingElement),
titleLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
].forEach { $0.isActive = true }
containerView.addSubview(markdownContainer)
markdownContainer.translatesAutoresizingMaskIntoConstraints = false
[
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: avatarView.bottomAnchor, constant: spacingElement),
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: spacingElement),
markdownContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
markdownContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
markdownContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func update(via object: AnyObject?) {
super.update(via: object)
guard let viewModel = object as? ViewModel else {
return
}
switch viewModel.participant {
case .system:
avatarView.image = UIImage(systemName: "gearshape.fill")
titleLabel.text = "System".localized()
backgroundColorType = .warning
case .assistant:
avatarView.image = UIImage(named: "spark", in: .module, with: .none)
titleLabel.text = "AFFiNE AI".localized()
backgroundColorType = .lightGray
case .user:
avatarView.image = UIImage(systemName: "person.fill")
titleLabel.text = "You".localized()
backgroundColorType = .clear
}
removableConstraints.forEach { $0.isActive = false }
if let markdownView { markdownView.removeFromSuperview() }
markdownContainer.subviews.forEach { $0.removeFromSuperview() }
let hostingView: UIView = UIHostingView(
rootView: Markdown(.init(viewModel.markdownDocument))
)
defer { markdownView = hostingView }
markdownContainer.addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
[
hostingView.topAnchor.constraint(equalTo: markdownContainer.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: markdownContainer.leadingAnchor),
hostingView.trailingAnchor.constraint(lessThanOrEqualTo: markdownContainer.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: markdownContainer.bottomAnchor),
].forEach {
$0.isActive = true
removableConstraints.append($0)
}
}
}
}
extension ChatTableView.ChatCell {
class ViewModel {
let participant: Participant
var markdownDocument: String
init(participant: Participant, markdownDocument: String) {
self.participant = participant
self.markdownDocument = markdownDocument
}
}
}
extension ChatTableView.ChatCell.ViewModel {
enum Participant {
case user
case assistant
case system
}
}

View File

@@ -1,57 +0,0 @@
//
// ChatTableView+Data.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension ChatTableView {
struct DataElement {
enum CellType: String, CaseIterable {
case base
case chat
}
let type: CellType
let object: AnyObject?
init(type: CellType, object: AnyObject?) {
self.type = type
self.object = object
}
}
}
extension ChatTableView.DataElement.CellType {
var cellClassType: ChatTableView.BaseCell.Type {
switch self {
case .base:
ChatTableView.BaseCell.self
case .chat:
ChatTableView.ChatCell.self
}
}
var cellIdentifier: String {
NSStringFromClass(cellClassType)
}
}
extension ChatTableView: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: dataSource[indexPath.row].type.cellIdentifier, for: indexPath) as! BaseCell
let object = dataSource[indexPath.row].object
cell.update(via: object)
return cell
}
}

View File

@@ -1,113 +0,0 @@
//
// ChatTableView+Scroll.swift
// Intelligents
//
// Created by on 2024/12/23.
//
import MSDisplayLink
import SpringInterpolation
import UIKit
extension ChatTableView: UIScrollViewDelegate, DisplayLinkDelegate {
func synchronization() {
let now = Date()
defer { scrollAnimationDeltaTimeHolder = now }
var deltaTime = now.timeIntervalSince(scrollAnimationDeltaTimeHolder)
if deltaTime > 0.5 { deltaTime = 0.5 }
guard scrollToBottomEnabled else { return }
DispatchQueue.main.async { self.tikVsync(deltaTime: deltaTime * 2) }
}
var bottomLocationY: CGFloat {
tableView.contentSize.height - tableView.bounds.height
}
private func tikVsync(deltaTime: TimeInterval) {
guard scrollToBottomEnabled else { return }
guard scrollToBottomAllowed else { return }
// read from contentSize if not needed to scroll
guard tableView.contentSize.height > tableView.bounds.height else {
resetAnimationContext(to: tableView.contentOffset.y)
return
}
guard abs(bottomLocationY - tableView.contentOffset.y) > 1 else {
return
}
scrollAnimationContext.setTarget(bottomLocationY)
scrollAnimationContext.update(withDeltaTime: deltaTime)
tableView.contentOffset.y = scrollAnimationContext.value
}
@inline(__always)
func resetAnimationContext(to offset: CGFloat) {
scrollAnimationContext.context = .init(
currentPos: offset,
currentVel: 0,
targetPos: offset
)
}
func presentScrollBottomIfNeeded() {
let visible = !scrollToBottomEnabled && scrollToBottomAllowed
UIView.animate(withDuration: 0.25) { [self] in
scrollDownButton.alpha = visible ? 1 : 0
}
}
func animationEnabledToggleDidSet(oldValue: Bool) {
assert(Thread.isMainThread)
guard scrollToBottomEnabled != oldValue else { return }
resetAnimationContext(to: tableView.contentOffset.y)
}
func animationAllowedToggleDidSet(oldValue: Bool) {
assert(Thread.isMainThread)
guard scrollToBottomAllowed != oldValue else { return }
resetAnimationContext(to: tableView.contentOffset.y)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
processScrollView(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate _: Bool) {
processScrollView(scrollView)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
processScrollView(scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
processScrollView(scrollView)
}
@inline(__always)
private func processScrollView(_ scrollView: UIScrollView) {
guard let tableView = scrollView as? UITableView else {
assertionFailure()
return
}
processTableViewMovements(tableView)
}
private func processTableViewMovements(_ tableView: UITableView) {
defer { presentScrollBottomIfNeeded() }
if tableView.isDragging {
scrollToBottomEnabled = false
scrollToBottomAllowed = false
} else {
//
scrollToBottomAllowed = !tableView.isDecelerating
//
let isBottom = tableView.contentOffset.y >= bottomLocationY
if isBottom { scrollToBottomEnabled = true }
}
}
func scrollToBottom() {
scrollToBottomEnabled = true
scrollToBottomAllowed = true
}
}

View File

@@ -1,86 +0,0 @@
//
// ChatTableView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import MSDisplayLink
import SpringInterpolation
import UIKit
class ChatTableView: UIView {
let tableView = UITableView()
let footerView = UIView()
let scrollDownButton = ScrollBottom()
var dataSource: [DataElement] = []
//
var scrollToBottomEnabled = false {
didSet { animationEnabledToggleDidSet(oldValue: oldValue) }
}
//
var scrollToBottomAllowed = false {
didSet { animationAllowedToggleDidSet(oldValue: oldValue) }
}
var scrollAnimationController: DisplayLink = .init()
var scrollAnimationContext: SpringInterpolation = .init()
var scrollAnimationDeltaTimeHolder: Date = .init()
init() {
super.init(frame: .zero)
for eachCase in DataElement.CellType.allCases {
let cellClass = eachCase.cellClassType
tableView.register(cellClass, forCellReuseIdentifier: eachCase.cellIdentifier)
}
tableView.backgroundColor = .clear
tableView.delegate = self
tableView.dataSource = self
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
[
tableView.topAnchor.constraint(equalTo: topAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
footerView.translatesAutoresizingMaskIntoConstraints = false
footerView.heightAnchor.constraint(equalToConstant: 128).isActive = true
footerView.widthAnchor.constraint(equalToConstant: 128).isActive = true
tableView.tableFooterView = footerView
tableView.separatorStyle = .none
addSubview(scrollDownButton)
scrollDownButton.translatesAutoresizingMaskIntoConstraints = false
[
// right bottom inset 16 size 32x32
scrollDownButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
scrollDownButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32),
scrollDownButton.widthAnchor.constraint(equalToConstant: 32),
scrollDownButton.heightAnchor.constraint(equalToConstant: 32),
].forEach { $0.isActive = true }
scrollDownButton.alpha = 0
scrollDownButton.onTap = { [weak self] in
self?.scrollToBottom()
}
scrollAnimationController.delegatingObject(self)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
func reloadData() {
tableView.reloadData()
}
}

View File

@@ -1,63 +0,0 @@
//
// ScrollBottom.swift
// Intelligents
//
// Created by on 2024/12/23.
//
import Foundation
import UIKit
class ScrollBottom: UIView {
let imageView = UIImageView()
let backgroundView: UIView = UIVisualEffectView(
effect: UIBlurEffect(style: .systemUltraThinMaterialDark)
)
var onTap: (() -> Void)?
init() {
super.init(frame: .zero)
addSubview(backgroundView)
addSubview(imageView)
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "arrow.down")
imageView.tintColor = .accent
isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(tapAction)
)
addGestureRecognizer(tapGesture)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
layer.cornerRadius = (bounds.width + bounds.height) / 4
layer.masksToBounds = true
backgroundView.frame = bounds
let imageInset = (bounds.width + bounds.height) / 8
imageView.frame = CGRect(
x: imageInset,
y: imageInset,
width: bounds.width - 2 * imageInset,
height: bounds.height - 2 * imageInset
)
}
@objc private func tapAction() {
onTap?()
}
}

View File

@@ -1,30 +0,0 @@
//
// IntelligentsChatController+Cell.swift
// Intelligents
//
// Created by on 2024/12/26.
//
import Foundation
extension IntelligentsChatController {
func insertIntoTableView(viewModel: ChatTableView.DataElement) {
assert(Thread.isMainThread)
tableView.dataSource.append(viewModel)
tableView.reloadData()
}
func insertIntoTableView(withChatModel chatModel: ChatTableView.ChatCell.ViewModel) {
insertIntoTableView(viewModel: .init(
type: .chat,
object: chatModel
))
}
func insertIntoTableView(withError error: Error) {
insertIntoTableView(withChatModel: .init(
participant: .system,
markdownDocument: error.localizedDescription
))
}
}

View File

@@ -7,6 +7,7 @@
import AffineGraphQL
import LDSwiftEventSource
import MarkdownParser
import UIKit
extension IntelligentsChatController {
@@ -29,6 +30,7 @@ extension IntelligentsChatController {
@objc func chat_onSend() {
beginProgress()
let viewModel = inputBox.editor.viewModel.duplicate()
viewModel.text = viewModel.text.trimmingCharacters(in: .whitespacesAndNewlines)
inputBox.editor.viewModel.reset()
inputBox.editor.updateValues()
DispatchQueue.global().async {
@@ -75,8 +77,12 @@ private extension IntelligentsChatController {
private extension IntelligentsChatController {
func chat_onError(_ error: Error) {
// TODO: IMPL add error cell
print("[*] chat error", error)
dispatchToMain {
let key = UUID()
let content = ChatContent.error(text: error.localizedDescription)
self.simpleChatContents.updateValue(content, forKey: key)
}
}
func chat_createSession(
@@ -85,9 +91,9 @@ private extension IntelligentsChatController {
) {
Intelligents.qlClient.perform(
mutation: CreateCopilotSessionMutation(options: .init(
docId: "", // TODO: put the real data
docId: metadata[.documentID] ?? "",
promptName: Prompt.general_Chat_With_AFFiNE_AI.rawValue,
workspaceId: "" // TODO: put the real data
workspaceId: metadata[.workspaceID] ?? ""
)),
queue: .global()
) { result in
@@ -117,10 +123,10 @@ private extension IntelligentsChatController {
// let images = viewModel.attachments
dispatchToMain {
self.insertIntoTableView(withChatModel: .init(
participant: .user,
markdownDocument: text
))
let content = ChatContent.user(document: text)
let key = UUID()
self.simpleChatContents.updateValue(content, forKey: key)
self.tableView.scrollLastCellToTop()
}
let sem = DispatchSemaphore(value: 0)
@@ -173,11 +179,13 @@ private extension IntelligentsChatController {
return
}
let chatModel = ChatTableView.ChatCell.ViewModel(
participant: .assistant,
markdownDocument: ""
)
dispatchToMain { self.insertIntoTableView(withChatModel: chatModel) }
let contentIdentifier = UUID()
dispatchToMain {
self.simpleChatContents.updateValue(
.assistant(document: "..."),
forKey: contentIdentifier
)
}
let sem = DispatchSemaphore(value: 0)
@@ -193,8 +201,14 @@ private extension IntelligentsChatController {
eventHandler.onErrorBlock = { error in
self.chat_onError(error)
}
var document = ""
eventHandler.onMessageBlock = { _, message in
self.chat_onEvent(message.data, chatModel: chatModel)
self.dispatchToMain {
document += message.data
let content = ChatContent.assistant(document: document)
self.simpleChatContents.updateValue(content, forKey: contentIdentifier)
}
}
let eventSource = EventSource(config: .init(handler: eventHandler, url: url))
chatTask = eventSource
@@ -202,11 +216,38 @@ private extension IntelligentsChatController {
sem.wait()
}
}
func chat_onEvent(_ data: String, chatModel: ChatTableView.ChatCell.ViewModel) {
dispatchToMain { [self] in
chatModel.markdownDocument += data
tableView.reloadData()
extension IntelligentsChatController {
func updateContentToPublisher() {
assert(Thread.isMainThread)
let copy = simpleChatContents
let input: [MessageListView.Element] = copy.map { key, value in
switch value {
case let .assistant(document):
let nodes = MarkdownParser().feed(document)
return .init(
id: key,
cell: .assistant,
viewModel: MessageListView.AssistantCell.ViewModel(blocks: nodes),
object: nil
)
case let .user(document):
return .init(
id: key,
cell: .user,
viewModel: MessageListView.UserCell.ViewModel(text: document),
object: nil
)
case let .error(text):
return .init(
id: key,
cell: .hint,
viewModel: MessageListView.HintCell.ViewModel(hint: text),
object: nil
)
}
}
publisher.send(input)
}
}

View File

@@ -1,43 +0,0 @@
//
// IntelligentsChatController+Keyboard.swift
// Intelligents
//
// Created by on 2024/12/6.
//
import UIKit
extension IntelligentsChatController {
@objc func keyboardWillAppear(_ notification: Notification) {
let info = notification.userInfo ?? [:]
let keyboardHeight = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?
.cgRectValue
.height ?? 0
inputBoxKeyboardAdapterHeightConstraint.constant = keyboardHeight
view.setNeedsUpdateConstraints()
animateWithKeyboard(userInfo: info)
}
@objc func keyboardWillDisappear(_ notification: Notification) {
let info = notification.userInfo ?? [:]
inputBoxKeyboardAdapterHeightConstraint.constant = 0
view.setNeedsUpdateConstraints()
animateWithKeyboard(userInfo: info)
}
private func animateWithKeyboard(userInfo info: [AnyHashable: Any]) {
let keyboardAnimationDuration = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?
.doubleValue ?? 0
let keyboardAnimationCurve = (info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?
.uintValue ?? 0
UIView.animate(
withDuration: keyboardAnimationDuration,
delay: 0,
options: UIView.AnimationOptions(rawValue: keyboardAnimationCurve),
animations: {
self.view.layoutIfNeeded()
},
completion: nil
)
}
}

View File

@@ -5,22 +5,43 @@
// Created by on 2024/11/18.
//
import Combine
import LDSwiftEventSource
import OrderedCollections
import UIKit
public class IntelligentsChatController: UIViewController {
let header = Header()
let inputBoxKeyboardAdapter = UIView()
let inputBox = InputBox()
let progressView = UIActivityIndicatorView()
let tableView = ChatTableView()
let publisher = PassthroughSubject<MessageListView.ElementPublisher.Output, Never>()
lazy var tableView = MessageListView(dataPublisher: publisher.eraseToAnyPublisher())
var inputBoxKeyboardAdapterHeightConstraint = NSLayoutConstraint()
enum ChatContent {
case user(document: String)
case assistant(document: String)
case error(text: String)
}
var simpleChatContents: OrderedDictionary<UUID, ChatContent> = [:] {
didSet { updateContentToPublisher() }
}
var sessionID: String = "" {
didSet { print("[*] new sessionID: \(sessionID)") }
}
public enum MetadataKey: String {
case documentID
case workspaceID
case content
}
public var metadata: [MetadataKey: String] = [:]
var chatTask: EventSource?
override public var title: String? {
@@ -38,19 +59,6 @@ public class IntelligentsChatController: UIViewController {
title = "Chat with AI".localized()
overrideUserInterfaceStyle = .dark
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillDisappear),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillAppear),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
}
@available(*, unavailable)
@@ -59,7 +67,6 @@ public class IntelligentsChatController: UIViewController {
}
deinit {
NotificationCenter.default.removeObserver(self)
chatTask?.stop()
chatTask = nil
}
@@ -73,11 +80,14 @@ public class IntelligentsChatController: UIViewController {
view.addSubview(header)
view.addSubview(tableView)
view.addSubview(inputBoxKeyboardAdapter)
view.addSubview(inputBox)
view.addSubview(progressView)
setupLayout()
// TODO: IMPL
inputBox.editor.controlBanner.cameraButton.isHidden = true
inputBox.editor.controlBanner.photoButton.isHidden = true
chat_onLoad()
}
@@ -96,21 +106,11 @@ public class IntelligentsChatController: UIViewController {
header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44),
].forEach { $0.isActive = true }
inputBoxKeyboardAdapter.translatesAutoresizingMaskIntoConstraints = false
[
inputBoxKeyboardAdapter.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBoxKeyboardAdapter.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBoxKeyboardAdapter.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
].forEach { $0.isActive = true }
inputBoxKeyboardAdapterHeightConstraint = inputBoxKeyboardAdapter.heightAnchor.constraint(equalToConstant: 0)
inputBoxKeyboardAdapterHeightConstraint.isActive = true
inputBoxKeyboardAdapter.backgroundColor = inputBox.backgroundView.backgroundColor
inputBox.translatesAutoresizingMaskIntoConstraints = false
[
inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBox.bottomAnchor.constraint(equalTo: inputBoxKeyboardAdapter.topAnchor),
inputBox.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
].forEach { $0.isActive = true }
tableView.translatesAutoresizingMaskIntoConstraints = false
@@ -118,7 +118,7 @@ public class IntelligentsChatController: UIViewController {
tableView.topAnchor.constraint(equalTo: header.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: inputBox.topAnchor, constant: 16),
tableView.bottomAnchor.constraint(equalTo: inputBox.topAnchor),
].forEach { $0.isActive = true }
inputBox.editor.controlBanner.sendButton.addTarget(
@@ -136,10 +136,4 @@ public class IntelligentsChatController: UIViewController {
].forEach { $0.isActive = true }
progressView.style = .large
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.scrollToBottomEnabled = true
tableView.scrollToBottomAllowed = true
}
}

View File

@@ -0,0 +1,150 @@
//
// MessageListView+AssistantCell.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import MarkdownParser
import MarkdownView
import UIKit
extension MessageListView {
class AssistantCell: BaseCell {
let avatarView = UIImageView()
let usernameView = UILabel()
let markdownView = MarkdownView()
override func initializeContent() {
super.initializeContent()
avatarView.contentMode = .scaleAspectFit
avatarView.image = UIImage(named: "spark", in: .module, with: nil)
usernameView.text = "AFFiNE AI"
usernameView.font = .preferredFont(forTextStyle: .body).bold
usernameView.textColor = .label
containerView.addSubview(avatarView)
containerView.addSubview(usernameView)
containerView.addSubview(markdownView)
}
override func prepareForReuse() {
super.prepareForReuse()
markdownView.prepareForReuse()
}
override func updateContent(
object: any MessageListView.Element.ViewModel,
originalObject _: Element.UserObject?
) {
guard let object = object as? ViewModel else {
assertionFailure()
return
}
_ = object
}
override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) {
super.layoutContent(cache: cache)
guard let cache = cache as? LayoutCache else {
assertionFailure()
return
}
avatarView.frame = cache.avatarRect
usernameView.frame = cache.usernameRect
markdownView.frame = cache.markdownFrame
UIView.performWithoutAnimation {
markdownView.updateContentViews(cache.manifests)
}
}
override class func layoutInsideContainer(
containerWidth: CGFloat,
object: any MessageListView.Element.ViewModel
) -> any MessageListView.TableLayoutEngine.LayoutCache {
guard let object = object as? ViewModel else {
assertionFailure()
return LayoutCache()
}
let cache = LayoutCache()
cache.width = containerWidth
let inset: CGFloat = 8
let bubbleInset = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
let avatarRect = CGRect(x: bubbleInset.left, y: bubbleInset.top, width: 24, height: 24)
let usernameRect = CGRect(
x: avatarRect.maxX + bubbleInset.right,
y: bubbleInset.top,
width: containerWidth - avatarRect.maxX - bubbleInset.right,
height: 24
)
let textWidth = containerWidth - bubbleInset.left - bubbleInset.right
var height: CGFloat = 0
let manifests = object.blocks.map {
let ret = $0.manifest(theme: object.theme)
ret.setLayoutTheme(.default)
ret.setLayoutWidth(textWidth)
ret.layoutIfNeeded()
height += ret.size.height + Theme.default.spacings.final
return ret
}
if height > 0 { height -= Theme.default.spacings.final }
let textRect = CGRect(
x: bubbleInset.left,
y: usernameRect.maxY + bubbleInset.bottom,
width: textWidth,
height: height
)
cache.markdownFrame = textRect
cache.avatarRect = avatarRect
cache.usernameRect = usernameRect
cache.manifests = manifests
cache.height = textRect.maxY + bubbleInset.bottom
return cache
}
}
}
extension MessageListView.AssistantCell {
class ViewModel: MessageListView.Element.ViewModel {
var theme: Theme
var blocks: [BlockNode]
enum GroupLocation {
case begin
case center
case end
}
var groupLocation: GroupLocation = .center
init(theme: Theme = .default, blocks: [BlockNode]) {
self.theme = theme
self.blocks = blocks
}
func contentIdentifier(hasher: inout Hasher) {
hasher.combine(blocks)
}
}
}
extension MessageListView.AssistantCell {
class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache {
var width: CGFloat = 0
var height: CGFloat = 0
var avatarRect: CGRect = .zero
var usernameRect: CGRect = .zero
var markdownFrame: CGRect = .zero
var manifests: [AnyBlockManifest] = []
}
}

View File

@@ -0,0 +1,143 @@
//
// MessageListView+BaseCell.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import UIKit
extension MessageListView {
class BaseCell: UITableViewCell, MessageListView.TableLayoutEngine.LayoutableCell {
var associatedObject: Element? = nil
var cancellable: Set<AnyCancellable> = []
let containerView: UIView = .init()
var layoutEngine: MessageListView.TableLayoutEngine? = nil
func layoutCache() -> MessageListView.TableLayoutEngine.LayoutCache {
guard let associatedObject, let engine = layoutEngine else {
return MessageListView.TableLayoutEngine.ZeroLayoutCache()
}
let cache = engine.requestLayoutCacheFromCell(
forElement: associatedObject,
atWidth: bounds.width
)
return cache
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commitInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commitInit()
}
private func commitInit() {
selectionStyle = .none
separatorInset = .zero
contentView.addSubview(containerView)
contentView.clipsToBounds = false
clipsToBounds = false
initializeContent()
}
override func layoutSubviews() {
super.layoutSubviews()
guard let cache = layoutCache() as? LayoutCache else {
assertionFailure()
return
}
containerView.frame = cache.containerRect
layoutContent(cache: cache.containerLayoutCache)
}
func registerViewModel(element: Element) {
removeViewModelObject()
associatedObject = element
updateContent(object: element.viewModel, originalObject: element.object)
setNeedsLayout()
}
func removeViewModelObject() {
associatedObject = nil
cancellable.forEach { $0.cancel() }
cancellable.removeAll()
}
func initializeContent() {}
func updateContent(object: any Element.ViewModel, originalObject: Element.UserObject?) {
_ = object
_ = originalObject
}
func layoutContent(cache: MessageListView.TableLayoutEngine.LayoutCache) {
_ = cache
}
class func layoutInsideContainer(
containerWidth: CGFloat,
object: any Element.ViewModel
) -> MessageListView.TableLayoutEngine.LayoutCache {
_ = containerWidth
_ = object
assertionFailure("must override")
return MessageListView.TableLayoutEngine.ZeroLayoutCache()
}
class func containerInset() -> UIEdgeInsets {
let inset: CGFloat = 16
let containerInset = UIEdgeInsets(top: inset / 2, left: inset, bottom: inset / 2, right: inset)
return containerInset
}
}
}
extension MessageListView.BaseCell {
class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache {
var width: CGFloat
var height: CGFloat
var containerRect: CGRect
var containerLayoutCache: any MessageListView.TableLayoutEngine.LayoutCache
init(
width: CGFloat,
height: CGFloat,
containerRect: CGRect,
containerLayoutCache: any MessageListView.TableLayoutEngine.LayoutCache
) {
self.width = width
self.height = height
self.containerRect = containerRect
self.containerLayoutCache = containerLayoutCache
}
}
class func resolveLayout(
dataElement element: MessageListView.Element,
contentWidth width: CGFloat
) -> any MessageListView.TableLayoutEngine.LayoutCache {
let object = element.viewModel
let containerInset = MessageListView.BaseCell.containerInset()
let containerWidth = width - containerInset.left - containerInset.right
let containerCache = Self.layoutInsideContainer(containerWidth: containerWidth, object: object)
let cellHeight = containerCache.height + containerInset.top + containerInset.bottom
let containerRect = CGRect(
x: containerInset.left,
y: containerInset.top,
width: containerWidth,
height: containerCache.height
)
let cache = LayoutCache(
width: width,
height: cellHeight,
containerRect: containerRect,
containerLayoutCache: containerCache
)
return cache
}
}

View File

@@ -0,0 +1,92 @@
//
// MessageListView+HintCell.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import UIKit
extension MessageListView {
class HintCell: BaseCell {
let label = UILabel()
override func initializeContent() {
super.initializeContent()
label.font = .preferredFont(forTextStyle: .footnote)
label.alpha = 0.5
label.numberOfLines = 0
containerView.addSubview(label)
}
override func updateContent(
object: any MessageListView.Element.ViewModel,
originalObject: Element.UserObject?
) {
super.updateContent(object: object, originalObject: originalObject)
guard let object = object as? ViewModel else { return }
label.attributedText = object.hint
}
override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) {
super.layoutContent(cache: cache)
guard let cache = cache as? LayoutCache else {
assertionFailure()
return
}
label.frame = cache.labelFrame
}
override class func layoutInsideContainer(
containerWidth: CGFloat,
object: any MessageListView.Element.ViewModel
) -> any MessageListView.TableLayoutEngine.LayoutCache {
guard let object = object as? ViewModel else {
assertionFailure()
return LayoutCache()
}
let cache = LayoutCache()
cache.width = containerWidth
cache.height = object.hint.measureHeight(usingWidth: containerWidth)
cache.labelFrame = .init(x: 0, y: 0, width: containerWidth, height: cache.height)
return cache
}
}
}
extension MessageListView.HintCell {
class ViewModel: MessageListView.Element.ViewModel {
var hint: NSAttributedString = .init()
init(hint: NSAttributedString) {
self.hint = hint
}
convenience init(hint: String) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .footnote),
.originalFont: UIFont.preferredFont(forTextStyle: .footnote),
.foregroundColor: UIColor.label,
.paragraphStyle: paragraphStyle,
]
let text = NSMutableAttributedString(string: hint, attributes: attributes)
self.init(hint: text)
}
func contentIdentifier(hasher: inout Hasher) {
hasher.combine(hint)
}
}
}
extension MessageListView.HintCell {
class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache {
var width: CGFloat = 0
var height: CGFloat = 0
var labelFrame: CGRect = .zero
}
}

View File

@@ -0,0 +1,47 @@
//
// MessageListView+SpacerCell.swift
// FlowDown
//
// Created by on 2025/1/12.
//
import Combine
import UIKit
extension MessageListView {
class SpacerCell: BaseCell {
override class func layoutInsideContainer(
containerWidth: CGFloat,
object: any MessageListView.Element.ViewModel
) -> any MessageListView.TableLayoutEngine.LayoutCache {
guard let object = object as? ViewModel else {
assertionFailure()
return LayoutCache()
}
let cache = LayoutCache()
cache.width = containerWidth
cache.height = object.height
return cache
}
}
}
extension MessageListView.SpacerCell {
class ViewModel: MessageListView.Element.ViewModel {
var height: CGFloat
init(height: CGFloat) {
self.height = height
}
func contentIdentifier(hasher: inout Hasher) {
hasher.combine(height)
}
}
}
extension MessageListView.SpacerCell {
class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache {
var width: CGFloat = 0
var height: CGFloat = 0
}
}

View File

@@ -0,0 +1,167 @@
//
// MessageListView+UserCell.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import UIKit
extension MessageListView {
class UserCell: BaseCell {
let avatarView = UIImageView()
let usernameView = UILabel()
let bubbleView = UIView()
let textView = UITextView()
override func initializeContent() {
super.initializeContent()
textView.isSelectable = true
textView.isScrollEnabled = true
textView.isEditable = false
textView.showsVerticalScrollIndicator = false
textView.showsHorizontalScrollIndicator = false
textView.textColor = .label
textView.textContainer.lineFragmentPadding = .zero
textView.textAlignment = .natural
textView.backgroundColor = .clear
textView.textContainerInset = .zero
textView.textContainer.lineBreakMode = .byTruncatingTail
avatarView.contentMode = .scaleAspectFit
avatarView.image = UIImage(systemName: "person.fill")
usernameView.text = "You"
usernameView.font = .preferredFont(forTextStyle: .body).bold
usernameView.textColor = .label
bubbleView.layer.cornerRadius = 8
bubbleView.backgroundColor = .gray.withAlphaComponent(0.1)
containerView.addSubview(bubbleView)
containerView.addSubview(avatarView)
containerView.addSubview(usernameView)
containerView.addSubview(textView)
}
override func updateContent(
object: any MessageListView.Element.ViewModel,
originalObject: Element.UserObject?
) {
super.updateContent(object: object, originalObject: originalObject)
guard let object = object as? ViewModel else {
assertionFailure()
return
}
textView.attributedText = object.text
}
override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) {
super.layoutContent(cache: cache)
guard let cache = cache as? LayoutCache else {
assertionFailure()
return
}
bubbleView.frame = cache.bubbleFrame
avatarView.frame = cache.avatarFrame
usernameView.frame = cache.usernameFrame
textView.frame = cache.labelFrame
}
override class func layoutInsideContainer(
containerWidth: CGFloat,
object: any MessageListView.Element.ViewModel
) -> any MessageListView.TableLayoutEngine.LayoutCache {
guard let object = object as? ViewModel else {
assertionFailure()
return LayoutCache()
}
let cache = LayoutCache()
cache.width = containerWidth
let inset: CGFloat = 8
let bubbleInset = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
let avatarRect = CGRect(
x: bubbleInset.left,
y: bubbleInset.top,
width: 24,
height: 24
)
let usernameFrame = CGRect(
x: avatarRect.maxX + inset,
y: bubbleInset.top,
width: containerWidth - avatarRect.maxX - bubbleInset.right,
height: 24
)
let textWidth = min(
object.text.measureWidth(),
containerWidth - inset * 2
)
let textHeight = object.text.measureHeight(usingWidth: textWidth)
let textRect = CGRect(
x: bubbleInset.left,
y: avatarRect.maxY + bubbleInset.top,
width: textWidth,
height: textHeight
)
let bubbleRect = CGRect(
x: 0,
y: 0,
width: containerWidth,
height: textRect.maxY + bubbleInset.bottom
)
cache.bubbleFrame = bubbleRect
cache.avatarFrame = avatarRect
cache.usernameFrame = usernameFrame
cache.labelFrame = textRect
cache.height = bubbleRect.maxY
return cache
}
}
}
extension MessageListView.UserCell {
class ViewModel: MessageListView.Element.ViewModel {
var text: NSAttributedString = .init()
init(text: NSAttributedString) {
self.text = text
}
convenience init(text: String) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .body),
.originalFont: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: UIColor.label,
.paragraphStyle: paragraphStyle,
]
var text = text
while text.contains("\n\n\n") {
text = text.replacingOccurrences(of: "\n\n\n", with: "\n\n")
}
print(text)
self.init(text: NSMutableAttributedString(string: text, attributes: attributes))
}
func contentIdentifier(hasher: inout Hasher) {
hasher.combine(text)
}
}
}
extension MessageListView.UserCell {
class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache {
var width: CGFloat = 0
var height: CGFloat = 0
var bubbleFrame: CGRect = .zero
var labelFrame: CGRect = .zero
var avatarFrame: CGRect = .zero
var usernameFrame: CGRect = .zero
}
}

View File

@@ -0,0 +1,61 @@
//
// MessageListView+DataElement.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import Foundation
import UIKit
extension MessageListView {
struct Element: Identifiable {
let id: AnyHashable // equals to message id if applicable
enum Cell: String, CaseIterable {
case base
case hint
case user
case assistant
case spacer
}
let cell: Cell
let viewModel: any ViewModel
typealias UserObject = any(Identifiable & Hashable)
let object: UserObject?
init(id: AnyHashable, cell: Cell, viewModel: any ViewModel, object: UserObject?) {
assert(cell != .base)
self.id = id
self.cell = cell
self.viewModel = viewModel
self.object = object
}
}
}
extension MessageListView.Element.Cell {
var cellClass: MessageListView.BaseCell.Type {
switch self {
case .base:
MessageListView.BaseCell.self
case .hint:
MessageListView.HintCell.self
case .user:
MessageListView.UserCell.self
case .assistant:
MessageListView.AssistantCell.self
case .spacer:
MessageListView.SpacerCell.self
}
}
}
extension MessageListView.Element {
protocol ViewModel {
func contentIdentifier(hasher: inout Hasher)
}
}

View File

@@ -0,0 +1,68 @@
//
// MessageListView+Delegate.swift
// FlowDown
//
// Created by on 2025/1/6.
//
import UIKit
extension MessageListView: UITableViewDelegate, UITableViewDataSource {
func item(forIndexPath indexPath: IndexPath) -> Element? {
guard indexPath.row < elements.count else {
return nil
}
guard indexPath.row >= 0 else {
return nil
}
return elements.values[indexPath.row]
}
func numberOfSections(in _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let item = item(forIndexPath: indexPath) else {
assertionFailure()
return UITableViewCell()
}
let cell = tableView.dequeueReusableCell(withIdentifier: item.cell.rawValue, for: indexPath)
if let cell = cell as? BaseCell {
cell.layoutEngine = layoutEngine
cell.registerViewModel(element: item)
}
cell.backgroundColor = .clear
return cell
}
func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let item = item(forIndexPath: indexPath) else {
return 0
}
if let height = layoutEngine.height(forElement: item) {
heightKeeper[item.id] = height
return height
}
let ret = layoutEngine.resolveLayoutNow(item).height
heightKeeper[item.id] = ret
return ret
}
func tableView(_: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let item = item(forIndexPath: indexPath) else {
return 0
}
if let height = layoutEngine.height(forElement: item) {
return height
}
if let height = heightKeeper[item.id] {
return height
}
return UITableView.automaticDimension
}
}

View File

@@ -0,0 +1,143 @@
//
// MessageListView+LayoutEngine.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Foundation
extension MessageListView {
class TableLayoutEngine {
private let lock = NSLock()
struct LayoutCacheBox {
var cache: LayoutCache
var contentIdentifier: Int
}
private var layoutCache: [Element.ID: LayoutCacheBox] = [:]
private(set) var contentWidth: CGFloat = .zero
var layoutSession: UUID = .init()
func setContentWidth(_ width: CGFloat) {
accessLayoutCache { _ in
contentWidth = width
layoutSession = .init()
}
}
func createSession() -> UUID {
let session = UUID()
layoutSession = session
return session
}
@discardableResult
func accessLayoutCache<T>(_ block: (inout [Element.ID: LayoutCacheBox]) -> T) -> T {
lock.lock()
defer { lock.unlock() }
return block(&layoutCache)
}
func contentIdentifier(forElement dataElement: Element) -> Int? {
accessLayoutCache { pool in
guard let box = pool[dataElement.id] else { return nil }
return box.contentIdentifier
}
}
}
func viewCallingUpdateLayoutEngineWidth() {
guard layoutEngine.contentWidth != tableView.bounds.width else { return }
layoutEngine.setContentWidth(tableView.bounds.width)
reconfigure(enforceReload: false)
NSObject.cancelPreviousPerformRequests(
withTarget: self,
selector: #selector(resolveAllLayoutInBackground),
object: nil
)
perform(#selector(resolveAllLayoutInBackground), with: nil, afterDelay: 0.1)
}
@objc private func resolveAllLayoutInBackground() {
let items = Array(elements.values)
let date = Date()
DispatchQueue.global().async {
let session = self.layoutEngine.createSession()
for element in items {
guard self.layoutEngine.layoutSession == session else { continue }
self.layoutEngine.resolveLayoutNow(element)
}
DispatchQueue.main.async {
self.reconfigure(enforceReload: true)
print("[*] layout engine updated \(items.count) items in \(Date().timeIntervalSince(date)) seconds")
}
}
}
}
extension MessageListView.TableLayoutEngine {
protocol LayoutableCell: AnyObject {
static func resolveLayout(
dataElement: MessageListView.Element,
contentWidth: CGFloat
) -> LayoutCache
}
protocol LayoutCache: AnyObject {
var width: CGFloat { get }
var height: CGFloat { get }
}
class ZeroLayoutCache: LayoutCache {
var width: CGFloat = 0
var height: CGFloat = 0
}
}
extension MessageListView.TableLayoutEngine {
@discardableResult
func resolveLayoutNow(_ element: MessageListView.Element) -> LayoutCache {
var hasher = Hasher()
element.viewModel.contentIdentifier(hasher: &hasher)
let contentIdentifier = hasher.finalize()
if let cacheBox = accessLayoutCache({ $0[element.id] }) {
if cacheBox.cache.width == contentWidth,
cacheBox.contentIdentifier == contentIdentifier
{ return cacheBox.cache }
}
let target = element.cell.cellClass.self
let cache = target.resolveLayout(dataElement: element, contentWidth: contentWidth)
let cacheBox = LayoutCacheBox(cache: cache, contentIdentifier: contentIdentifier)
accessLayoutCache { $0[element.id] = cacheBox }
return cache
}
func requestLayoutCacheFromCell(
forElement dataElement: MessageListView.Element,
atWidth width: CGFloat
) -> LayoutCache {
let cache = accessLayoutCache { pool -> LayoutCache? in
guard let box = pool[dataElement.id] else { return nil }
guard box.contentIdentifier == dataElement.object?.hashValue else { return nil }
guard box.cache.width == contentWidth else { return nil }
guard box.cache.width == width else { return nil }
return box.cache
}
if let cache { return cache }
return resolveLayoutNow(dataElement)
}
}
extension MessageListView.TableLayoutEngine {
func height(forElement dataElement: MessageListView.Element) -> CGFloat? {
accessLayoutCache { pool in
guard let box = pool[dataElement.id] else { return nil }
guard box.contentIdentifier == dataElement.object?.hashValue else { return nil }
guard box.cache.width == contentWidth else { return nil }
return box.cache.height
}
}
}

View File

@@ -0,0 +1,143 @@
//
// MessageListView+Update.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import Foundation
import OrderedCollections
import UIKit
extension MessageListView {
func setupPublishers(dataPublisher: AnyPublisher<[Element], Never>) {
// process input from data source where we transform those to view model
let publisher = dataPublisher
.map { input -> [Element] in input + [Element(
id: "spacer",
cell: .spacer,
viewModel: MessageListView.SpacerCell.ViewModel(height: 32),
object: nil
)] }
.map { output in
OrderedDictionary<Element.ID, Element>(
uniqueKeysWithValues: output.map { ($0.id, $0) }
)
}
.eraseToAnyPublisher()
// after so, limit the refresh rate so we can handle them better
let updateQueue = DispatchQueue(label: "flowdown.message-list-update-queue", qos: .userInteractive)
let inQueuePublisher = publisher
.throttle(for: .seconds(1 / 5), scheduler: updateQueue, latest: true)
.eraseToAnyPublisher()
// finally before sending to display, call layout engine to process those items
inQueuePublisher
.sink { [weak self] output in self?.prepare(forNewElements: output) }
.store(in: &cancellables)
}
func prepare(forNewElements elements: Elements) {
print("[*] received \(elements.count) for update at \(Date())")
elementUpdateProcessLock.lock()
distributedPendingUpdateElements = elements
elementUpdateProcessLock.unlock()
performSelector(onMainThread: #selector(elementsUpdateExecute), with: nil, waitUntilDone: false)
}
private func pickupElementsPair() -> (oldValue: Elements, newValue: Elements)? {
#if DEBUG // just make sure assert is not called in release mode
assert(!elementUpdateProcessLock.try(), "Should not call this method without lock")
#endif
guard let distributedPendingUpdateElements else { return nil }
let oldValue = elements
elements = distributedPendingUpdateElements
self.distributedPendingUpdateElements = nil
print("[*] pikup is sending \(elements.count) for update at \(Date())")
return (oldValue, distributedPendingUpdateElements)
}
@objc private func elementsUpdateExecute() {
assert(Thread.isMainThread)
elementUpdateProcessLock.lock()
defer { elementUpdateProcessLock.unlock() }
let pickup = pickupElementsPair()
guard let (oldValue, newValue) = pickup else { return }
guard window != nil else { return }
for value in heightKeeper.keys where !newValue.keys.contains(value) {
heightKeeper.removeValue(forKey: value)
}
let shouldRealodTableView = newValue.count != oldValue.count
let contentOffset = tableView.contentOffset
UIView.performWithoutAnimation {
self.reconfigure(enforceReload: shouldRealodTableView)
self.tableView.layoutIfNeeded()
}
tableView.contentOffset = contentOffset
}
func reconfigure(enforceReload: Bool) {
if enforceReload || tableView(tableView, numberOfRowsInSection: 0) != elements.count {
tableView.reloadData()
return
}
var requiresReload = [IndexPath]()
for indexPath in tableView.indexPathsForVisibleRows ?? [] {
guard let item = item(forIndexPath: indexPath) else { continue }
guard let cell = tableView.cellForRow(at: indexPath) as? BaseCell else { continue }
guard type(of: cell) == item.cell.cellClass else {
requiresReload.append(indexPath)
continue
}
layoutEngine.resolveLayoutNow(item)
cell.registerViewModel(element: item)
}
tableView.beginUpdates()
tableView.reloadRows(at: requiresReload, with: .none)
tableView.endUpdates()
}
}
extension MessageListView {
func scrollToBottom(useTableViewAnimation: Bool = false) {
guard elements.count > 0 else { return }
guard tableView.contentSize.height > tableView.frame.height else { return }
let targetIndexPath = IndexPath(row: elements.count - 1, section: 0)
let cellRect = tableView.rectForRow(at: targetIndexPath)
if tableView.contentOffset.y + tableView.frame.height >= cellRect.origin.y + cellRect.height { return }
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) {
self.tableView.scrollToRow(
at: targetIndexPath,
at: .bottom,
animated: useTableViewAnimation
)
self.tableView.layoutIfNeeded()
}
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil)
isAutomaticScrollAnimating = true
perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5)
}
func scrollLastCellToTop(useTableViewAnimation: Bool = false) {
guard elements.count > 1 else { return }
guard tableView.contentSize.height > tableView.frame.height else { return }
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) {
self.tableView.scrollToRow(
at: IndexPath(row: self.elements.count - 1, section: 0),
at: .top,
animated: useTableViewAnimation
)
}
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil)
isAutomaticScrollAnimating = true
perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5)
}
@objc private func finishAutomaticScroll() {
isAutomaticScrollAnimating = false
}
}

View File

@@ -0,0 +1,71 @@
//
// MessageListView.swift
// FlowDown
//
// Created by on 2025/1/2.
//
import Combine
import OrderedCollections
import UIKit
class MessageListView: UIView {
typealias ElementPublisher = AnyPublisher<[Element], Never>
typealias Elements = OrderedDictionary<Element.ID, Element>
var elements: Elements = .init()
var cancellables: Set<AnyCancellable> = []
let tableView: UITableView = .init(frame: .zero, style: .plain)
let layoutEngine = TableLayoutEngine()
var heightKeeper: [Element.ID: CGFloat] = [:]
let elementUpdateProcessLock = NSLock()
var distributedPendingUpdateElements: Elements? = nil
var isAutomaticScrollAnimating: Bool = false
let footerView = UIView(frame: .init(x: 0, y: 0, width: 0, height: 500))
init(dataPublisher: AnyPublisher<[Element], Never>) {
super.init(frame: .zero)
tableView.delegate = self
tableView.dataSource = self
tableView.allowsSelection = false
tableView.allowsMultipleSelection = false
tableView.allowsFocus = false
tableView.selectionFollowsFocus = true
tableView.separatorColor = .clear
tableView.backgroundColor = .clear
for cellIdentifier in Element.Cell.allCases {
tableView.register(cellIdentifier.cellClass, forCellReuseIdentifier: cellIdentifier.rawValue)
}
addSubview(tableView)
tableView.tableFooterView = footerView
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: topAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
setupPublishers(dataPublisher: dataPublisher)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
override func layoutSubviews() {
super.layoutSubviews()
viewCallingUpdateLayoutEngineWidth()
}
}

View File

@@ -0,0 +1,48 @@
//
// EphemeralAction.swift
// Intelligents
//
// Created by on 2025/1/8.
//
import Foundation
public extension IntelligentsEphemeralActionController {
enum EphemeralAction {
public enum Language: String, CaseIterable {
case langEnglish = "English"
case langSpanish = "Spanish"
case langGerman = "German"
case langFrench = "French"
case langItalian = "Italian"
case langSimplifiedChinese = "Simplified Chinese"
case langTraditionalChinese = "Traditional Chinese"
case langJapanese = "Japanese"
case langRussian = "Russian"
case langKorean = "Korean"
}
case translate(to: Language)
case summarize
}
}
extension IntelligentsEphemeralActionController.EphemeralAction {
var title: String {
switch self {
case let .translate(to):
String(format: NSLocalizedString("Translate to %@", comment: ""), to.rawValue)
case .summarize:
NSLocalizedString("Summarize", comment: "")
}
}
var prompt: Prompt {
switch self {
case .translate:
.general_Translate_to
case .summarize:
.general_Summary
}
}
}

View File

@@ -0,0 +1,60 @@
//
// ImageRotatedPreview.swift
// Intelligents
//
// Created by on 2025/1/8.
//
import UIKit
public class RotatedImagePreview: UIView {
let imageView = UIImageView()
let rotationDegree: CGFloat = 5
public init() {
super.init(frame: .zero)
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 16
imageView.clipsToBounds = true
addSubview(imageView)
clipsToBounds = false
heightAnchor.constraint(equalToConstant: 300).isActive = true
imageView.transform = CGAffineTransform(rotationAngle: rotationDegree * CGFloat.pi / 180)
}
@available(*, unavailable)
public required init?(coder _: NSCoder) {
fatalError()
}
public func configure(previewImage: UIImage) {
imageView.image = previewImage
setNeedsLayout()
}
override public func layoutSubviews() {
super.layoutSubviews()
guard let image = imageView.image else {
imageView.frame = .zero
return
}
let viewHeight = bounds.height // limiter
guard bounds.height > 0 else { return }
// fit in side
let imageAspectRatio = image.size.width / image.size.height
let imageHeight = viewHeight
let imageWidth = imageHeight * imageAspectRatio
imageView.frame = CGRect(
x: (bounds.width - imageWidth) / 2,
y: (bounds.height - imageHeight) / 2,
width: imageWidth,
height: imageHeight
)
}
}

View File

@@ -0,0 +1,145 @@
//
// IntelligentsEphemeralActionController+API.swift
// Intelligents
//
// Created by on 2025/1/15.
//
import AffineGraphQL
import Foundation
import LDSwiftEventSource
extension IntelligentsEphemeralActionController {
func beginAction() {
print("[*] begin ephemeral action for did \(documentID) wid \(workspaceID)")
chatTask?.stop()
chatTask = nil
copilotDocumentStorage = ""
chat_createSession(
documentIdentifier: documentID,
workspaceIdentifier: workspaceID
) { session in
self.sessionID = session
self.beginThisRound()
} onFailure: { error in
self.presentError(error) {
self.close()
}
}
}
func chat_createSession(
documentIdentifier: String,
workspaceIdentifier: String,
onSuccess: @escaping (String) -> Void,
onFailure: @escaping (Error) -> Void
) {
if documentIdentifier.isEmpty || workspaceIdentifier.isEmpty {
onFailure(
NSError(
domain: "Intelligents",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "Unable to identify the document or workspace"]
)
)
}
Intelligents.qlClient.perform(
mutation: CreateCopilotSessionMutation(options: .init(
docId: documentIdentifier,
promptName: ation.prompt.rawValue,
workspaceId: workspaceIdentifier
)),
queue: .global()
) { result in
switch result {
case let .success(value):
if let session = value.data?.createCopilotSession, !session.isEmpty {
DispatchQueue.main.async { onSuccess(session) }
} else {
DispatchQueue.main.async {
onFailure(
NSError(
domain: "Intelligents",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "No session created"]
)
)
}
}
case let .failure(error):
DispatchQueue.main.async { onFailure(error) }
}
}
}
func beginThisRound() {
Intelligents.qlClient.perform(
mutation: CreateCopilotMessageMutation(options: .init(
content: .init(stringLiteral: "\(documentContent)"),
sessionId: sessionID
)),
queue: .global()
) { result in
switch result {
case let .success(value):
if let messageID = value.data?.createCopilotMessage {
print("[*] messageID", messageID)
self.chat_processWithMessageID(sessionID: self.sessionID, messageID: messageID)
}
case let .failure(error):
self.presentError(error) {
self.close()
}
}
}
}
func chat_processWithMessageID(sessionID: String, messageID: String) {
let url = Constant.affineUpstreamURL
.appendingPathComponent("api")
.appendingPathComponent("copilot")
.appendingPathComponent("chat")
.appendingPathComponent(sessionID)
.appendingPathComponent("stream")
var comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
comps?.queryItems = [URLQueryItem(name: "messageId", value: messageID)]
guard let url = comps?.url else {
assertionFailure()
presentError(NSError(
domain: "Intelligents",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "No message created"]
))
return
}
let eventHandler = BlockEventHandler()
eventHandler.onOpenedBlock = {
print("[*] chat opened")
}
eventHandler.onErrorBlock = { error in
self.presentError(error) { self.close() }
}
eventHandler.onMessageBlock = { _, message in
self.chat_onEvent(message.data)
}
eventHandler.onClosedBlock = {
self.chatTask?.stop()
self.chatTask = nil
}
let eventSource = EventSource(config: .init(handler: eventHandler, url: url))
eventSource.start()
chatTask = eventSource
}
func chat_onEvent(_ data: String) {
if Thread.isMainThread {
copilotDocumentStorage += data
} else {
DispatchQueue.main.asyncAndWait {
self.copilotDocumentStorage += data
}
}
}
}

View File

@@ -0,0 +1,80 @@
//
// IntelligentsEphemeralActionController+ActionBar.swift
// Intelligents
//
// Created by on 2025/1/15.
//
import UIKit
extension IntelligentsEphemeralActionController {
class ActionBar: UIView {
let retryButton = DarkActionButton()
let continueToChat = DarkActionButton()
let createNewDoc = DarkActionButton()
init() {
super.init(frame: .zero)
defer { removeEveryAutoResizingMasks() }
let contentSpacing: CGFloat = 16
let buttonGroupHeight: CGFloat = 55
let firstButtonSectionGroup = UIView()
addSubview(firstButtonSectionGroup)
[
firstButtonSectionGroup.topAnchor.constraint(equalTo: topAnchor, constant: contentSpacing),
firstButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor),
firstButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor),
firstButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight),
].forEach { $0.isActive = true }
retryButton.title = NSLocalizedString("Retry", comment: "")
retryButton.iconSystemName = "arrow.clockwise"
continueToChat.title = NSLocalizedString("Continue to Chat", comment: "")
continueToChat.iconSystemName = "paperplane"
firstButtonSectionGroup.addSubview(retryButton)
firstButtonSectionGroup.addSubview(continueToChat)
[
retryButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor),
retryButton.leadingAnchor.constraint(equalTo: firstButtonSectionGroup.leadingAnchor),
retryButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor),
continueToChat.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor),
continueToChat.trailingAnchor.constraint(equalTo: firstButtonSectionGroup.trailingAnchor),
continueToChat.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor),
retryButton.widthAnchor.constraint(equalTo: continueToChat.widthAnchor),
retryButton.trailingAnchor.constraint(equalTo: continueToChat.leadingAnchor, constant: -contentSpacing),
].forEach { $0.isActive = true }
let secondButtonSectionGroup = UIView()
addSubview(secondButtonSectionGroup)
[
secondButtonSectionGroup.topAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor, constant: contentSpacing),
secondButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor),
secondButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor),
secondButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight),
].forEach { $0.isActive = true }
secondButtonSectionGroup.addSubview(createNewDoc)
createNewDoc.title = NSLocalizedString("Create New Doc", comment: "")
createNewDoc.iconSystemName = "doc.badge.plus"
[
createNewDoc.topAnchor.constraint(equalTo: secondButtonSectionGroup.topAnchor),
createNewDoc.leadingAnchor.constraint(equalTo: secondButtonSectionGroup.leadingAnchor),
createNewDoc.bottomAnchor.constraint(equalTo: secondButtonSectionGroup.bottomAnchor),
createNewDoc.trailingAnchor.constraint(equalTo: secondButtonSectionGroup.trailingAnchor),
].forEach { $0.isActive = true }
[
secondButtonSectionGroup.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}

View File

@@ -0,0 +1,115 @@
//
// IntelligentsEphemeralActionController+Header.swift
// Intelligents
//
// Created by on 2025/1/8.
//
//
// IntelligentsChatController+Header.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension IntelligentsEphemeralActionController {
class Header: UIView {
static let height: CGFloat = 44
let contentView = UIView()
let titleLabel = UILabel()
let dropMenu = UIButton()
let backButton = UIButton()
let rightBarItemsStack = UIStackView()
let moreMenu = UIButton()
init() {
super.init(frame: .zero)
setupLayout()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func navigateActionBack() {
parentViewController?.dismissInContext()
}
}
}
private extension IntelligentsEphemeralActionController.Header {
func setupLayout() {
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
[
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.heightAnchor.constraint(equalToConstant: Self.height),
].forEach { $0.isActive = true }
titleLabel.textColor = .label
titleLabel.font = .systemFont(
ofSize: UIFont.labelFontSize,
weight: .semibold
)
backButton.setImage(
UIImage(systemName: "chevron.left"),
for: .normal
)
backButton.tintColor = .accent
backButton.addTarget(self, action: #selector(navigateActionBack), for: .touchUpInside)
dropMenu.setImage(
.init(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate),
for: .normal
)
dropMenu.tintColor = .gray.withAlphaComponent(0.5)
contentView.addSubview(titleLabel)
contentView.addSubview(backButton)
contentView.addSubview(dropMenu)
contentView.addSubview(rightBarItemsStack)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.translatesAutoresizingMaskIntoConstraints = false
dropMenu.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.axis = .horizontal
rightBarItemsStack.spacing = 10
rightBarItemsStack.alignment = .center
rightBarItemsStack.distribution = .equalSpacing
[
backButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
backButton.widthAnchor.constraint(equalToConstant: 44),
backButton.heightAnchor.constraint(equalToConstant: 44),
rightBarItemsStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
rightBarItemsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
rightBarItemsStack.heightAnchor.constraint(equalToConstant: 44),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: backButton.trailingAnchor, constant: 10),
dropMenu.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
dropMenu.widthAnchor.constraint(equalToConstant: 44),
dropMenu.heightAnchor.constraint(equalToConstant: 44),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: dropMenu.leadingAnchor, constant: -10),
].forEach { $0.isActive = true }
rightBarItemsStack.addArrangedSubview(moreMenu)
moreMenu.setImage(
.init(systemName: "ellipsis.circle"),
for: .normal
)
moreMenu.tintColor = .accent
}
}

View File

@@ -0,0 +1,278 @@
//
// IntelligentsEphemeralActionController.swift
// Intelligents
//
// Created by on 2025/1/8.
//
import LDSwiftEventSource
import MarkdownParser
import MarkdownView
import UIKit
public class IntelligentsEphemeralActionController: UIViewController {
let ation: EphemeralAction
let scrollView = UIScrollView()
let stackView = UIStackView()
let header = Header()
let preview = RotatedImagePreview()
let markdownView = MarkdownView()
let indicator = UIActivityIndicatorView(style: .large)
var responseContainer: UIView = .init()
var responseHeightAnchor: NSLayoutConstraint?
let actionBar = ActionBar()
public var documentID: String = ""
public var workspaceID: String = ""
public var documentContent: String = ""
var sessionID: String = ""
var chatTask: EventSource?
var copilotDocumentStorage: String = "" {
didSet {
guard copilotDocumentStorage != oldValue else { return }
updateDocumentPresentationView()
scrollToBottom()
}
}
public init(action: EphemeralAction) {
ation = action
super.init(nibName: nil, bundle: nil)
title = action.title
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override public func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .dark
hideKeyboardWhenTappedAround()
view.backgroundColor = .systemBackground
header.titleLabel.text = title
header.dropMenu.isHidden = true
header.moreMenu.isHidden = true
view.addSubview(header)
header.translatesAutoresizingMaskIntoConstraints = false
[
header.topAnchor.constraint(equalTo: view.topAnchor),
header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor),
header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44),
].forEach { $0.isActive = true }
view.addSubview(actionBar)
actionBar.translatesAutoresizingMaskIntoConstraints = false
[
actionBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
actionBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
actionBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
].forEach { $0.isActive = true }
scrollView.clipsToBounds = true
scrollView.alwaysBounceVertical = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
[
scrollView.topAnchor.constraint(equalTo: header.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: actionBar.topAnchor),
].forEach { $0.isActive = true }
let contentView = UIView()
scrollView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
[
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.heightAnchor),
].forEach { $0.isActive = true }
contentView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 16
stackView.alignment = .fill
stackView.distribution = .fill
contentView.addSubview(stackView)
let stackViewInset: CGFloat = 8
[
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: stackViewInset),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: stackViewInset),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -stackViewInset),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -stackViewInset),
].forEach { $0.isActive = true }
setupContentViews()
actionBar.retryButton.action = { [weak self] in
self?.beginAction()
}
}
func setupContentViews() {
defer { stackView.addArrangedSubview(UIView()) }
preview.layer.cornerRadius = 16
preview.clipsToBounds = true
preview.contentMode = .scaleAspectFill
preview.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(preview)
let headerGroup = UIView()
headerGroup.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(headerGroup)
let headerLabel = UILabel()
let headerIcon = UIImageView()
headerLabel.translatesAutoresizingMaskIntoConstraints = false
headerLabel.text = NSLocalizedString("AFFiNE AI", comment: "")
headerLabel.font = .preferredFont(for: .title3, weight: .bold)
headerLabel.textColor = .white
headerLabel.textAlignment = .left
headerIcon.translatesAutoresizingMaskIntoConstraints = false
headerIcon.image = .init(named: "spark", in: .module, with: nil)
headerIcon.contentMode = .scaleAspectFit
headerIcon.tintColor = .accent
headerGroup.addSubview(headerLabel)
headerGroup.addSubview(headerIcon)
[
headerIcon.leadingAnchor.constraint(equalTo: headerGroup.leadingAnchor),
headerIcon.centerYAnchor.constraint(equalTo: headerGroup.centerYAnchor),
headerIcon.widthAnchor.constraint(equalToConstant: 32),
headerLabel.leadingAnchor.constraint(equalTo: headerIcon.trailingAnchor, constant: 16),
headerLabel.topAnchor.constraint(equalTo: headerGroup.topAnchor),
headerLabel.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor),
].forEach { $0.isActive = true }
responseContainer.translatesAutoresizingMaskIntoConstraints = false
responseContainer.setContentHuggingPriority(.required, for: .vertical)
responseContainer.setContentCompressionResistancePriority(.required, for: .vertical)
responseContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 350).isActive = true
stackView.addArrangedSubview(responseContainer)
responseContainer.addSubview(markdownView)
markdownView.translatesAutoresizingMaskIntoConstraints = false
[
markdownView.topAnchor.constraint(equalTo: responseContainer.topAnchor),
markdownView.leadingAnchor.constraint(equalTo: responseContainer.leadingAnchor),
markdownView.trailingAnchor.constraint(equalTo: responseContainer.trailingAnchor),
markdownView.bottomAnchor.constraint(equalTo: responseContainer.bottomAnchor),
].forEach {
$0.isActive = true
}
indicator.startAnimating()
indicator.translatesAutoresizingMaskIntoConstraints = false
responseContainer.addSubview(indicator)
[
indicator.centerXAnchor.constraint(equalTo: responseContainer.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: responseContainer.centerYAnchor),
indicator.heightAnchor.constraint(equalToConstant: 200),
].forEach {
$0.isActive = true
}
updateDocumentPresentationView()
}
public func configure(previewImage: UIImage) {
preview.configure(previewImage: previewImage)
}
private var isFirstAppear: Bool = true
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard isFirstAppear else { return }
isFirstAppear = false
onFirstAppear()
}
func onFirstAppear() {
beginAction()
}
func close() {
if let navigationController {
navigationController.popViewController(animated: true)
} else {
dismiss(animated: true)
}
}
private var previousLayoutWidth: CGFloat = 0
override public func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if previousLayoutWidth != view.bounds.width {
previousLayoutWidth = view.bounds.width
updateDocumentPresentationView()
}
}
func updateDocumentPresentationView() {
assert(Thread.isMainThread)
responseHeightAnchor?.isActive = false
responseHeightAnchor = nil
if copilotDocumentStorage.isEmpty {
indicator.isHidden = false
indicator.startAnimating()
responseHeightAnchor = responseContainer.heightAnchor.constraint(equalToConstant: 200)
responseHeightAnchor?.isActive = true
markdownView.updateContentViews([])
return
}
indicator.isHidden = true
indicator.stopAnimating()
let document = MarkdownParser().feed(copilotDocumentStorage)
var height: CGFloat = 0
let manifests = document.map {
let ret = $0.manifest(theme: .default)
ret.setLayoutWidth(responseContainer.bounds.width)
ret.layoutIfNeeded()
height += ret.size.height
height += Theme.default.spacings.final
return ret
}
markdownView.updateContentViews(manifests)
if height > 0 { height -= Theme.default.spacings.final }
responseHeightAnchor = responseContainer.heightAnchor.constraint(equalToConstant: height)
responseHeightAnchor?.isActive = true
}
func scrollToBottom() {
guard !copilotDocumentStorage.isEmpty else { return }
let bottomOffset = CGPoint(
x: 0,
y: max(0, scrollView.contentSize.height - scrollView.bounds.size.height)
)
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) { self.scrollView.setContentOffset(bottomOffset, animated: false) }
}
}

View File

@@ -1,76 +0,0 @@
//
// IntelligentsFocusApertureView+ActionButton.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView.ControlButtonsPanel {
class DarkActionButton: UIView {
var iconSystemName: String {
set { iconView.image = UIImage(systemName: newValue) }
get { fatalError() }
}
var title: String {
set { titleLabel.text = newValue }
get { titleLabel.text ?? "" }
}
let titleLabel = UILabel()
let iconView = UIImageView()
var action: (() -> Void)? = nil
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white.withAlphaComponent(0.25)
layer.cornerRadius = 12
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
titleLabel.textAlignment = .center
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .semibold)
titleLabel.textColor = .white
addSubview(titleLabel)
iconView.contentMode = .scaleAspectFit
iconView.tintColor = .white
addSubview(iconView)
[
layoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
layoutGuide.centerYAnchor.constraint(equalTo: centerYAnchor),
iconView.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
iconView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
iconView.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
titleLabel.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
].forEach { $0.isActive = true }
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(onTapped)
))
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func onTapped() {
action?()
}
}
}

View File

@@ -9,12 +9,20 @@ import UIKit
extension IntelligentsFocusApertureView {
func captureImageBuffer(_ targetContentView: UIView) {
let imageSize = targetContentView.frame.size
let renderer = UIGraphicsImageRenderer(size: imageSize)
let contentSize = targetContentView.frame.size
let renderer = UIGraphicsImageRenderer(size: contentSize)
let image = renderer.image { _ in
let drawRect = CGRect(
x: 0,
y: 0,
width: contentSize.width,
height: contentSize.height
)
targetContentView.drawHierarchy(
in: targetContentView.bounds,
afterScreenUpdates: false
in: drawRect,
afterScreenUpdates: true
)
}
capturedImage = image

View File

@@ -15,5 +15,8 @@ public enum IntelligentsFocusApertureViewActionType: String {
}
public protocol IntelligentsFocusApertureViewDelegate: AnyObject {
func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType)
func focusApertureRequestAction(
from: IntelligentsFocusApertureView,
actionType: IntelligentsFocusApertureViewActionType
)
}

View File

@@ -38,10 +38,10 @@ extension IntelligentsFocusApertureView {
}
contentBeginConstraints = [
snapshotView.leftAnchor.constraint(equalTo: targetView.leftAnchor),
snapshotView.rightAnchor.constraint(equalTo: targetView.rightAnchor),
snapshotView.topAnchor.constraint(equalTo: targetView.topAnchor),
snapshotView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor),
snapshotImageView.leftAnchor.constraint(equalTo: targetView.leftAnchor),
snapshotImageView.rightAnchor.constraint(equalTo: targetView.rightAnchor),
snapshotImageView.topAnchor.constraint(equalTo: targetView.topAnchor),
snapshotImageView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor),
controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor),
controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor),
@@ -50,10 +50,10 @@ extension IntelligentsFocusApertureView {
let sharedInset: CGFloat = 32
contentFinalConstraints = [
snapshotView.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
snapshotView.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
snapshotView.topAnchor.constraint(equalTo: topAnchor),
snapshotView.bottomAnchor.constraint(equalTo: controlButtonsPanel.topAnchor, constant: -sharedInset / 2),
snapshotImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
snapshotImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
snapshotImageView.topAnchor.constraint(equalTo: topAnchor),
snapshotImageView.bottomAnchor.constraint(equalTo: controlButtonsPanel.topAnchor, constant: -sharedInset / 2),
controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
@@ -73,12 +73,12 @@ extension IntelligentsFocusApertureView {
NSLayoutConstraint.deactivate(contentFinalConstraints)
NSLayoutConstraint.activate(contentBeginConstraints)
snapshotView.layer.cornerRadius = 0
snapshotImageView.layer.cornerRadius = 0
case .complete:
NSLayoutConstraint.deactivate(contentBeginConstraints)
NSLayoutConstraint.activate(contentFinalConstraints)
snapshotView.layer.cornerRadius = 32
snapshotImageView.layer.cornerRadius = 32
}
let effectiveView = superview ?? self
effectiveView.setNeedsUpdateConstraints()

View File

@@ -8,8 +8,8 @@
import UIKit
public class IntelligentsFocusApertureView: UIView {
let backgroundView = UIView()
let snapshotView = UIImageView()
public let backgroundView = UIView()
public let snapshotImageView = UIImageView()
let controlButtonsPanel = ControlButtonsPanel()
public var animationDuration: TimeInterval = 0.75
@@ -17,8 +17,8 @@ public class IntelligentsFocusApertureView: UIView {
public internal(set) weak var targetView: UIView?
public internal(set) weak var targetViewController: UIViewController?
public internal(set) weak var capturedImage: UIImage? {
get { snapshotView.image }
set { snapshotView.image = newValue }
get { snapshotImageView.image }
set { snapshotImageView.image = newValue }
}
var frameConstraints: [NSLayoutConstraint] = []
@@ -32,35 +32,40 @@ public class IntelligentsFocusApertureView: UIView {
backgroundView.backgroundColor = .black
backgroundView.isUserInteractionEnabled = true
backgroundView.addGestureRecognizer(UITapGestureRecognizer(
let tap = UITapGestureRecognizer(
target: self,
action: #selector(dismissFocus)
))
)
tap.cancelsTouchesInView = true
backgroundView.addGestureRecognizer(tap)
snapshotView.setContentHuggingPriority(.defaultLow, for: .vertical)
snapshotView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
snapshotView.layer.contentsGravity = .top
snapshotView.layer.masksToBounds = true
snapshotView.contentMode = .scaleAspectFill
snapshotView.isUserInteractionEnabled = true
snapshotView.addGestureRecognizer(UITapGestureRecognizer(
snapshotImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
snapshotImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
snapshotImageView.layer.contentsGravity = .top
snapshotImageView.layer.masksToBounds = true
snapshotImageView.contentMode = .scaleAspectFill
snapshotImageView.isUserInteractionEnabled = true
snapshotImageView.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(dismissFocus)
))
addSubview(backgroundView)
addSubview(controlButtonsPanel)
addSubview(snapshotView)
bringSubviewToFront(snapshotView)
addSubview(snapshotImageView)
bringSubviewToFront(snapshotImageView)
controlButtonsPanel.translateButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .translateTo)
guard let self else { return }
delegate?.focusApertureRequestAction(from: self, actionType: .translateTo)
}
controlButtonsPanel.summaryButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .summary)
guard let self else { return }
delegate?.focusApertureRequestAction(from: self, actionType: .summary)
}
controlButtonsPanel.chatWithAIButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .chatWithAI)
guard let self else { return }
delegate?.focusApertureRequestAction(from: self, actionType: .chatWithAI)
}
removeEveryAutoResizingMasks()
}
@@ -122,7 +127,7 @@ public class IntelligentsFocusApertureView: UIView {
isUserInteractionEnabled = false
executeAnimationDismiss {
self.removeFromSuperview()
self.delegate?.focusApertureRequestAction(actionType: .dismiss)
self.delegate?.focusApertureRequestAction(from: self, actionType: .dismiss)
}
}
}

View File

@@ -0,0 +1,74 @@
//
// DarkActionButton.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
class DarkActionButton: UIView {
var iconSystemName: String {
set { iconView.image = UIImage(systemName: newValue) }
get { fatalError() }
}
var title: String {
set { titleLabel.text = newValue }
get { titleLabel.text ?? "" }
}
let titleLabel = UILabel()
let iconView = UIImageView()
var action: (() -> Void)? = nil
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white.withAlphaComponent(0.25)
layer.cornerRadius = 12
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
titleLabel.textAlignment = .center
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .semibold)
titleLabel.textColor = .white
addSubview(titleLabel)
iconView.contentMode = .scaleAspectFit
iconView.tintColor = .white
addSubview(iconView)
[
layoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
layoutGuide.centerYAnchor.constraint(equalTo: centerYAnchor),
iconView.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
iconView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
iconView.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
titleLabel.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
].forEach { $0.isActive = true }
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(onTapped)
))
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func onTapped() {
action?()
}
}

View File

@@ -0,0 +1,46 @@
# Object files
*.o
*.ko
*.obj
*.elf
# Libraries
*.lib
*.a
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
*.pyc
*~
*.bak
*.diff
*#
*.zip
bstrlib.txt
build
cmark.dSYM/*
cmark
.vscode
.DS_Store
# Testing and benchmark
alltests.md
progit/
bench/benchinput.md
test/afl_results/
# Build directories for SwiftPM and Xcode
.swiftpm
.build

View File

@@ -0,0 +1,170 @@
Copyright (c) 2014, John MacFarlane
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
houdini.h, houdini_href_e.c, houdini_html_e.c, houdini_html_u.c
derive from https://github.com/vmg/houdini (with some modifications)
Copyright (C) 2012 Vicent Martí
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----
buffer.h, buffer.c, chunk.h
are derived from code (C) 2012 Github, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----
utf8.c and utf8.c
are derived from utf8proc
(<http://www.public-software-group.org/utf8proc>),
(C) 2009 Public Software Group e. V., Berlin, Germany.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
-----
The normalization code in normalize.py was derived from the
markdowntest project, Copyright 2013 Karl Dubost:
The MIT License (MIT)
Copyright (c) 2013 Karl Dubost
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
The CommonMark spec (test/spec.txt) is
Copyright (C) 2014-15 John MacFarlane
Released under the Creative Commons CC-BY-SA 4.0 license:
<http://creativecommons.org/licenses/by-sa/4.0/>.
-----
The test software in test/ is
Copyright (c) 2014, John MacFarlane
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,42 @@
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
/*
The main purpose to fork this library is for adding streamed parser support.
Suppose we add a new function:
- cmark_parser *cmark_parser_fork(cmark_parser *parser);
In this way we can call `cmark_parser_finish` to get the result without redo the entire document.
But it is over my ability to implement this function. The parser class is too complex to manage.
*/
let cSettings: [CSetting] = [
.define("CMARK_THREADING"),
]
let package = Package(
name: "MarkdownParserCore",
products: [
.library(name: "MarkdownParserCore", targets: ["cmark-gfm"]),
.library(name: "MarkdownParserCoreExtension", targets: ["cmark-gfm-extensions"]),
],
targets: [
.target(
name: "cmark-gfm",
path: "src",
cSettings: cSettings
),
.target(
name: "cmark-gfm-extensions",
dependencies: ["cmark-gfm"],
path: "extensions",
cSettings: cSettings
),
]
)

View File

@@ -0,0 +1,510 @@
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include "autolink.h"
#include <parser.h>
#include <utf8.h>
#if defined(_WIN32)
#define strncasecmp _strnicmp
#else
#include <strings.h>
#endif
static int is_valid_hostchar(const uint8_t *link, size_t link_len) {
int32_t ch;
int r = cmark_utf8proc_iterate(link, (bufsize_t)link_len, &ch);
if (r < 0)
return 0;
return !cmark_utf8proc_is_space(ch) && !cmark_utf8proc_is_punctuation(ch);
}
static int sd_autolink_issafe(const uint8_t *link, size_t link_len) {
static const size_t valid_uris_count = 3;
static const char *valid_uris[] = {"http://", "https://", "ftp://"};
size_t i;
for (i = 0; i < valid_uris_count; ++i) {
size_t len = strlen(valid_uris[i]);
if (link_len > len && strncasecmp((char *)link, valid_uris[i], len) == 0 &&
is_valid_hostchar(link + len, link_len - len))
return 1;
}
return 0;
}
static size_t autolink_delim(uint8_t *data, size_t link_end) {
size_t i;
size_t closing = 0;
size_t opening = 0;
for (i = 0; i < link_end; ++i) {
const uint8_t c = data[i];
if (c == '<') {
link_end = i;
break;
} else if (c == '(') {
opening++;
} else if (c == ')') {
closing++;
}
}
while (link_end > 0) {
switch (data[link_end - 1]) {
case ')':
/* Allow any number of matching brackets (as recognised in copen/cclose)
* at the end of the URL. If there is a greater number of closing
* brackets than opening ones, we remove one character from the end of
* the link.
*
* Examples (input text => output linked portion):
*
* http://www.pokemon.com/Pikachu_(Electric)
* => http://www.pokemon.com/Pikachu_(Electric)
*
* http://www.pokemon.com/Pikachu_((Electric)
* => http://www.pokemon.com/Pikachu_((Electric)
*
* http://www.pokemon.com/Pikachu_(Electric))
* => http://www.pokemon.com/Pikachu_(Electric)
*
* http://www.pokemon.com/Pikachu_((Electric))
* => http://www.pokemon.com/Pikachu_((Electric))
*/
if (closing <= opening) {
return link_end;
}
closing--;
link_end--;
break;
case '?':
case '!':
case '.':
case ',':
case ':':
case '*':
case '_':
case '~':
case '\'':
case '"':
link_end--;
break;
case ';': {
size_t new_end = link_end - 2;
while (new_end > 0 && cmark_isalpha(data[new_end]))
new_end--;
if (new_end < link_end - 2 && data[new_end] == '&')
link_end = new_end;
else
link_end--;
break;
}
default:
return link_end;
}
}
return link_end;
}
static size_t check_domain(uint8_t *data, size_t size, int allow_short) {
size_t i, np = 0, uscore1 = 0, uscore2 = 0;
/* The purpose of this code is to reject urls that contain an underscore
* in one of the last two segments. Examples:
*
* www.xxx.yyy.zzz autolinked
* www.xxx.yyy._zzz not autolinked
* www.xxx._yyy.zzz not autolinked
* www._xxx.yyy.zzz autolinked
*
* The reason is that domain names are allowed to include underscores,
* but host names are not. See: https://stackoverflow.com/a/2183140
*/
for (i = 1; i < size - 1; i++) {
if (data[i] == '\\' && i < size - 2)
i++;
if (data[i] == '_')
uscore2++;
else if (data[i] == '.') {
uscore1 = uscore2;
uscore2 = 0;
np++;
} else if (!is_valid_hostchar(data + i, size - i) && data[i] != '-')
break;
}
if (uscore1 > 0 || uscore2 > 0) {
/* If the url is very long then accept it despite the underscores,
* to avoid quadratic behavior causing a denial of service. See:
* https://github.com/github/cmark-gfm/security/advisories/GHSA-29g3-96g3-jg6c
* Reasonable urls are unlikely to have more than 10 segments, so
* this extra condition shouldn't have any impact on normal usage.
*/
if (np <= 10) {
return 0;
}
}
if (allow_short) {
/* We don't need a valid domain in the strict sense (with
* least one dot; so just make sure it's composed of valid
* domain characters and return the length of the the valid
* sequence. */
return i;
} else {
/* a valid domain needs to have at least a dot.
* that's as far as we get */
return np ? i : 0;
}
}
static cmark_node *www_match(cmark_parser *parser, cmark_node *parent,
cmark_inline_parser *inline_parser) {
cmark_chunk *chunk = cmark_inline_parser_get_chunk(inline_parser);
size_t max_rewind = cmark_inline_parser_get_offset(inline_parser);
uint8_t *data = chunk->data + max_rewind;
size_t size = chunk->len - max_rewind;
int start = cmark_inline_parser_get_column(inline_parser);
size_t link_end;
if (max_rewind > 0 && strchr("*_~(", data[-1]) == NULL &&
!cmark_isspace(data[-1]))
return 0;
if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
return 0;
link_end = check_domain(data, size, 0);
if (link_end == 0)
return NULL;
while (link_end < size && !cmark_isspace(data[link_end]) && data[link_end] != '<')
link_end++;
link_end = autolink_delim(data, link_end);
if (link_end == 0)
return NULL;
cmark_inline_parser_set_offset(inline_parser, (int)(max_rewind + link_end));
cmark_node *node = cmark_node_new_with_mem(CMARK_NODE_LINK, parser->mem);
cmark_strbuf buf;
cmark_strbuf_init(parser->mem, &buf, 10);
cmark_strbuf_puts(&buf, "http://");
cmark_strbuf_put(&buf, data, (bufsize_t)link_end);
node->as.link.url = cmark_chunk_buf_detach(&buf);
cmark_node *text = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
text->as.literal =
cmark_chunk_dup(chunk, (bufsize_t)max_rewind, (bufsize_t)link_end);
cmark_node_append_child(node, text);
node->start_line = text->start_line =
node->end_line = text->end_line =
cmark_inline_parser_get_line(inline_parser);
node->start_column = text->start_column = start - 1;
node->end_column = text->end_column = cmark_inline_parser_get_column(inline_parser) - 1;
return node;
}
static cmark_node *url_match(cmark_parser *parser, cmark_node *parent,
cmark_inline_parser *inline_parser) {
size_t link_end, domain_len;
int rewind = 0;
cmark_chunk *chunk = cmark_inline_parser_get_chunk(inline_parser);
int max_rewind = cmark_inline_parser_get_offset(inline_parser);
uint8_t *data = chunk->data + max_rewind;
size_t size = chunk->len - max_rewind;
if (size < 4 || data[1] != '/' || data[2] != '/')
return 0;
while (rewind < max_rewind && cmark_isalpha(data[-rewind - 1]))
rewind++;
if (!sd_autolink_issafe(data - rewind, size + rewind))
return 0;
link_end = strlen("://");
domain_len = check_domain(data + link_end, size - link_end, 1);
if (domain_len == 0)
return 0;
link_end += domain_len;
while (link_end < size && !cmark_isspace(data[link_end]) && data[link_end] != '<')
link_end++;
link_end = autolink_delim(data, link_end);
if (link_end == 0)
return NULL;
cmark_inline_parser_set_offset(inline_parser, (int)(max_rewind + link_end));
cmark_node_unput(parent, rewind);
cmark_node *node = cmark_node_new_with_mem(CMARK_NODE_LINK, parser->mem);
cmark_chunk url = cmark_chunk_dup(chunk, max_rewind - rewind,
(bufsize_t)(link_end + rewind));
node->as.link.url = url;
cmark_node *text = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
text->as.literal = url;
cmark_node_append_child(node, text);
node->start_line = text->start_line = node->end_line = text->end_line = cmark_inline_parser_get_line(inline_parser);
node->start_column = text->start_column = max_rewind - rewind;
node->end_column = text->end_column = cmark_inline_parser_get_column(inline_parser) - 1;
return node;
}
static cmark_node *match(cmark_syntax_extension *ext, cmark_parser *parser,
cmark_node *parent, unsigned char c,
cmark_inline_parser *inline_parser) {
if (cmark_inline_parser_in_bracket(inline_parser, false) ||
cmark_inline_parser_in_bracket(inline_parser, true))
return NULL;
if (c == ':')
return url_match(parser, parent, inline_parser);
if (c == 'w')
return www_match(parser, parent, inline_parser);
return NULL;
// note that we could end up re-consuming something already a
// part of an inline, because we don't track when the last
// inline was finished in inlines.c.
}
static bool validate_protocol(const char protocol[], uint8_t *data, size_t rewind, size_t max_rewind) {
size_t len = strlen(protocol);
if (len > (max_rewind - rewind)) {
return false;
}
// Check that the protocol matches
if (memcmp(data - rewind - len, protocol, len) != 0) {
return false;
}
if (len == (max_rewind - rewind)) {
return true;
}
char prev_char = data[-((ptrdiff_t)rewind) - len - 1];
// Make sure the character before the protocol is non-alphanumeric
return !cmark_isalnum(prev_char);
}
static void postprocess_text(cmark_parser *parser, cmark_node *text) {
size_t start = 0;
size_t offset = 0;
// `text` is going to be split into a list of nodes containing shorter segments
// of text, so we detach the memory buffer from text and use `cmark_chunk_dup` to
// create references to it. Later, `cmark_chunk_to_cstr` is used to convert
// the references into allocated buffers. The detached buffer is freed before we
// return.
cmark_chunk detached_chunk = text->as.literal;
text->as.literal = cmark_chunk_dup(&detached_chunk, 0, detached_chunk.len);
uint8_t *data = text->as.literal.data;
size_t remaining = text->as.literal.len;
while (true) {
size_t link_end;
uint8_t *at;
bool auto_mailto = true;
bool is_xmpp = false;
size_t rewind;
size_t max_rewind;
size_t np = 0;
if (offset >= remaining)
break;
at = (uint8_t *)memchr(data + start + offset, '@', remaining - offset);
if (!at)
break;
max_rewind = at - (data + start + offset);
found_at:
for (rewind = 0; rewind < max_rewind; ++rewind) {
uint8_t c = data[start + offset + max_rewind - rewind - 1];
if (cmark_isalnum(c))
continue;
if (strchr(".+-_", c) != NULL)
continue;
if (strchr(":", c) != NULL) {
if (validate_protocol("mailto:", data + start + offset + max_rewind, rewind, max_rewind)) {
auto_mailto = false;
continue;
}
if (validate_protocol("xmpp:", data + start + offset + max_rewind, rewind, max_rewind)) {
auto_mailto = false;
is_xmpp = true;
continue;
}
}
break;
}
if (rewind == 0) {
offset += max_rewind + 1;
continue;
}
assert(data[start + offset + max_rewind] == '@');
for (link_end = 1; link_end < remaining - offset - max_rewind; ++link_end) {
uint8_t c = data[start + offset + max_rewind + link_end];
if (cmark_isalnum(c))
continue;
if (c == '@') {
// Found another '@', so go back and try again with an updated offset and max_rewind.
offset += max_rewind + 1;
max_rewind = link_end - 1;
goto found_at;
} else if (c == '.' && link_end < remaining - offset - max_rewind - 1 &&
cmark_isalnum(data[start + offset + max_rewind + link_end + 1]))
np++;
else if (c == '/' && is_xmpp)
continue;
else if (c != '-' && c != '_')
break;
}
if (link_end < 2 || np == 0 ||
(!cmark_isalpha(data[start + offset + max_rewind + link_end - 1]) &&
data[start + offset + max_rewind + link_end - 1] != '.')) {
offset += max_rewind + link_end;
continue;
}
link_end = autolink_delim(data + start + offset + max_rewind, link_end);
if (link_end == 0) {
offset += max_rewind + 1;
continue;
}
cmark_node *link_node = cmark_node_new_with_mem(CMARK_NODE_LINK, parser->mem);
cmark_strbuf buf;
cmark_strbuf_init(parser->mem, &buf, 10);
if (auto_mailto)
cmark_strbuf_puts(&buf, "mailto:");
cmark_strbuf_put(&buf, data + start + offset + max_rewind - rewind, (bufsize_t)(link_end + rewind));
link_node->as.link.url = cmark_chunk_buf_detach(&buf);
cmark_node *link_text = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
cmark_chunk email = cmark_chunk_dup(
&detached_chunk,
(bufsize_t)(start + offset + max_rewind - rewind),
(bufsize_t)(link_end + rewind));
cmark_chunk_to_cstr(parser->mem, &email);
link_text->as.literal = email;
cmark_node_append_child(link_node, link_text);
cmark_node_insert_after(text, link_node);
cmark_node *post = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
post->as.literal = cmark_chunk_dup(&detached_chunk,
(bufsize_t)(start + offset + max_rewind + link_end),
(bufsize_t)(remaining - offset - max_rewind - link_end));
cmark_node_insert_after(link_node, post);
text->as.literal = cmark_chunk_dup(&detached_chunk, (bufsize_t)start, (bufsize_t)(offset + max_rewind - rewind));
cmark_chunk_to_cstr(parser->mem, &text->as.literal);
text = post;
start += offset + max_rewind + link_end;
remaining -= offset + max_rewind + link_end;
offset = 0;
}
// Convert the reference to allocated memory.
assert(!text->as.literal.alloc);
cmark_chunk_to_cstr(parser->mem, &text->as.literal);
// Free the detached buffer.
cmark_chunk_free(parser->mem, &detached_chunk);
}
static cmark_node *postprocess(cmark_syntax_extension *ext, cmark_parser *parser, cmark_node *root) {
cmark_iter *iter;
cmark_event_type ev;
cmark_node *node;
bool in_link = false;
cmark_consolidate_text_nodes(root);
iter = cmark_iter_new(root);
while ((ev = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
node = cmark_iter_get_node(iter);
if (in_link) {
if (ev == CMARK_EVENT_EXIT && node->type == CMARK_NODE_LINK) {
in_link = false;
}
continue;
}
if (ev == CMARK_EVENT_ENTER && node->type == CMARK_NODE_LINK) {
in_link = true;
continue;
}
if (ev == CMARK_EVENT_ENTER && node->type == CMARK_NODE_TEXT) {
postprocess_text(parser, node);
}
}
cmark_iter_free(iter);
return root;
}
cmark_syntax_extension *create_autolink_extension(void) {
cmark_syntax_extension *ext = cmark_syntax_extension_new("autolink");
cmark_llist *special_chars = NULL;
cmark_syntax_extension_set_match_inline_func(ext, match);
cmark_syntax_extension_set_postprocess_func(ext, postprocess);
cmark_mem *mem = cmark_get_default_mem_allocator();
special_chars = cmark_llist_append(mem, special_chars, (void *)':');
special_chars = cmark_llist_append(mem, special_chars, (void *)'w');
cmark_syntax_extension_set_special_inline_chars(ext, special_chars);
return ext;
}

View File

@@ -0,0 +1,8 @@
#ifndef CMARK_GFM_AUTOLINK_H
#define CMARK_GFM_AUTOLINK_H
#include "cmark-gfm-core-extensions.h"
cmark_syntax_extension *create_autolink_extension(void);
#endif

View File

@@ -0,0 +1,31 @@
#include "cmark-gfm-core-extensions.h"
#include "autolink.h"
#include "mutex.h"
#include "node.h"
#include "strikethrough.h"
#include "table.h"
#include "tagfilter.h"
#include "tasklist.h"
#include "registry.h"
#include "plugin.h"
static int core_extensions_registration(cmark_plugin *plugin) {
cmark_plugin_register_syntax_extension(plugin, create_table_extension());
cmark_plugin_register_syntax_extension(plugin,
create_strikethrough_extension());
cmark_plugin_register_syntax_extension(plugin, create_autolink_extension());
cmark_plugin_register_syntax_extension(plugin, create_tagfilter_extension());
cmark_plugin_register_syntax_extension(plugin, create_tasklist_extension());
return 1;
}
CMARK_DEFINE_ONCE(registered);
static void register_plugins(void) {
cmark_register_plugin(core_extensions_registration);
}
CMARK_GFM_EXPORT
void cmark_gfm_core_extensions_ensure_registered(void) {
CMARK_RUN_ONCE(registered, register_plugins);
}

View File

@@ -0,0 +1,879 @@
/* Generated by re2c 1.3 */
#include "ext_scanners.h"
#include <stdlib.h>
bufsize_t _ext_scan_at(bufsize_t (*scanner)(const unsigned char *),
unsigned char *ptr, int len, bufsize_t offset) {
bufsize_t res;
if (ptr == NULL || offset >= len) {
return 0;
} else {
unsigned char lim = ptr[len];
ptr[len] = '\0';
res = scanner(ptr + offset);
ptr[len] = lim;
}
return res;
}
bufsize_t _scan_table_start(const unsigned char *p) {
const unsigned char *marker = NULL;
const unsigned char *start = p;
{
unsigned char yych;
static const unsigned char yybm[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
yych = *p;
if (yych <= ' ') {
if (yych <= '\n') {
if (yych == '\t')
goto yy4;
} else {
if (yych <= '\f')
goto yy4;
if (yych >= ' ')
goto yy4;
}
} else {
if (yych <= '9') {
if (yych == '-')
goto yy5;
} else {
if (yych <= ':')
goto yy6;
if (yych == '|')
goto yy4;
}
}
++p;
yy3 : { return 0; }
yy4:
yych = *(marker = ++p);
if (yybm[0 + yych] & 64) {
goto yy7;
}
if (yych == '-')
goto yy10;
if (yych == ':')
goto yy12;
goto yy3;
yy5:
yych = *(marker = ++p);
if (yybm[0 + yych] & 128) {
goto yy10;
}
if (yych <= ' ') {
if (yych <= 0x08)
goto yy3;
if (yych <= '\r')
goto yy14;
if (yych <= 0x1F)
goto yy3;
goto yy14;
} else {
if (yych <= ':') {
if (yych <= '9')
goto yy3;
goto yy13;
} else {
if (yych == '|')
goto yy14;
goto yy3;
}
}
yy6:
yych = *(marker = ++p);
if (yybm[0 + yych] & 128) {
goto yy10;
}
goto yy3;
yy7:
yych = *++p;
if (yybm[0 + yych] & 64) {
goto yy7;
}
if (yych == '-')
goto yy10;
if (yych == ':')
goto yy12;
yy9:
p = marker;
goto yy3;
yy10:
yych = *++p;
if (yybm[0 + yych] & 128) {
goto yy10;
}
if (yych <= 0x1F) {
if (yych <= '\n') {
if (yych <= 0x08)
goto yy9;
if (yych <= '\t')
goto yy13;
goto yy15;
} else {
if (yych <= '\f')
goto yy13;
if (yych <= '\r')
goto yy17;
goto yy9;
}
} else {
if (yych <= ':') {
if (yych <= ' ')
goto yy13;
if (yych <= '9')
goto yy9;
goto yy13;
} else {
if (yych == '|')
goto yy18;
goto yy9;
}
}
yy12:
yych = *++p;
if (yybm[0 + yych] & 128) {
goto yy10;
}
goto yy9;
yy13:
yych = *++p;
yy14:
if (yych <= '\r') {
if (yych <= '\t') {
if (yych <= 0x08)
goto yy9;
goto yy13;
} else {
if (yych <= '\n')
goto yy15;
if (yych <= '\f')
goto yy13;
goto yy17;
}
} else {
if (yych <= ' ') {
if (yych <= 0x1F)
goto yy9;
goto yy13;
} else {
if (yych == '|')
goto yy18;
goto yy9;
}
}
yy15:
++p;
{ return (bufsize_t)(p - start); }
yy17:
yych = *++p;
if (yych == '\n')
goto yy15;
goto yy9;
yy18:
yych = *++p;
if (yybm[0 + yych] & 128) {
goto yy10;
}
if (yych <= '\r') {
if (yych <= '\t') {
if (yych <= 0x08)
goto yy9;
goto yy18;
} else {
if (yych <= '\n')
goto yy15;
if (yych <= '\f')
goto yy18;
goto yy17;
}
} else {
if (yych <= ' ') {
if (yych <= 0x1F)
goto yy9;
goto yy18;
} else {
if (yych == ':')
goto yy12;
goto yy9;
}
}
}
}
bufsize_t _scan_table_cell(const unsigned char *p) {
const unsigned char *marker = NULL;
const unsigned char *start = p;
{
unsigned char yych;
unsigned int yyaccept = 0;
static const unsigned char yybm[] = {
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 64, 64, 0, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 128, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 64,
64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
};
yych = *p;
if (yybm[0 + yych] & 64) {
goto yy22;
}
if (yych <= 0xEC) {
if (yych <= 0xC1) {
if (yych <= '\r')
goto yy25;
if (yych <= '\\')
goto yy27;
goto yy25;
} else {
if (yych <= 0xDF)
goto yy29;
if (yych <= 0xE0)
goto yy30;
goto yy31;
}
} else {
if (yych <= 0xF0) {
if (yych <= 0xED)
goto yy32;
if (yych <= 0xEF)
goto yy31;
goto yy33;
} else {
if (yych <= 0xF3)
goto yy34;
if (yych <= 0xF4)
goto yy35;
goto yy25;
}
}
yy22:
yyaccept = 0;
yych = *(marker = ++p);
if (yybm[0 + yych] & 64) {
goto yy22;
}
if (yych <= 0xEC) {
if (yych <= 0xC1) {
if (yych <= '\r')
goto yy24;
if (yych <= '\\')
goto yy27;
} else {
if (yych <= 0xDF)
goto yy36;
if (yych <= 0xE0)
goto yy38;
goto yy39;
}
} else {
if (yych <= 0xF0) {
if (yych <= 0xED)
goto yy40;
if (yych <= 0xEF)
goto yy39;
goto yy41;
} else {
if (yych <= 0xF3)
goto yy42;
if (yych <= 0xF4)
goto yy43;
}
}
yy24 : { return (bufsize_t)(p - start); }
yy25:
++p;
yy26 : { return 0; }
yy27:
yyaccept = 0;
yych = *(marker = ++p);
if (yybm[0 + yych] & 128) {
goto yy27;
}
if (yych <= 0xDF) {
if (yych <= '\f') {
if (yych == '\n')
goto yy24;
goto yy22;
} else {
if (yych <= '\r')
goto yy24;
if (yych <= 0x7F)
goto yy22;
if (yych <= 0xC1)
goto yy24;
goto yy36;
}
} else {
if (yych <= 0xEF) {
if (yych <= 0xE0)
goto yy38;
if (yych == 0xED)
goto yy40;
goto yy39;
} else {
if (yych <= 0xF0)
goto yy41;
if (yych <= 0xF3)
goto yy42;
if (yych <= 0xF4)
goto yy43;
goto yy24;
}
}
yy29:
yych = *++p;
if (yych <= 0x7F)
goto yy26;
if (yych <= 0xBF)
goto yy22;
goto yy26;
yy30:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x9F)
goto yy26;
if (yych <= 0xBF)
goto yy36;
goto yy26;
yy31:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x7F)
goto yy26;
if (yych <= 0xBF)
goto yy36;
goto yy26;
yy32:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x7F)
goto yy26;
if (yych <= 0x9F)
goto yy36;
goto yy26;
yy33:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x8F)
goto yy26;
if (yych <= 0xBF)
goto yy39;
goto yy26;
yy34:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x7F)
goto yy26;
if (yych <= 0xBF)
goto yy39;
goto yy26;
yy35:
yyaccept = 1;
yych = *(marker = ++p);
if (yych <= 0x7F)
goto yy26;
if (yych <= 0x8F)
goto yy39;
goto yy26;
yy36:
yych = *++p;
if (yych <= 0x7F)
goto yy37;
if (yych <= 0xBF)
goto yy22;
yy37:
p = marker;
if (yyaccept == 0) {
goto yy24;
} else {
goto yy26;
}
yy38:
yych = *++p;
if (yych <= 0x9F)
goto yy37;
if (yych <= 0xBF)
goto yy36;
goto yy37;
yy39:
yych = *++p;
if (yych <= 0x7F)
goto yy37;
if (yych <= 0xBF)
goto yy36;
goto yy37;
yy40:
yych = *++p;
if (yych <= 0x7F)
goto yy37;
if (yych <= 0x9F)
goto yy36;
goto yy37;
yy41:
yych = *++p;
if (yych <= 0x8F)
goto yy37;
if (yych <= 0xBF)
goto yy39;
goto yy37;
yy42:
yych = *++p;
if (yych <= 0x7F)
goto yy37;
if (yych <= 0xBF)
goto yy39;
goto yy37;
yy43:
yych = *++p;
if (yych <= 0x7F)
goto yy37;
if (yych <= 0x8F)
goto yy39;
goto yy37;
}
}
bufsize_t _scan_table_cell_end(const unsigned char *p) {
const unsigned char *start = p;
{
unsigned char yych;
static const unsigned char yybm[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
yych = *p;
if (yych == '|')
goto yy48;
++p;
{ return 0; }
yy48:
yych = *++p;
if (yybm[0 + yych] & 128) {
goto yy48;
}
{ return (bufsize_t)(p - start); }
}
}
bufsize_t _scan_table_row_end(const unsigned char *p) {
const unsigned char *marker = NULL;
const unsigned char *start = p;
{
unsigned char yych;
static const unsigned char yybm[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
yych = *p;
if (yych <= '\f') {
if (yych <= 0x08)
goto yy53;
if (yych == '\n')
goto yy56;
goto yy55;
} else {
if (yych <= '\r')
goto yy58;
if (yych == ' ')
goto yy55;
}
yy53:
++p;
yy54 : { return 0; }
yy55:
yych = *(marker = ++p);
if (yych <= 0x08)
goto yy54;
if (yych <= '\r')
goto yy60;
if (yych == ' ')
goto yy60;
goto yy54;
yy56:
++p;
{ return (bufsize_t)(p - start); }
yy58:
yych = *++p;
if (yych == '\n')
goto yy56;
goto yy54;
yy59:
yych = *++p;
yy60:
if (yybm[0 + yych] & 128) {
goto yy59;
}
if (yych <= 0x08)
goto yy61;
if (yych <= '\n')
goto yy56;
if (yych <= '\r')
goto yy62;
yy61:
p = marker;
goto yy54;
yy62:
yych = *++p;
if (yych == '\n')
goto yy56;
goto yy61;
}
}
bufsize_t _scan_tasklist(const unsigned char *p) {
const unsigned char *marker = NULL;
const unsigned char *start = p;
{
unsigned char yych;
static const unsigned char yybm[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 64, 64, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
yych = *p;
if (yych <= ' ') {
if (yych <= '\n') {
if (yych == '\t')
goto yy67;
} else {
if (yych <= '\f')
goto yy67;
if (yych >= ' ')
goto yy67;
}
} else {
if (yych <= ',') {
if (yych <= ')')
goto yy65;
if (yych <= '+')
goto yy68;
} else {
if (yych <= '-')
goto yy68;
if (yych <= '/')
goto yy65;
if (yych <= '9')
goto yy69;
}
}
yy65:
++p;
yy66 : { return 0; }
yy67:
yych = *(marker = ++p);
if (yybm[0 + yych] & 64) {
goto yy70;
}
if (yych <= ',') {
if (yych <= ')')
goto yy66;
if (yych <= '+')
goto yy73;
goto yy66;
} else {
if (yych <= '-')
goto yy73;
if (yych <= '/')
goto yy66;
if (yych <= '9')
goto yy74;
goto yy66;
}
yy68:
yych = *(marker = ++p);
if (yych <= '\n') {
if (yych == '\t')
goto yy75;
goto yy66;
} else {
if (yych <= '\f')
goto yy75;
if (yych == ' ')
goto yy75;
goto yy66;
}
yy69:
yych = *(marker = ++p);
if (yych <= 0x1F) {
if (yych <= '\t') {
if (yych <= 0x08)
goto yy78;
goto yy73;
} else {
if (yych <= '\n')
goto yy66;
if (yych <= '\f')
goto yy73;
goto yy78;
}
} else {
if (yych <= 0x7F) {
if (yych <= ' ')
goto yy73;
goto yy78;
} else {
if (yych <= 0xC1)
goto yy66;
if (yych <= 0xF4)
goto yy78;
goto yy66;
}
}
yy70:
yych = *++p;
if (yybm[0 + yych] & 64) {
goto yy70;
}
if (yych <= ',') {
if (yych <= ')')
goto yy72;
if (yych <= '+')
goto yy73;
} else {
if (yych <= '-')
goto yy73;
if (yych <= '/')
goto yy72;
if (yych <= '9')
goto yy74;
}
yy72:
p = marker;
goto yy66;
yy73:
yych = *++p;
if (yych == '[')
goto yy72;
goto yy76;
yy74:
yych = *++p;
if (yych <= '\n') {
if (yych == '\t')
goto yy73;
goto yy78;
} else {
if (yych <= '\f')
goto yy73;
if (yych == ' ')
goto yy73;
goto yy78;
}
yy75:
yych = *++p;
yy76:
if (yych <= '\f') {
if (yych == '\t')
goto yy75;
if (yych <= '\n')
goto yy72;
goto yy75;
} else {
if (yych <= ' ') {
if (yych <= 0x1F)
goto yy72;
goto yy75;
} else {
if (yych == '[')
goto yy86;
goto yy72;
}
}
yy77:
yych = *++p;
yy78:
if (yybm[0 + yych] & 128) {
goto yy77;
}
if (yych <= 0xC1) {
if (yych <= '\f') {
if (yych <= 0x08)
goto yy73;
if (yych == '\n')
goto yy72;
goto yy75;
} else {
if (yych == ' ')
goto yy75;
if (yych <= 0x7F)
goto yy73;
goto yy72;
}
} else {
if (yych <= 0xED) {
if (yych <= 0xDF)
goto yy79;
if (yych <= 0xE0)
goto yy80;
if (yych <= 0xEC)
goto yy81;
goto yy82;
} else {
if (yych <= 0xF0) {
if (yych <= 0xEF)
goto yy81;
goto yy83;
} else {
if (yych <= 0xF3)
goto yy84;
if (yych <= 0xF4)
goto yy85;
goto yy72;
}
}
}
yy79:
yych = *++p;
if (yych <= 0x7F)
goto yy72;
if (yych <= 0xBF)
goto yy73;
goto yy72;
yy80:
yych = *++p;
if (yych <= 0x9F)
goto yy72;
if (yych <= 0xBF)
goto yy79;
goto yy72;
yy81:
yych = *++p;
if (yych <= 0x7F)
goto yy72;
if (yych <= 0xBF)
goto yy79;
goto yy72;
yy82:
yych = *++p;
if (yych <= 0x7F)
goto yy72;
if (yych <= 0x9F)
goto yy79;
goto yy72;
yy83:
yych = *++p;
if (yych <= 0x8F)
goto yy72;
if (yych <= 0xBF)
goto yy81;
goto yy72;
yy84:
yych = *++p;
if (yych <= 0x7F)
goto yy72;
if (yych <= 0xBF)
goto yy81;
goto yy72;
yy85:
yych = *++p;
if (yych <= 0x7F)
goto yy72;
if (yych <= 0x8F)
goto yy81;
goto yy72;
yy86:
yych = *++p;
if (yych <= 'W') {
if (yych != ' ')
goto yy72;
} else {
if (yych <= 'X')
goto yy87;
if (yych != 'x')
goto yy72;
}
yy87:
yych = *++p;
if (yych != ']')
goto yy72;
yych = *++p;
if (yych <= '\n') {
if (yych != '\t')
goto yy72;
} else {
if (yych <= '\f')
goto yy89;
if (yych != ' ')
goto yy72;
}
yy89:
yych = *++p;
if (yych <= '\n') {
if (yych == '\t')
goto yy89;
} else {
if (yych <= '\f')
goto yy89;
if (yych == ' ')
goto yy89;
}
{ return (bufsize_t)(p - start); }
}
}

View File

@@ -0,0 +1,24 @@
#include "chunk.h"
#include "cmark-gfm.h"
#ifdef __cplusplus
extern "C" {
#endif
bufsize_t _ext_scan_at(bufsize_t (*scanner)(const unsigned char *),
unsigned char *ptr, int len, bufsize_t offset);
bufsize_t _scan_table_start(const unsigned char *p);
bufsize_t _scan_table_cell(const unsigned char *p);
bufsize_t _scan_table_cell_end(const unsigned char *p);
bufsize_t _scan_table_row_end(const unsigned char *p);
bufsize_t _scan_tasklist(const unsigned char *p);
#define scan_table_start(c, l, n) _ext_scan_at(&_scan_table_start, c, l, n)
#define scan_table_cell(c, l, n) _ext_scan_at(&_scan_table_cell, c, l, n)
#define scan_table_cell_end(c, l, n) _ext_scan_at(&_scan_table_cell_end, c, l, n)
#define scan_table_row_end(c, l, n) _ext_scan_at(&_scan_table_row_end, c, l, n)
#define scan_tasklist(c, l, n) _ext_scan_at(&_scan_tasklist, c, l, n)
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,87 @@
#ifndef CMARK_GFM_CORE_EXTENSIONS_H
#define CMARK_GFM_CORE_EXTENSIONS_H
#ifdef __cplusplus
extern "C" {
#endif
#include "cmark-gfm-extension_api.h"
#include "export.h"
#include <stdbool.h>
#include <stdint.h>
CMARK_GFM_EXPORT
void cmark_gfm_core_extensions_ensure_registered(void);
CMARK_GFM_EXPORT
uint16_t cmark_gfm_extensions_get_table_columns(cmark_node *node);
/** Sets the number of columns for the table, returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_table_columns(cmark_node *node, uint16_t n_columns);
CMARK_GFM_EXPORT
uint8_t *cmark_gfm_extensions_get_table_alignments(cmark_node *node);
/** Sets the alignments for the table, returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_table_alignments(cmark_node *node, uint16_t ncols, uint8_t *alignments);
CMARK_GFM_EXPORT
int cmark_gfm_extensions_get_table_row_is_header(cmark_node *node);
/** Sets the column span for the table cell, returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_table_cell_colspan(cmark_node *node, unsigned colspan);
/** Sets the row span for the table cell, returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowspan);
/**
Gets the column span for the table cell, returning \c UINT_MAX on error.
A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous
cell with a span > 1.
Column span is only parsed when \c CMARK_OPT_TABLE_SPANS is set.
*/
CMARK_GFM_EXPORT
unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node);
/**
Gets the row span for the table cell, returning \c UINT_MAX on error.
A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous
cell with a span > 1.
Row span is only parsed when \c CMARK_OPT_TABLE_SPANS is set.
*/
CMARK_GFM_EXPORT
unsigned cmark_gfm_extensions_get_table_cell_rowspan(cmark_node *node);
/** Sets whether the node is a table header row, returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_table_row_is_header(cmark_node *node, int is_header);
CMARK_GFM_EXPORT
bool cmark_gfm_extensions_get_tasklist_item_checked(cmark_node *node);
/* For backwards compatibility */
#define cmark_gfm_extensions_tasklist_is_checked cmark_gfm_extensions_get_tasklist_item_checked
/** Sets whether a tasklist item is "checked" (completed), returning 1 on success and 0 on error.
*/
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_tasklist_item_checked(cmark_node *node, bool is_checked);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,5 @@
module cmark_gfm_extensions {
header "cmark-gfm-core-extensions.h"
}

View File

@@ -0,0 +1,169 @@
#include <stdbool.h>
#include "strikethrough.h"
#include <parser.h>
#include <render.h>
cmark_node_type CMARK_NODE_STRIKETHROUGH;
static cmark_node *match(cmark_syntax_extension *self, cmark_parser *parser,
cmark_node *parent, unsigned char character,
cmark_inline_parser *inline_parser) {
cmark_node *res = NULL;
int left_flanking, right_flanking, punct_before, punct_after, delims;
char buffer[101];
if (character != '~')
return NULL;
delims = cmark_inline_parser_scan_delimiters(
inline_parser, sizeof(buffer) - 1, '~',
&left_flanking,
&right_flanking, &punct_before, &punct_after);
memset(buffer, '~', delims);
buffer[delims] = 0;
res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
cmark_node_set_literal(res, buffer);
res->start_line = res->end_line = cmark_inline_parser_get_line(inline_parser);
res->start_column = cmark_inline_parser_get_column(inline_parser) - delims;
if ((left_flanking || right_flanking) &&
(delims == 2 || (!(parser->options & CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE) && delims == 1))) {
cmark_inline_parser_push_delimiter(inline_parser, character, left_flanking,
right_flanking, res);
}
return res;
}
static delimiter *insert(cmark_syntax_extension *self, cmark_parser *parser,
cmark_inline_parser *inline_parser, delimiter *opener,
delimiter *closer) {
cmark_node *strikethrough;
cmark_node *tmp, *next;
delimiter *delim, *tmp_delim;
delimiter *res = closer->next;
strikethrough = opener->inl_text;
if (opener->inl_text->as.literal.len != closer->inl_text->as.literal.len)
goto done;
if (!cmark_node_set_type(strikethrough, CMARK_NODE_STRIKETHROUGH))
goto done;
cmark_node_set_syntax_extension(strikethrough, self);
tmp = cmark_node_next(opener->inl_text);
while (tmp) {
if (tmp == closer->inl_text)
break;
next = cmark_node_next(tmp);
cmark_node_append_child(strikethrough, tmp);
tmp = next;
}
strikethrough->end_column = closer->inl_text->start_column + closer->inl_text->as.literal.len - 1;
cmark_node_free(closer->inl_text);
done:
delim = closer;
while (delim != NULL && delim != opener) {
tmp_delim = delim->previous;
cmark_inline_parser_remove_delimiter(inline_parser, delim);
delim = tmp_delim;
}
cmark_inline_parser_remove_delimiter(inline_parser, opener);
return res;
}
static const char *get_type_string(cmark_syntax_extension *extension,
cmark_node *node) {
return node->type == CMARK_NODE_STRIKETHROUGH ? "strikethrough" : "<unknown>";
}
static int can_contain(cmark_syntax_extension *extension, cmark_node *node,
cmark_node_type child_type) {
if (node->type != CMARK_NODE_STRIKETHROUGH)
return false;
return CMARK_NODE_TYPE_INLINE_P(child_type);
}
static void commonmark_render(cmark_syntax_extension *extension,
cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
renderer->out(renderer, node, "~~", false, LITERAL);
}
static void latex_render(cmark_syntax_extension *extension,
cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
// requires \usepackage{ulem}
bool entering = (ev_type == CMARK_EVENT_ENTER);
if (entering) {
renderer->out(renderer, node, "\\sout{", false, LITERAL);
} else {
renderer->out(renderer, node, "}", false, LITERAL);
}
}
static void man_render(cmark_syntax_extension *extension,
cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
bool entering = (ev_type == CMARK_EVENT_ENTER);
if (entering) {
renderer->cr(renderer);
renderer->out(renderer, node, ".ST \"", false, LITERAL);
} else {
renderer->out(renderer, node, "\"", false, LITERAL);
renderer->cr(renderer);
}
}
static void html_render(cmark_syntax_extension *extension,
cmark_html_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
bool entering = (ev_type == CMARK_EVENT_ENTER);
if (entering) {
cmark_strbuf_puts(renderer->html, "<del>");
} else {
cmark_strbuf_puts(renderer->html, "</del>");
}
}
static void plaintext_render(cmark_syntax_extension *extension,
cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
renderer->out(renderer, node, "~", false, LITERAL);
}
cmark_syntax_extension *create_strikethrough_extension(void) {
cmark_syntax_extension *ext = cmark_syntax_extension_new("strikethrough");
cmark_llist *special_chars = NULL;
cmark_syntax_extension_set_get_type_string_func(ext, get_type_string);
cmark_syntax_extension_set_can_contain_func(ext, can_contain);
cmark_syntax_extension_set_commonmark_render_func(ext, commonmark_render);
cmark_syntax_extension_set_latex_render_func(ext, latex_render);
cmark_syntax_extension_set_man_render_func(ext, man_render);
cmark_syntax_extension_set_html_render_func(ext, html_render);
cmark_syntax_extension_set_plaintext_render_func(ext, plaintext_render);
CMARK_NODE_STRIKETHROUGH = cmark_syntax_extension_add_node(1);
cmark_syntax_extension_set_match_inline_func(ext, match);
cmark_syntax_extension_set_inline_from_delim_func(ext, insert);
cmark_mem *mem = cmark_get_default_mem_allocator();
special_chars = cmark_llist_append(mem, special_chars, (void *)'~');
cmark_syntax_extension_set_special_inline_chars(ext, special_chars);
cmark_syntax_extension_set_emphasis(ext, 1);
return ext;
}

View File

@@ -0,0 +1,9 @@
#ifndef CMARK_GFM_STRIKETHROUGH_H
#define CMARK_GFM_STRIKETHROUGH_H
#include "cmark-gfm-core-extensions.h"
extern cmark_node_type CMARK_NODE_STRIKETHROUGH;
cmark_syntax_extension *create_strikethrough_extension(void);
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
#ifndef CMARK_GFM_TABLE_H
#define CMARK_GFM_TABLE_H
#include "cmark-gfm-core-extensions.h"
extern cmark_node_type CMARK_NODE_TABLE, CMARK_NODE_TABLE_ROW,
CMARK_NODE_TABLE_CELL;
cmark_syntax_extension *create_table_extension(void);
#endif

View File

@@ -0,0 +1,60 @@
#include "tagfilter.h"
#include <parser.h>
#include <ctype.h>
static const char *blacklist[] = {
"title", "textarea", "style", "xmp", "iframe",
"noembed", "noframes", "script", "plaintext", NULL,
};
static int is_tag(const unsigned char *tag_data, size_t tag_size,
const char *tagname) {
size_t i;
if (tag_size < 3 || tag_data[0] != '<')
return 0;
i = 1;
if (tag_data[i] == '/') {
i++;
}
for (; i < tag_size; ++i, ++tagname) {
if (*tagname == 0)
break;
if (tolower(tag_data[i]) != *tagname)
return 0;
}
if (i == tag_size)
return 0;
if (cmark_isspace(tag_data[i]) || tag_data[i] == '>')
return 1;
if (tag_data[i] == '/' && tag_size >= i + 2 && tag_data[i + 1] == '>')
return 1;
return 0;
}
static int filter(cmark_syntax_extension *ext, const unsigned char *tag,
size_t tag_len) {
const char **it;
for (it = blacklist; *it; ++it) {
if (is_tag(tag, tag_len, *it)) {
return 0;
}
}
return 1;
}
cmark_syntax_extension *create_tagfilter_extension(void) {
cmark_syntax_extension *ext = cmark_syntax_extension_new("tagfilter");
cmark_syntax_extension_set_html_filter_func(ext, filter);
return ext;
}

View File

@@ -0,0 +1,8 @@
#ifndef CMARK_GFM_TAGFILTER_H
#define CMARK_GFM_TAGFILTER_H
#include "cmark-gfm-core-extensions.h"
cmark_syntax_extension *create_tagfilter_extension(void);
#endif

View File

@@ -0,0 +1,160 @@
#include <stdbool.h>
#include "tasklist.h"
#include <parser.h>
#include <render.h>
#include <html.h>
#include "ext_scanners.h"
typedef enum {
CMARK_TASKLIST_NOCHECKED,
CMARK_TASKLIST_CHECKED,
} cmark_tasklist_type;
// Local constants
static const char *TYPE_STRING = "tasklist";
static const char *get_type_string(cmark_syntax_extension *extension, cmark_node *node) {
return TYPE_STRING;
}
// Return 1 if state was set, 0 otherwise
CMARK_GFM_EXPORT
int cmark_gfm_extensions_set_tasklist_item_checked(cmark_node *node, bool is_checked) {
// The node has to exist, and be an extension, and actually be the right type in order to get the value.
if (!node || !node->extension || strcmp(cmark_node_get_type_string(node), TYPE_STRING))
return 0;
node->as.list.checked = is_checked;
return 1;
}
CMARK_GFM_EXPORT
bool cmark_gfm_extensions_get_tasklist_item_checked(cmark_node *node) {
if (!node || !node->extension || strcmp(cmark_node_get_type_string(node), TYPE_STRING))
return false;
if (node->as.list.checked) {
return true;
}
else {
return false;
}
}
static bool parse_node_item_prefix(cmark_parser *parser, const char *input,
cmark_node *container) {
bool res = false;
if (parser->indent >=
container->as.list.marker_offset + container->as.list.padding) {
cmark_parser_advance_offset(parser, input, container->as.list.marker_offset +
container->as.list.padding,
true);
res = true;
} else if (parser->blank && container->first_child != NULL) {
// if container->first_child is NULL, then the opening line
// of the list item was blank after the list marker; in this
// case, we are done with the list item.
cmark_parser_advance_offset(parser, input, parser->first_nonspace - parser->offset,
false);
res = true;
}
return res;
}
static int matches(cmark_syntax_extension *self, cmark_parser *parser,
unsigned char *input, int len,
cmark_node *parent_container) {
return parse_node_item_prefix(parser, (const char*)input, parent_container);
}
static int can_contain(cmark_syntax_extension *extension, cmark_node *node,
cmark_node_type child_type) {
return (node->type == CMARK_NODE_ITEM) ? 1 : 0;
}
static cmark_node *open_tasklist_item(cmark_syntax_extension *self,
int indented, cmark_parser *parser,
cmark_node *parent_container,
unsigned char *input, int len) {
cmark_node_type node_type = cmark_node_get_type(parent_container);
if (node_type != CMARK_NODE_ITEM) {
return NULL;
}
bufsize_t matched = scan_tasklist(input, len, 0);
if (!matched) {
return NULL;
}
cmark_node_set_syntax_extension(parent_container, self);
cmark_parser_advance_offset(parser, (char *)input, 3, false);
// Either an upper or lower case X means the task is completed.
parent_container->as.list.checked = (strstr((char*)input, "[x]") || strstr((char*)input, "[X]"));
return NULL;
}
static void commonmark_render(cmark_syntax_extension *extension,
cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
bool entering = (ev_type == CMARK_EVENT_ENTER);
if (entering) {
renderer->cr(renderer);
if (node->as.list.checked) {
renderer->out(renderer, node, "- [x] ", false, LITERAL);
} else {
renderer->out(renderer, node, "- [ ] ", false, LITERAL);
}
cmark_strbuf_puts(renderer->prefix, " ");
} else {
cmark_strbuf_truncate(renderer->prefix, renderer->prefix->size - 2);
renderer->cr(renderer);
}
}
static void html_render(cmark_syntax_extension *extension,
cmark_html_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
bool entering = (ev_type == CMARK_EVENT_ENTER);
if (entering) {
cmark_html_render_cr(renderer->html);
cmark_strbuf_puts(renderer->html, "<li");
cmark_html_render_sourcepos(node, renderer->html, options);
cmark_strbuf_putc(renderer->html, '>');
if (node->as.list.checked) {
cmark_strbuf_puts(renderer->html, "<input type=\"checkbox\" checked=\"\" disabled=\"\" /> ");
} else {
cmark_strbuf_puts(renderer->html, "<input type=\"checkbox\" disabled=\"\" /> ");
}
} else {
cmark_strbuf_puts(renderer->html, "</li>\n");
}
}
static const char *xml_attr(cmark_syntax_extension *extension,
cmark_node *node) {
if (node->as.list.checked) {
return " completed=\"true\"";
} else {
return " completed=\"false\"";
}
}
cmark_syntax_extension *create_tasklist_extension(void) {
cmark_syntax_extension *ext = cmark_syntax_extension_new("tasklist");
cmark_syntax_extension_set_match_block_func(ext, matches);
cmark_syntax_extension_set_get_type_string_func(ext, get_type_string);
cmark_syntax_extension_set_open_block_func(ext, open_tasklist_item);
cmark_syntax_extension_set_can_contain_func(ext, can_contain);
cmark_syntax_extension_set_commonmark_render_func(ext, commonmark_render);
cmark_syntax_extension_set_plaintext_render_func(ext, commonmark_render);
cmark_syntax_extension_set_html_render_func(ext, html_render);
cmark_syntax_extension_set_xml_attr_func(ext, xml_attr);
return ext;
}

View File

@@ -0,0 +1,8 @@
#ifndef TASKLIST_H
#define TASKLIST_H
#include "cmark-gfm-core-extensions.h"
cmark_syntax_extension *create_tasklist_extension(void);
#endif

View File

@@ -0,0 +1,123 @@
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "cmark-gfm.h"
#include "cmark-gfm-extension_api.h"
#include "mutex.h"
CMARK_DEFINE_LOCK(arena)
static struct arena_chunk {
size_t sz, used;
uint8_t push_point;
void *ptr;
struct arena_chunk *prev;
} *A = NULL;
static struct arena_chunk *alloc_arena_chunk(size_t sz, struct arena_chunk *prev) {
struct arena_chunk *c = (struct arena_chunk *)calloc(1, sizeof(*c));
if (!c)
abort();
c->sz = sz;
c->ptr = calloc(1, sz);
if (!c->ptr)
abort();
c->prev = prev;
return c;
}
void cmark_arena_push(void) {
CMARK_INITIALIZE_AND_LOCK(arena);
if (A) {
A->push_point = 1;
A = alloc_arena_chunk(10240, A);
}
CMARK_UNLOCK(arena);
}
int cmark_arena_pop(void) {
int ret = 1;
CMARK_INITIALIZE_AND_LOCK(arena);
if (!A)
ret = 0;
else {
while (A && !A->push_point) {
free(A->ptr);
struct arena_chunk *n = A->prev;
free(A);
A = n;
}
if (A)
A->push_point = 0;
}
CMARK_UNLOCK(arena);
return ret;
}
static void init_arena(void) {
CMARK_INITIALIZE_AND_LOCK(arena);
A = alloc_arena_chunk(4 * 1048576, NULL);
CMARK_UNLOCK(arena);
}
void cmark_arena_reset(void) {
CMARK_INITIALIZE_AND_LOCK(arena);
while (A) {
free(A->ptr);
struct arena_chunk *n = A->prev;
free(A);
A = n;
}
CMARK_UNLOCK(arena);
}
static void *arena_calloc(size_t nmem, size_t size) {
if (!A)
init_arena();
size_t sz = nmem * size + sizeof(size_t);
// Round allocation sizes to largest integer size to
// ensure returned memory is correctly aligned
const size_t align = sizeof(size_t) - 1;
sz = (sz + align) & ~align;
CMARK_INITIALIZE_AND_LOCK(arena);
struct arena_chunk *chunk;
if (sz > A->sz) {
A->prev = chunk = alloc_arena_chunk(sz, A->prev);
} else if (sz > A->sz - A->used) {
A = chunk = alloc_arena_chunk(A->sz + A->sz / 2, A);
} else {
chunk = A;
}
void *ptr = (uint8_t *) chunk->ptr + chunk->used;
chunk->used += sz;
*((size_t *) ptr) = sz - sizeof(size_t);
CMARK_UNLOCK(arena);
return (uint8_t *) ptr + sizeof(size_t);
}
static void *arena_realloc(void *ptr, size_t size) {
if (!A)
init_arena();
void *new_ptr = arena_calloc(1, size);
if (ptr)
memcpy(new_ptr, ptr, ((size_t *) ptr)[-1]);
return new_ptr;
}
static void arena_free(void *ptr) {
(void) ptr;
/* no-op */
}
cmark_mem CMARK_ARENA_MEM_ALLOCATOR = {arena_calloc, arena_realloc, arena_free};
cmark_mem *cmark_get_arena_mem_allocator(void) {
return &CMARK_ARENA_MEM_ALLOCATOR;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,277 @@
#include <assert.h>
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cmark_ctype.h"
#include "buffer.h"
/* Used as default value for cmark_strbuf->ptr so that people can always
* assume ptr is non-NULL and zero terminated even for new cmark_strbufs.
*/
unsigned char cmark_strbuf__initbuf[1];
#ifndef MIN
#define MIN(x, y) ((x < y) ? x : y)
#endif
void cmark_strbuf_init(cmark_mem *mem, cmark_strbuf *buf,
bufsize_t initial_size) {
buf->mem = mem;
buf->asize = 0;
buf->size = 0;
buf->ptr = cmark_strbuf__initbuf;
if (initial_size > 0)
cmark_strbuf_grow(buf, initial_size);
}
static inline void S_strbuf_grow_by(cmark_strbuf *buf, bufsize_t add) {
cmark_strbuf_grow(buf, buf->size + add);
}
void cmark_strbuf_grow(cmark_strbuf *buf, bufsize_t target_size) {
assert(target_size > 0);
if (target_size < buf->asize)
return;
if (target_size > (bufsize_t)(INT32_MAX / 2)) {
fprintf(stderr,
"[cmark] cmark_strbuf_grow requests buffer with size > %d, aborting\n",
(INT32_MAX / 2));
abort();
}
/* Oversize the buffer by 50% to guarantee amortized linear time
* complexity on append operations. */
bufsize_t new_size = target_size + target_size / 2;
new_size += 1;
new_size = (new_size + 7) & ~7;
buf->ptr = (unsigned char *)buf->mem->realloc(buf->asize ? buf->ptr : NULL,
new_size);
buf->asize = new_size;
}
bufsize_t cmark_strbuf_len(const cmark_strbuf *buf) { return buf->size; }
void cmark_strbuf_free(cmark_strbuf *buf) {
if (!buf)
return;
if (buf->ptr != cmark_strbuf__initbuf)
buf->mem->free(buf->ptr);
cmark_strbuf_init(buf->mem, buf, 0);
}
void cmark_strbuf_clear(cmark_strbuf *buf) {
buf->size = 0;
if (buf->asize > 0)
buf->ptr[0] = '\0';
}
void cmark_strbuf_set(cmark_strbuf *buf, const unsigned char *data,
bufsize_t len) {
if (len <= 0 || data == NULL) {
cmark_strbuf_clear(buf);
} else {
if (data != buf->ptr) {
if (len >= buf->asize)
cmark_strbuf_grow(buf, len);
memmove(buf->ptr, data, len);
}
buf->size = len;
buf->ptr[buf->size] = '\0';
}
}
void cmark_strbuf_sets(cmark_strbuf *buf, const char *string) {
cmark_strbuf_set(buf, (const unsigned char *)string,
string ? (bufsize_t)strlen(string) : 0);
}
void cmark_strbuf_putc(cmark_strbuf *buf, int c) {
S_strbuf_grow_by(buf, 1);
buf->ptr[buf->size++] = (unsigned char)(c & 0xFF);
buf->ptr[buf->size] = '\0';
}
void cmark_strbuf_put(cmark_strbuf *buf, const unsigned char *data,
bufsize_t len) {
if (len <= 0)
return;
S_strbuf_grow_by(buf, len);
memmove(buf->ptr + buf->size, data, len);
buf->size += len;
buf->ptr[buf->size] = '\0';
}
void cmark_strbuf_puts(cmark_strbuf *buf, const char *string) {
cmark_strbuf_put(buf, (const unsigned char *)string, (bufsize_t)strlen(string));
}
void cmark_strbuf_copy_cstr(char *data, bufsize_t datasize,
const cmark_strbuf *buf) {
bufsize_t copylen;
assert(buf);
if (!data || datasize <= 0)
return;
data[0] = '\0';
if (buf->size == 0 || buf->asize <= 0)
return;
copylen = buf->size;
if (copylen > datasize - 1)
copylen = datasize - 1;
memmove(data, buf->ptr, copylen);
data[copylen] = '\0';
}
void cmark_strbuf_swap(cmark_strbuf *buf_a, cmark_strbuf *buf_b) {
cmark_strbuf t = *buf_a;
*buf_a = *buf_b;
*buf_b = t;
}
unsigned char *cmark_strbuf_detach(cmark_strbuf *buf) {
unsigned char *data = buf->ptr;
if (buf->asize == 0) {
/* return an empty string */
return (unsigned char *)buf->mem->calloc(1, 1);
}
cmark_strbuf_init(buf->mem, buf, 0);
return data;
}
int cmark_strbuf_cmp(const cmark_strbuf *a, const cmark_strbuf *b) {
int result = memcmp(a->ptr, b->ptr, MIN(a->size, b->size));
return (result != 0) ? result
: (a->size < b->size) ? -1 : (a->size > b->size) ? 1 : 0;
}
bufsize_t cmark_strbuf_strchr(const cmark_strbuf *buf, int c, bufsize_t pos) {
if (pos >= buf->size)
return -1;
if (pos < 0)
pos = 0;
const unsigned char *p =
(unsigned char *)memchr(buf->ptr + pos, c, buf->size - pos);
if (!p)
return -1;
return (bufsize_t)(p - (const unsigned char *)buf->ptr);
}
bufsize_t cmark_strbuf_strrchr(const cmark_strbuf *buf, int c, bufsize_t pos) {
if (pos < 0 || buf->size == 0)
return -1;
if (pos >= buf->size)
pos = buf->size - 1;
bufsize_t i;
for (i = pos; i >= 0; i--) {
if (buf->ptr[i] == (unsigned char)c)
return i;
}
return -1;
}
void cmark_strbuf_truncate(cmark_strbuf *buf, bufsize_t len) {
if (len < 0)
len = 0;
if (len < buf->size) {
buf->size = len;
buf->ptr[buf->size] = '\0';
}
}
void cmark_strbuf_drop(cmark_strbuf *buf, bufsize_t n) {
if (n > 0) {
if (n > buf->size)
n = buf->size;
buf->size = buf->size - n;
if (buf->size)
memmove(buf->ptr, buf->ptr + n, buf->size);
buf->ptr[buf->size] = '\0';
}
}
void cmark_strbuf_rtrim(cmark_strbuf *buf) {
if (!buf->size)
return;
while (buf->size > 0) {
if (!cmark_isspace(buf->ptr[buf->size - 1]))
break;
buf->size--;
}
buf->ptr[buf->size] = '\0';
}
void cmark_strbuf_trim(cmark_strbuf *buf) {
bufsize_t i = 0;
if (!buf->size)
return;
while (i < buf->size && cmark_isspace(buf->ptr[i]))
i++;
cmark_strbuf_drop(buf, i);
cmark_strbuf_rtrim(buf);
}
// Destructively modify string, collapsing consecutive
// space and newline characters into a single space.
void cmark_strbuf_normalize_whitespace(cmark_strbuf *s) {
bool last_char_was_space = false;
bufsize_t r, w;
for (r = 0, w = 0; r < s->size; ++r) {
if (cmark_isspace(s->ptr[r])) {
if (!last_char_was_space) {
s->ptr[w++] = ' ';
last_char_was_space = true;
}
} else {
s->ptr[w++] = s->ptr[r];
last_char_was_space = false;
}
}
cmark_strbuf_truncate(s, w);
}
// Destructively unescape a string: remove backslashes before punctuation chars.
extern void cmark_strbuf_unescape(cmark_strbuf *buf) {
bufsize_t r, w;
for (r = 0, w = 0; r < buf->size; ++r) {
if (buf->ptr[r] == '\\' && cmark_ispunct(buf->ptr[r + 1]))
r++;
buf->ptr[w++] = buf->ptr[r];
}
cmark_strbuf_truncate(buf, w);
}

View File

@@ -0,0 +1,55 @@
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include "registry.h"
#include "node.h"
#include "houdini.h"
#include "cmark-gfm.h"
#include "buffer.h"
cmark_node_type CMARK_NODE_LAST_BLOCK = CMARK_NODE_FOOTNOTE_DEFINITION;
cmark_node_type CMARK_NODE_LAST_INLINE = CMARK_NODE_ATTRIBUTE;
int cmark_version(void) { return CMARK_GFM_VERSION; }
const char *cmark_version_string(void) { return CMARK_GFM_VERSION_STRING; }
static void *xcalloc(size_t nmem, size_t size) {
void *ptr = calloc(nmem, size);
if (!ptr) {
fprintf(stderr, "[cmark] calloc returned null pointer, aborting\n");
abort();
}
return ptr;
}
static void *xrealloc(void *ptr, size_t size) {
void *new_ptr = realloc(ptr, size);
if (!new_ptr) {
fprintf(stderr, "[cmark] realloc returned null pointer, aborting\n");
abort();
}
return new_ptr;
}
static void xfree(void *ptr) {
free(ptr);
}
cmark_mem CMARK_DEFAULT_MEM_ALLOCATOR = {xcalloc, xrealloc, xfree};
cmark_mem *cmark_get_default_mem_allocator(void) {
return &CMARK_DEFAULT_MEM_ALLOCATOR;
}
char *cmark_markdown_to_html(const char *text, size_t len, int options) {
cmark_node *doc;
char *result;
doc = cmark_parse_document(text, len, options);
result = cmark_render_html(doc, options, NULL);
cmark_node_free(doc);
return result;
}

View File

@@ -0,0 +1,44 @@
#include <stdint.h>
#include "cmark_ctype.h"
/** 1 = space, 2 = punct, 3 = digit, 4 = alpha, 0 = other
*/
static const uint8_t cmark_ctype_class[256] = {
/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */
/* 0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0,
/* 1 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 2 */ 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
/* 3 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2,
/* 4 */ 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
/* 5 */ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 2,
/* 6 */ 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
/* 7 */ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 0,
/* 8 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 9 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* a */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* b */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* c */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* d */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* e */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* f */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/**
* Returns 1 if c is a "whitespace" character as defined by the spec.
*/
int cmark_isspace(char c) { return cmark_ctype_class[(uint8_t)c] == 1; }
/**
* Returns 1 if c is an ascii punctuation character.
*/
int cmark_ispunct(char c) { return cmark_ctype_class[(uint8_t)c] == 2; }
int cmark_isalnum(char c) {
uint8_t result;
result = cmark_ctype_class[(uint8_t)c];
return (result == 3 || result == 4);
}
int cmark_isdigit(char c) { return cmark_ctype_class[(uint8_t)c] == 3; }
int cmark_isalpha(char c) { return cmark_ctype_class[(uint8_t)c] == 4; }

View File

@@ -0,0 +1,524 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cmark-gfm.h"
#include "node.h"
#include "buffer.h"
#include "utf8.h"
#include "scanners.h"
#include "render.h"
#include "syntax_extension.h"
#define OUT(s, wrap, escaping) renderer->out(renderer, node, s, wrap, escaping)
#define LIT(s) renderer->out(renderer, node, s, false, LITERAL)
#define CR() renderer->cr(renderer)
#define BLANKLINE() renderer->blankline(renderer)
#define ENCODED_SIZE 20
#define LISTMARKER_SIZE 20
// Functions to convert cmark_nodes to commonmark strings.
static inline void outc(cmark_renderer *renderer, cmark_node *node,
cmark_escaping escape, int32_t c, unsigned char nextc) {
bool needs_escaping = false;
bool follows_digit =
renderer->buffer->size > 0 &&
cmark_isdigit(renderer->buffer->ptr[renderer->buffer->size - 1]);
char encoded[ENCODED_SIZE];
needs_escaping =
c < 0x80 && escape != LITERAL &&
((escape == NORMAL &&
(c < 0x20 ||
c == '*' || c == '_' || c == '[' || c == ']' || c == '#' || c == '<' ||
c == '>' || c == '\\' || c == '`' || c == '~' || c == '!' ||
(c == '&' && cmark_isalpha(nextc)) || (c == '!' && nextc == '[') ||
(c == '^' && nextc == '[') ||
(renderer->begin_content && (c == '-' || c == '+' || c == '=') &&
// begin_content doesn't get set to false til we've passed digits
// at the beginning of line, so...
!follows_digit) ||
(renderer->begin_content && (c == '.' || c == ')') && follows_digit &&
(nextc == 0 || cmark_isspace(nextc))))) ||
(escape == URL &&
(c == '`' || c == '<' || c == '>' || cmark_isspace((char)c) || c == '\\' ||
c == ')' || c == '(')) ||
(escape == TITLE &&
(c == '`' || c == '<' || c == '>' || c == '"' || c == '\\')));
if (needs_escaping) {
if (escape == URL && cmark_isspace((char)c)) {
// use percent encoding for spaces
snprintf(encoded, ENCODED_SIZE, "%%%2X", c);
cmark_strbuf_puts(renderer->buffer, encoded);
renderer->column += 3;
} else if (cmark_ispunct((char)c)) {
cmark_render_ascii(renderer, "\\");
cmark_render_code_point(renderer, c);
} else { // render as entity
snprintf(encoded, ENCODED_SIZE, "&#%d;", c);
cmark_strbuf_puts(renderer->buffer, encoded);
renderer->column += (int)strlen(encoded);
}
} else {
cmark_render_code_point(renderer, c);
}
}
static int longest_backtick_sequence(const char *code) {
int longest = 0;
int current = 0;
size_t i = 0;
size_t code_len = strlen(code);
while (i <= code_len) {
if (code[i] == '`') {
current++;
} else {
if (current > longest) {
longest = current;
}
current = 0;
}
i++;
}
return longest;
}
static int shortest_unused_backtick_sequence(const char *code) {
// note: if the shortest sequence is >= 32, this returns 32
// so as not to overflow the bit array.
uint32_t used = 1;
int current = 0;
size_t i = 0;
size_t code_len = strlen(code);
while (i <= code_len) {
if (code[i] == '`') {
current++;
} else {
if (current > 0 && current < 32) {
used |= (1U << current);
}
current = 0;
}
i++;
}
// return number of first bit that is 0:
i = 0;
while (i < 32 && used & 1) {
used = used >> 1;
i++;
}
return (int)i;
}
static bool is_autolink(cmark_node *node) {
cmark_chunk *title;
cmark_chunk *url;
cmark_node *link_text;
char *realurl;
int realurllen;
if (node->type != CMARK_NODE_LINK) {
return false;
}
url = &node->as.link.url;
if (url->len == 0 || scan_scheme(url, 0) == 0) {
return false;
}
title = &node->as.link.title;
// if it has a title, we can't treat it as an autolink:
if (title->len > 0) {
return false;
}
link_text = node->first_child;
if (link_text == NULL) {
return false;
}
cmark_consolidate_text_nodes(link_text);
realurl = (char *)url->data;
realurllen = url->len;
if (strncmp(realurl, "mailto:", 7) == 0) {
realurl += 7;
realurllen -= 7;
}
return (realurllen == link_text->as.literal.len &&
strncmp(realurl, (char *)link_text->as.literal.data,
link_text->as.literal.len) == 0);
}
static int S_render_node(cmark_renderer *renderer, cmark_node *node,
cmark_event_type ev_type, int options) {
int list_number;
cmark_delim_type list_delim;
int numticks;
bool extra_spaces;
int i;
bool entering = (ev_type == CMARK_EVENT_ENTER);
const char *info, *code, *title;
char fencechar[2] = {'\0', '\0'};
size_t info_len, code_len;
char listmarker[LISTMARKER_SIZE];
const char *emph_delim;
bool first_in_list_item;
bufsize_t marker_width;
bool allow_wrap = renderer->width > 0 && !(CMARK_OPT_NOBREAKS & options) &&
!(CMARK_OPT_HARDBREAKS & options);
// Don't adjust tight list status til we've started the list.
// Otherwise we loose the blank line between a paragraph and
// a following list.
if (entering) {
if (node->parent && node->parent->type == CMARK_NODE_ITEM) {
renderer->in_tight_list_item = node->parent->parent->as.list.tight;
}
} else {
if (node->type == CMARK_NODE_LIST) {
renderer->in_tight_list_item =
node->parent &&
node->parent->type == CMARK_NODE_ITEM &&
node->parent->parent->as.list.tight;
}
}
if (node->extension && node->extension->commonmark_render_func) {
node->extension->commonmark_render_func(node->extension, renderer, node, ev_type, options);
return 1;
}
switch (node->type) {
case CMARK_NODE_DOCUMENT:
break;
case CMARK_NODE_BLOCK_QUOTE:
if (entering) {
LIT("> ");
renderer->begin_content = true;
cmark_strbuf_puts(renderer->prefix, "> ");
} else {
cmark_strbuf_truncate(renderer->prefix, renderer->prefix->size - 2);
BLANKLINE();
}
break;
case CMARK_NODE_LIST:
if (!entering && node->next && (node->next->type == CMARK_NODE_CODE_BLOCK ||
node->next->type == CMARK_NODE_LIST)) {
// this ensures that a following indented code block or list will be
// inteprereted correctly.
CR();
LIT("<!-- end list -->");
BLANKLINE();
}
break;
case CMARK_NODE_ITEM:
if (cmark_node_get_list_type(node->parent) == CMARK_BULLET_LIST) {
marker_width = 4;
} else {
list_number = cmark_node_get_item_index(node);
list_delim = cmark_node_get_list_delim(node->parent);
// we ensure a width of at least 4 so
// we get nice transition from single digits
// to double
snprintf(listmarker, LISTMARKER_SIZE, "%d%s%s", list_number,
list_delim == CMARK_PAREN_DELIM ? ")" : ".",
list_number < 10 ? " " : " ");
marker_width = (bufsize_t)strlen(listmarker);
}
if (entering) {
if (cmark_node_get_list_type(node->parent) == CMARK_BULLET_LIST) {
LIT(" - ");
renderer->begin_content = true;
} else {
LIT(listmarker);
renderer->begin_content = true;
}
for (i = marker_width; i--;) {
cmark_strbuf_putc(renderer->prefix, ' ');
}
} else {
cmark_strbuf_truncate(renderer->prefix,
renderer->prefix->size - marker_width);
CR();
}
break;
case CMARK_NODE_HEADING:
if (entering) {
for (i = cmark_node_get_heading_level(node); i > 0; i--) {
LIT("#");
}
LIT(" ");
renderer->begin_content = true;
renderer->no_linebreaks = true;
} else {
renderer->no_linebreaks = false;
BLANKLINE();
}
break;
case CMARK_NODE_CODE_BLOCK:
first_in_list_item = node->prev == NULL && node->parent &&
node->parent->type == CMARK_NODE_ITEM;
if (!first_in_list_item) {
BLANKLINE();
}
info = cmark_node_get_fence_info(node);
info_len = strlen(info);
fencechar[0] = strchr(info, '`') == NULL ? '`' : '~';
code = cmark_node_get_literal(node);
code_len = strlen(code);
// use indented form if no info, and code doesn't
// begin or end with a blank line, and code isn't
// first thing in a list item
if (info_len == 0 && (code_len > 2 && !cmark_isspace(code[0]) &&
!(cmark_isspace(code[code_len - 1]) &&
cmark_isspace(code[code_len - 2]))) &&
!first_in_list_item) {
LIT(" ");
cmark_strbuf_puts(renderer->prefix, " ");
OUT(cmark_node_get_literal(node), false, LITERAL);
cmark_strbuf_truncate(renderer->prefix, renderer->prefix->size - 4);
} else {
numticks = longest_backtick_sequence(code) + 1;
if (numticks < 3) {
numticks = 3;
}
for (i = 0; i < numticks; i++) {
LIT(fencechar);
}
LIT(" ");
OUT(info, false, LITERAL);
CR();
OUT(cmark_node_get_literal(node), false, LITERAL);
CR();
for (i = 0; i < numticks; i++) {
LIT(fencechar);
}
}
BLANKLINE();
break;
case CMARK_NODE_HTML_BLOCK:
BLANKLINE();
OUT(cmark_node_get_literal(node), false, LITERAL);
BLANKLINE();
break;
case CMARK_NODE_CUSTOM_BLOCK:
BLANKLINE();
OUT(entering ? cmark_node_get_on_enter(node) : cmark_node_get_on_exit(node),
false, LITERAL);
BLANKLINE();
break;
case CMARK_NODE_THEMATIC_BREAK:
BLANKLINE();
LIT("-----");
BLANKLINE();
break;
case CMARK_NODE_PARAGRAPH:
if (!entering) {
BLANKLINE();
}
break;
case CMARK_NODE_TEXT:
OUT(cmark_node_get_literal(node), allow_wrap, NORMAL);
break;
case CMARK_NODE_LINEBREAK:
if (!(CMARK_OPT_HARDBREAKS & options)) {
LIT(" ");
}
CR();
break;
case CMARK_NODE_SOFTBREAK:
if (CMARK_OPT_HARDBREAKS & options) {
LIT(" ");
CR();
} else if (!renderer->no_linebreaks && renderer->width == 0 &&
!(CMARK_OPT_HARDBREAKS & options) &&
!(CMARK_OPT_NOBREAKS & options)) {
CR();
} else {
OUT(" ", allow_wrap, LITERAL);
}
break;
case CMARK_NODE_CODE:
code = cmark_node_get_literal(node);
code_len = strlen(code);
numticks = shortest_unused_backtick_sequence(code);
extra_spaces = code_len == 0 ||
code[0] == '`' || code[code_len - 1] == '`' ||
code[0] == ' ' || code[code_len - 1] == ' ';
for (i = 0; i < numticks; i++) {
LIT("`");
}
if (extra_spaces) {
LIT(" ");
}
OUT(cmark_node_get_literal(node), allow_wrap, LITERAL);
if (extra_spaces) {
LIT(" ");
}
for (i = 0; i < numticks; i++) {
LIT("`");
}
break;
case CMARK_NODE_HTML_INLINE:
OUT(cmark_node_get_literal(node), false, LITERAL);
break;
case CMARK_NODE_CUSTOM_INLINE:
OUT(entering ? cmark_node_get_on_enter(node) : cmark_node_get_on_exit(node),
false, LITERAL);
break;
case CMARK_NODE_STRONG:
if (node->parent == NULL || node->parent->type != CMARK_NODE_STRONG) {
if (entering) {
LIT("**");
} else {
LIT("**");
}
}
break;
case CMARK_NODE_EMPH:
// If we have EMPH(EMPH(x)), we need to use *_x_*
// because **x** is STRONG(x):
if (node->parent && node->parent->type == CMARK_NODE_EMPH &&
node->next == NULL && node->prev == NULL) {
emph_delim = "_";
} else {
emph_delim = "*";
}
if (entering) {
LIT(emph_delim);
} else {
LIT(emph_delim);
}
break;
case CMARK_NODE_LINK:
if (is_autolink(node)) {
if (entering) {
LIT("<");
if (strncmp(cmark_node_get_url(node), "mailto:", 7) == 0) {
LIT((const char *)cmark_node_get_url(node) + 7);
} else {
LIT((const char *)cmark_node_get_url(node));
}
LIT(">");
// return signal to skip contents of node...
return 0;
}
} else {
if (entering) {
LIT("[");
} else {
LIT("](");
OUT(cmark_node_get_url(node), false, URL);
title = cmark_node_get_title(node);
if (strlen(title) > 0) {
LIT(" \"");
OUT(title, false, TITLE);
LIT("\"");
}
LIT(")");
}
}
break;
case CMARK_NODE_IMAGE:
if (entering) {
LIT("![");
} else {
LIT("](");
OUT(cmark_node_get_url(node), false, URL);
title = cmark_node_get_title(node);
if (strlen(title) > 0) {
OUT(" \"", allow_wrap, LITERAL);
OUT(title, false, TITLE);
LIT("\"");
}
LIT(")");
}
break;
case CMARK_NODE_ATTRIBUTE:
if (entering) {
LIT("^[");
} else {
LIT("](");
OUT(cmark_node_get_attributes(node), false, LITERAL);
LIT(")");
}
break;
case CMARK_NODE_FOOTNOTE_REFERENCE:
if (entering) {
LIT("[^");
char *footnote_label = renderer->mem->calloc(node->parent_footnote_def->as.literal.len + 1, sizeof(char));
memmove(footnote_label, node->parent_footnote_def->as.literal.data, node->parent_footnote_def->as.literal.len);
OUT(footnote_label, false, LITERAL);
renderer->mem->free(footnote_label);
LIT("]");
}
break;
case CMARK_NODE_FOOTNOTE_DEFINITION:
if (entering) {
renderer->footnote_ix += 1;
LIT("[^");
char *footnote_label = renderer->mem->calloc(node->as.literal.len + 1, sizeof(char));
memmove(footnote_label, node->as.literal.data, node->as.literal.len);
OUT(footnote_label, false, LITERAL);
renderer->mem->free(footnote_label);
LIT("]:\n");
cmark_strbuf_puts(renderer->prefix, " ");
} else {
cmark_strbuf_truncate(renderer->prefix, renderer->prefix->size - 4);
}
break;
default:
assert(false);
break;
}
return 1;
}
char *cmark_render_commonmark(cmark_node *root, int options, int width) {
return cmark_render_commonmark_with_mem(root, options, width, cmark_node_mem(root));
}
char *cmark_render_commonmark_with_mem(cmark_node *root, int options, int width, cmark_mem *mem) {
if (options & CMARK_OPT_HARDBREAKS) {
// disable breaking on width, since it has
// a different meaning with OPT_HARDBREAKS
width = 0;
}
return cmark_render(mem, root, options, width, outc, S_render_node);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
#include "cmark-gfm.h"
#include "parser.h"
#include "footnotes.h"
#include "inlines.h"
#include "chunk.h"
static void footnote_free(cmark_map *map, cmark_map_entry *_ref) {
cmark_footnote *ref = (cmark_footnote *)_ref;
cmark_mem *mem = map->mem;
if (ref != NULL) {
mem->free(ref->entry.label);
if (ref->node)
cmark_node_free(ref->node);
mem->free(ref);
}
}
void cmark_footnote_create(cmark_map *map, cmark_node *node) {
cmark_footnote *ref;
unsigned char *reflabel = normalize_map_label(map->mem, &node->as.literal);
/* empty footnote name, or composed from only whitespace */
if (reflabel == NULL)
return;
assert(map->sorted == NULL);
ref = (cmark_footnote *)map->mem->calloc(1, sizeof(*ref));
ref->entry.label = reflabel;
ref->node = node;
ref->entry.age = map->size;
ref->entry.next = map->refs;
map->refs = (cmark_map_entry *)ref;
map->size++;
}
cmark_map *cmark_footnote_map_new(cmark_mem *mem) {
return cmark_map_new(mem, footnote_free);
}
// Before calling `cmark_map_free` on a map with `cmark_footnotes`, first
// unlink all of the footnote nodes before freeing their memory.
//
// Sometimes, two (unused) footnote nodes can end up referencing each other,
// which as they get freed up by calling `cmark_map_free` -> `footnote_free` ->
// etc, can lead to a use-after-free error.
//
// Better to `unlink` every footnote node first, setting their next, prev, and
// parent pointers to NULL, and only then walk thru & free them up.
void cmark_unlink_footnotes_map(cmark_map *map) {
cmark_map_entry *ref;
cmark_map_entry *next;
ref = map->refs;
while(ref) {
next = ref->next;
if (((cmark_footnote *)ref)->node) {
cmark_node_unlink(((cmark_footnote *)ref)->node);
}
ref = next;
}
}

View File

@@ -0,0 +1,110 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "houdini.h"
#if !defined(__has_builtin)
# define __has_builtin(b) 0
#endif
#if !__has_builtin(__builtin_expect)
# define __builtin_expect(e, v) (e)
#endif
#define likely(e) __builtin_expect((e), 1)
/*
* The following characters will not be escaped:
*
* -_.+!*'(),%#@?=;:/,+&$~ alphanum
*
* Note that this character set is the addition of:
*
* - The characters which are safe to be in an URL
* - The characters which are *not* safe to be in
* an URL because they are RESERVED characters.
*
* We assume (lazily) that any RESERVED char that
* appears inside an URL is actually meant to
* have its native function (i.e. as an URL
* component/separator) and hence needs no escaping.
*
* There are two exceptions: the chacters & (amp)
* and ' (single quote) do not appear in the table.
* They are meant to appear in the URL as components,
* yet they require special HTML-entity escaping
* to generate valid HTML markup.
*
* All other characters will be escaped to %XX.
*
*/
static const char HREF_SAFE[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
int houdini_escape_href(cmark_strbuf *ob, const uint8_t *src, bufsize_t size) {
static const uint8_t hex_chars[] = "0123456789ABCDEF";
bufsize_t i = 0, org;
uint8_t hex_str[3];
hex_str[0] = '%';
while (i < size) {
org = i;
while (i < size && HREF_SAFE[src[i]] != 0)
i++;
if (likely(i > org))
cmark_strbuf_put(ob, src + org, i - org);
/* escaping */
if (i >= size)
break;
switch (src[i]) {
/* amp appears all the time in URLs, but needs
* HTML-entity escaping to be inside an href */
case '&':
cmark_strbuf_puts(ob, "&amp;");
break;
/* the single quote is a valid URL character
* according to the standard; it needs HTML
* entity escaping too */
case '\'':
cmark_strbuf_puts(ob, "&#x27;");
break;
/* the space can be escaped to %20 or a plus
* sign. we're going with the generic escape
* for now. the plus thing is more commonly seen
* when building GET strings */
#if 0
case ' ':
cmark_strbuf_putc(ob, '+');
break;
#endif
/* every other character goes with a %XX escaping */
default:
hex_str[1] = hex_chars[(src[i] >> 4) & 0xF];
hex_str[2] = hex_chars[src[i] & 0xF];
cmark_strbuf_put(ob, hex_str, 3);
}
i++;
}
return 1;
}

Some files were not shown because too many files have changed in this diff Show More