2026-06-11 18:12:13 -07:00
|
|
|
import AppKit
|
|
|
|
|
import Foundation
|
|
|
|
|
import PDFKit
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
public enum AcademicAnnotationKind: String, CaseIterable {
|
2026-06-11 18:12:13 -07:00
|
|
|
case comment
|
|
|
|
|
case highlight
|
|
|
|
|
case underline
|
|
|
|
|
case note
|
|
|
|
|
case freeText
|
|
|
|
|
case reply
|
|
|
|
|
case other
|
|
|
|
|
|
|
|
|
|
public init(annotation: PDFAnnotation) {
|
|
|
|
|
if annotation.value(forAnnotationKey: AnnotationKeys.appKind) as? String == AnnotationKeys.appKindComment {
|
|
|
|
|
self = .comment
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if AnnotationKeys.isReply(annotation) {
|
|
|
|
|
self = .reply
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if AnnotationKeys.annotation(annotation, hasSubtype: .highlight) {
|
|
|
|
|
self = .highlight
|
|
|
|
|
} else if AnnotationKeys.annotation(annotation, hasSubtype: .underline) {
|
|
|
|
|
self = .underline
|
|
|
|
|
} else if AnnotationKeys.annotation(annotation, hasSubtype: .text) {
|
|
|
|
|
self = .note
|
|
|
|
|
} else if AnnotationKeys.annotation(annotation, hasSubtype: .freeText) {
|
|
|
|
|
self = .freeText
|
|
|
|
|
} else {
|
|
|
|
|
self = .other
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var displayName: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .comment: return "Comment"
|
|
|
|
|
case .highlight: return "Highlight"
|
|
|
|
|
case .underline: return "Underline"
|
|
|
|
|
case .note: return "Note"
|
|
|
|
|
case .freeText: return "Free Text"
|
|
|
|
|
case .reply: return "Reply"
|
|
|
|
|
case .other: return "Other"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var symbolName: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .comment: return "text.bubble"
|
|
|
|
|
case .highlight: return "highlighter"
|
|
|
|
|
case .underline: return "underline"
|
|
|
|
|
case .note: return "note.text"
|
|
|
|
|
case .freeText: return "textformat"
|
|
|
|
|
case .reply: return "arrowshape.turn.up.left"
|
|
|
|
|
case .other: return "ellipsis"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
public struct AnnotationSnapshot: Identifiable {
|
2026-06-11 18:12:13 -07:00
|
|
|
public let id: String
|
|
|
|
|
public let pageIndex: Int
|
|
|
|
|
public let pageLabel: String
|
|
|
|
|
public let annotationIndex: Int
|
|
|
|
|
public let kind: AcademicAnnotationKind
|
|
|
|
|
public let author: String
|
|
|
|
|
public let createdAt: Date?
|
|
|
|
|
public let modifiedAt: Date?
|
|
|
|
|
public let status: String
|
|
|
|
|
public let contents: String
|
2026-06-29 23:42:39 -07:00
|
|
|
public let highlightText: String
|
2026-06-11 18:12:13 -07:00
|
|
|
public let bounds: CGRect
|
|
|
|
|
public let annotation: PDFAnnotation
|
|
|
|
|
public let page: PDFPage
|
|
|
|
|
public let parentID: String?
|
|
|
|
|
|
|
|
|
|
public init(
|
|
|
|
|
id: String,
|
|
|
|
|
pageIndex: Int,
|
|
|
|
|
pageLabel: String,
|
|
|
|
|
annotationIndex: Int,
|
|
|
|
|
kind: AcademicAnnotationKind,
|
|
|
|
|
author: String,
|
|
|
|
|
createdAt: Date?,
|
|
|
|
|
modifiedAt: Date?,
|
|
|
|
|
status: String,
|
|
|
|
|
contents: String,
|
2026-06-29 23:42:39 -07:00
|
|
|
highlightText: String,
|
2026-06-11 18:12:13 -07:00
|
|
|
bounds: CGRect,
|
|
|
|
|
annotation: PDFAnnotation,
|
|
|
|
|
page: PDFPage,
|
|
|
|
|
parentID: String?
|
|
|
|
|
) {
|
|
|
|
|
self.id = id
|
|
|
|
|
self.pageIndex = pageIndex
|
|
|
|
|
self.pageLabel = pageLabel
|
|
|
|
|
self.annotationIndex = annotationIndex
|
|
|
|
|
self.kind = kind
|
|
|
|
|
self.author = author
|
|
|
|
|
self.createdAt = createdAt
|
|
|
|
|
self.modifiedAt = modifiedAt
|
|
|
|
|
self.status = status
|
|
|
|
|
self.contents = contents
|
2026-06-29 23:42:39 -07:00
|
|
|
self.highlightText = highlightText
|
2026-06-11 18:12:13 -07:00
|
|
|
self.bounds = bounds
|
|
|
|
|
self.annotation = annotation
|
|
|
|
|
self.page = page
|
|
|
|
|
self.parentID = parentID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var firstLine: String {
|
|
|
|
|
let trimmed = contents.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
guard let first = trimmed
|
|
|
|
|
.split(whereSeparator: \.isNewline)
|
|
|
|
|
.first
|
|
|
|
|
.map(String.init)
|
|
|
|
|
else {
|
|
|
|
|
return "No comment"
|
|
|
|
|
}
|
|
|
|
|
return first
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var hasComment: Bool {
|
|
|
|
|
!contents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
public var highlightExcerpt: String {
|
|
|
|
|
let stored = highlightText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
if !stored.isEmpty {
|
|
|
|
|
return stored
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fallback = contents.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
if !fallback.isEmpty {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "Highlight on page \(pageLabel)"
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
public var isReply: Bool {
|
|
|
|
|
parentID != nil
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum AnnotationKeys {
|
|
|
|
|
public static let inReplyTo = PDFAnnotationKey(rawValue: "IRT")
|
|
|
|
|
public static let replyType = PDFAnnotationKey(rawValue: "RT")
|
|
|
|
|
public static let creationDate = PDFAnnotationKey(rawValue: "CreationDate")
|
|
|
|
|
public static let state = PDFAnnotationKey(rawValue: "State")
|
|
|
|
|
public static let stateModel = PDFAnnotationKey(rawValue: "StateModel")
|
|
|
|
|
public static let appKind = PDFAnnotationKey(rawValue: "IHatePDFsKind")
|
|
|
|
|
public static let appKindComment = "Comment"
|
2026-06-11 22:03:43 -07:00
|
|
|
public static let appCommentText = PDFAnnotationKey(rawValue: "IHatePDFsCommentText")
|
2026-06-29 23:42:39 -07:00
|
|
|
public static let appHighlightText = PDFAnnotationKey(rawValue: "IHatePDFsHighlightText")
|
2026-06-11 22:03:43 -07:00
|
|
|
|
|
|
|
|
public static func commentText(for annotation: PDFAnnotation) -> String {
|
2026-06-24 17:51:26 -07:00
|
|
|
if let value = annotation.value(forAnnotationKey: appCommentText) as? String,
|
|
|
|
|
!value.isEmpty {
|
2026-06-11 22:03:43 -07:00
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
if let contents = annotation.contents, !contents.isEmpty {
|
|
|
|
|
return contents
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return annotation.popup?.contents ?? ""
|
2026-06-11 22:03:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func setCommentText(_ text: String, for annotation: PDFAnnotation) {
|
|
|
|
|
_ = annotation.setValue(text, forAnnotationKey: appCommentText)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
public static func stableID(
|
|
|
|
|
for annotation: PDFAnnotation,
|
|
|
|
|
pageIndex: Int,
|
|
|
|
|
annotationIndex: Int
|
|
|
|
|
) -> String {
|
|
|
|
|
if let name = annotation.value(forAnnotationKey: .name) as? String, !name.isEmpty {
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let type = annotation.type ?? "Unknown"
|
|
|
|
|
let rect = annotation.bounds
|
|
|
|
|
return [
|
|
|
|
|
"page-\(pageIndex + 1)",
|
|
|
|
|
"annotation-\(annotationIndex)",
|
|
|
|
|
type,
|
|
|
|
|
String(format: "%.2f-%.2f-%.2f-%.2f", rect.minX, rect.minY, rect.width, rect.height)
|
|
|
|
|
].joined(separator: "-")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func parentID(
|
|
|
|
|
for annotation: PDFAnnotation,
|
|
|
|
|
document: PDFDocument?
|
|
|
|
|
) -> String? {
|
|
|
|
|
if let parentID = annotation.value(forAnnotationKey: inReplyTo) as? String,
|
|
|
|
|
!parentID.isEmpty {
|
2026-06-11 18:34:41 -07:00
|
|
|
return stableIDForAnnotation(named: parentID, in: document) ?? parentID
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let parent = annotation.value(forAnnotationKey: inReplyTo) as? PDFAnnotation else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let page = parent.page,
|
|
|
|
|
let document,
|
|
|
|
|
document.index(for: page) != NSNotFound
|
|
|
|
|
else {
|
|
|
|
|
return parent.value(forAnnotationKey: .name) as? String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let pageIndex = document.index(for: page)
|
|
|
|
|
let annotationIndex = page.annotations.firstIndex(where: { $0 === parent }) ?? 0
|
|
|
|
|
return stableID(for: parent, pageIndex: pageIndex, annotationIndex: annotationIndex)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:34:41 -07:00
|
|
|
private static func stableIDForAnnotation(named name: String, in document: PDFDocument?) -> String? {
|
|
|
|
|
guard let document else { return nil }
|
|
|
|
|
|
|
|
|
|
for pageIndex in 0..<document.pageCount {
|
|
|
|
|
guard let page = document.page(at: pageIndex) else { continue }
|
|
|
|
|
for (annotationIndex, candidate) in page.annotations.enumerated() {
|
|
|
|
|
guard candidate.value(forAnnotationKey: .name) as? String == name else { continue }
|
|
|
|
|
return stableID(for: candidate, pageIndex: pageIndex, annotationIndex: annotationIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:12:13 -07:00
|
|
|
public static func isReply(_ annotation: PDFAnnotation) -> Bool {
|
|
|
|
|
annotation.value(forAnnotationKey: inReplyTo) is PDFAnnotation
|
|
|
|
|
|| annotation.value(forAnnotationKey: inReplyTo) is String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func annotation(_ annotation: PDFAnnotation, hasSubtype subtype: PDFAnnotationSubtype) -> Bool {
|
|
|
|
|
guard let type = annotation.type else { return false }
|
|
|
|
|
let raw = subtype.rawValue
|
|
|
|
|
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
|
|
|
|
|
return type == raw || type == normalized
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func pdfDateString(from date: Date) -> String {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.calendar = Calendar(identifier: .gregorian)
|
|
|
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
|
|
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
|
|
|
formatter.dateFormat = "'D:'yyyyMMddHHmmss'Z00''00'''"
|
|
|
|
|
return formatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func dateValue(for key: PDFAnnotationKey, in annotation: PDFAnnotation) -> Date? {
|
|
|
|
|
if let date = annotation.value(forAnnotationKey: key) as? Date {
|
|
|
|
|
return date
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let value = annotation.value(forAnnotationKey: key) as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parsePDFDate(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func parsePDFDate(_ value: String) -> Date? {
|
|
|
|
|
let normalized = value
|
|
|
|
|
.replacingOccurrences(of: "Z00'00'", with: "Z")
|
|
|
|
|
.replacingOccurrences(of: "Z00\\'00\\'", with: "Z")
|
|
|
|
|
let formats = [
|
|
|
|
|
"'D:'yyyyMMddHHmmss'Z'",
|
|
|
|
|
"'D:'yyyyMMddHHmmss",
|
|
|
|
|
"yyyy-MM-dd'T'HH:mm:ssZ"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for format in formats {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.calendar = Calendar(identifier: .gregorian)
|
|
|
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
|
|
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
|
|
|
formatter.dateFormat = format
|
|
|
|
|
if let date = formatter.date(from: normalized) {
|
|
|
|
|
return date
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum AnnotationReader {
|
|
|
|
|
public static func snapshots(in document: PDFDocument) -> [AnnotationSnapshot] {
|
|
|
|
|
var result: [AnnotationSnapshot] = []
|
2026-06-24 17:51:26 -07:00
|
|
|
var namedAnnotationIDs: [String: String]?
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
for pageIndex in 0..<document.pageCount {
|
|
|
|
|
guard let page = document.page(at: pageIndex) else { continue }
|
2026-06-24 17:51:26 -07:00
|
|
|
result.append(contentsOf: snapshots(
|
|
|
|
|
in: document,
|
|
|
|
|
page: page,
|
|
|
|
|
pageIndex: pageIndex,
|
|
|
|
|
namedAnnotationIDs: &namedAnnotationIDs
|
|
|
|
|
))
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
return sorted(result)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
public static func snapshots(in document: PDFDocument, pages: [PDFPage]) -> [AnnotationSnapshot] {
|
|
|
|
|
var result: [AnnotationSnapshot] = []
|
|
|
|
|
var seenPageIndexes = Set<Int>()
|
|
|
|
|
var namedAnnotationIDs: [String: String]?
|
|
|
|
|
|
|
|
|
|
for page in pages {
|
|
|
|
|
let pageIndex = document.index(for: page)
|
|
|
|
|
guard pageIndex != NSNotFound, seenPageIndexes.insert(pageIndex).inserted else { continue }
|
|
|
|
|
result.append(contentsOf: snapshots(
|
|
|
|
|
in: document,
|
|
|
|
|
page: page,
|
|
|
|
|
pageIndex: pageIndex,
|
|
|
|
|
namedAnnotationIDs: &namedAnnotationIDs
|
|
|
|
|
))
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
return sorted(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func sorted(_ snapshots: [AnnotationSnapshot]) -> [AnnotationSnapshot] {
|
|
|
|
|
snapshots.sorted { left, right in
|
2026-06-11 18:12:13 -07:00
|
|
|
if left.pageIndex != right.pageIndex {
|
|
|
|
|
return left.pageIndex < right.pageIndex
|
|
|
|
|
}
|
|
|
|
|
if left.bounds.maxY != right.bounds.maxY {
|
|
|
|
|
return left.bounds.maxY > right.bounds.maxY
|
|
|
|
|
}
|
|
|
|
|
return left.bounds.minX < right.bounds.minX
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-24 17:51:26 -07:00
|
|
|
|
|
|
|
|
private static func snapshots(
|
|
|
|
|
in document: PDFDocument,
|
|
|
|
|
page: PDFPage,
|
|
|
|
|
pageIndex: Int,
|
|
|
|
|
namedAnnotationIDs: inout [String: String]?
|
|
|
|
|
) -> [AnnotationSnapshot] {
|
|
|
|
|
var result: [AnnotationSnapshot] = []
|
|
|
|
|
|
|
|
|
|
for (annotationIndex, annotation) in page.annotations.enumerated() {
|
|
|
|
|
guard !AnnotationKeys.annotation(annotation, hasSubtype: .popup) else { continue }
|
|
|
|
|
|
|
|
|
|
let kind = AcademicAnnotationKind(annotation: annotation)
|
|
|
|
|
let contents = AnnotationKeys.commentText(for: annotation)
|
2026-06-29 23:42:39 -07:00
|
|
|
let highlightText = annotation.value(forAnnotationKey: AnnotationKeys.appHighlightText) as? String ?? ""
|
2026-06-24 17:51:26 -07:00
|
|
|
guard kind != .other || !contents.isEmpty else { continue }
|
|
|
|
|
|
|
|
|
|
let id = AnnotationKeys.stableID(
|
|
|
|
|
for: annotation,
|
|
|
|
|
pageIndex: pageIndex,
|
|
|
|
|
annotationIndex: annotationIndex
|
|
|
|
|
)
|
|
|
|
|
let pageLabel = page.label ?? "\(pageIndex + 1)"
|
|
|
|
|
let author = annotation.userName
|
|
|
|
|
?? annotation.value(forAnnotationKey: .textLabel) as? String
|
|
|
|
|
?? "Unknown"
|
|
|
|
|
let createdAt = AnnotationKeys.dateValue(for: AnnotationKeys.creationDate, in: annotation)
|
|
|
|
|
?? annotation.modificationDate
|
|
|
|
|
let status = annotation.value(forAnnotationKey: AnnotationKeys.state) as? String
|
|
|
|
|
?? "Unmarked"
|
|
|
|
|
let parentID = parentID(
|
|
|
|
|
for: annotation,
|
|
|
|
|
document: document,
|
|
|
|
|
namedAnnotationIDs: &namedAnnotationIDs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result.append(
|
|
|
|
|
AnnotationSnapshot(
|
|
|
|
|
id: id,
|
|
|
|
|
pageIndex: pageIndex,
|
|
|
|
|
pageLabel: pageLabel,
|
|
|
|
|
annotationIndex: annotationIndex,
|
|
|
|
|
kind: kind,
|
|
|
|
|
author: author,
|
|
|
|
|
createdAt: createdAt,
|
|
|
|
|
modifiedAt: annotation.modificationDate,
|
|
|
|
|
status: status,
|
|
|
|
|
contents: contents,
|
2026-06-29 23:42:39 -07:00
|
|
|
highlightText: highlightText,
|
2026-06-24 17:51:26 -07:00
|
|
|
bounds: annotation.bounds,
|
|
|
|
|
annotation: annotation,
|
|
|
|
|
page: page,
|
|
|
|
|
parentID: parentID
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func parentID(
|
|
|
|
|
for annotation: PDFAnnotation,
|
|
|
|
|
document: PDFDocument,
|
|
|
|
|
namedAnnotationIDs: inout [String: String]?
|
|
|
|
|
) -> String? {
|
|
|
|
|
if let parentID = annotation.value(forAnnotationKey: AnnotationKeys.inReplyTo) as? String,
|
|
|
|
|
!parentID.isEmpty {
|
|
|
|
|
if namedAnnotationIDs == nil {
|
|
|
|
|
namedAnnotationIDs = makeNamedAnnotationIDs(in: document)
|
|
|
|
|
}
|
|
|
|
|
return namedAnnotationIDs?[parentID]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let parent = annotation.value(forAnnotationKey: AnnotationKeys.inReplyTo) as? PDFAnnotation else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let page = parent.page,
|
|
|
|
|
document.index(for: page) != NSNotFound
|
|
|
|
|
else {
|
|
|
|
|
return parent.value(forAnnotationKey: .name) as? String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let pageIndex = document.index(for: page)
|
|
|
|
|
let annotationIndex = page.annotations.firstIndex(where: { $0 === parent }) ?? 0
|
|
|
|
|
return AnnotationKeys.stableID(for: parent, pageIndex: pageIndex, annotationIndex: annotationIndex)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func makeNamedAnnotationIDs(in document: PDFDocument) -> [String: String] {
|
|
|
|
|
var result: [String: String] = [:]
|
|
|
|
|
|
|
|
|
|
for pageIndex in 0..<document.pageCount {
|
|
|
|
|
guard let page = document.page(at: pageIndex) else { continue }
|
|
|
|
|
for (annotationIndex, annotation) in page.annotations.enumerated() {
|
|
|
|
|
guard let name = annotation.value(forAnnotationKey: .name) as? String,
|
|
|
|
|
!name.isEmpty
|
|
|
|
|
else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result[name] = AnnotationKeys.stableID(
|
|
|
|
|
for: annotation,
|
|
|
|
|
pageIndex: pageIndex,
|
|
|
|
|
annotationIndex: annotationIndex
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|