import Foundation #if os(macOS) import AppKit struct MarkdownPresentationSnapshot: Hashable { var lines: [String] var signature: String { lines.joined(separator: "\n") } static func make( source: String, activeLineIndex: Int, containerWidth: CGFloat = 420, usesRenderedControls: Bool = true ) -> MarkdownPresentationSnapshot { let storage = NSTextStorage(string: source) let lineIndex = DocumentLineIndex(source: source) MarkdownTextStyler.apply( to: storage, lineIndex: lineIndex, invalidationPlan: EditorDirtyLineInvalidator.plan( previousText: nil, currentLineIndex: lineIndex, edit: nil, previousActiveLineIndex: nil, currentActiveLineIndex: activeLineIndex ), activeLineIndex: activeLineIndex, backgroundColor: .textBackgroundColor, activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10), textColor: .labelColor, secondaryTextColor: .secondaryLabelColor, accentColor: .controlAccentColor, usesRenderedControls: usesRenderedControls ) return make(from: storage, lineIndex: lineIndex, containerWidth: containerWidth) } static func make( from storage: NSTextStorage, lineIndex: DocumentLineIndex, containerWidth: CGFloat = 420 ) -> MarkdownPresentationSnapshot { let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: NSSize(width: containerWidth, height: CGFloat.greatestFiniteMagnitude)) textContainer.lineFragmentPadding = 0 layoutManager.addTextContainer(textContainer) storage.addLayoutManager(layoutManager) defer { storage.removeLayoutManager(layoutManager) } layoutManager.ensureLayout(for: textContainer) var lines: [String] = [] for boundary in lineIndex.boundaries { let contentRange = boundary.contentRange let paragraphRange = NSRange( location: boundary.contentRange.location, length: min(boundary.nextLineLocation, storage.length) - boundary.contentRange.location ) let glyphRange = layoutManager.glyphRange(forCharacterRange: paragraphRange, actualCharacterRange: nil) let fragment = glyphRange.length > 0 ? layoutManager.lineFragmentRect(forGlyphAt: glyphRange.location, effectiveRange: nil) : .zero let paragraphStyle = storage.attribute(.paragraphStyle, at: min(paragraphRange.location, max(0, storage.length - 1)), effectiveRange: nil) as? NSParagraphStyle let font = contentRange.length > 0 ? storage.attribute(.font, at: contentRange.location, effectiveRange: nil) as? NSFont : nil lines.append([ "line=\(boundary.index)", "range=\(paragraphRange.location):\(paragraphRange.length)", "fragment=\(rounded(fragment.minX)),\(rounded(fragment.minY)),\(rounded(fragment.width)),\(rounded(fragment.height))", "font=\(fontDescription(font))", "paragraph=\(paragraphDescription(paragraphStyle))", "runs=\(attributeRuns(in: storage, range: paragraphRange).joined(separator: ","))" ].joined(separator: "|")) } return MarkdownPresentationSnapshot(lines: lines) } static func contentOrigin( source: String, text: String, activeLineIndex: Int, containerWidth: CGFloat = 420, usesRenderedControls: Bool = true ) -> CGPoint? { let storage = NSTextStorage(string: source) let lineIndex = DocumentLineIndex(source: source) MarkdownTextStyler.apply( to: storage, lineIndex: lineIndex, invalidationPlan: EditorDirtyLineInvalidator.plan( previousText: nil, currentLineIndex: lineIndex, edit: nil, previousActiveLineIndex: nil, currentActiveLineIndex: activeLineIndex ), activeLineIndex: activeLineIndex, backgroundColor: .textBackgroundColor, activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10), textColor: .labelColor, secondaryTextColor: .secondaryLabelColor, accentColor: .controlAccentColor, usesRenderedControls: usesRenderedControls ) let textRange = (source as NSString).range(of: text) guard textRange.location != NSNotFound else { return nil } let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: NSSize(width: containerWidth, height: CGFloat.greatestFiniteMagnitude)) textContainer.lineFragmentPadding = 0 layoutManager.addTextContainer(textContainer) storage.addLayoutManager(layoutManager) defer { storage.removeLayoutManager(layoutManager) } layoutManager.ensureLayout(for: textContainer) let glyphRange = layoutManager.glyphRange(forCharacterRange: NSRange(location: textRange.location, length: 1), actualCharacterRange: nil) guard glyphRange.length > 0 else { return nil } let lineFragment = layoutManager.lineFragmentRect(forGlyphAt: glyphRange.location, effectiveRange: nil) let glyphLocation = layoutManager.location(forGlyphAt: glyphRange.location) return CGPoint(x: lineFragment.minX + glyphLocation.x, y: lineFragment.minY + glyphLocation.y) } private static func attributeRuns(in storage: NSTextStorage, range: NSRange) -> [String] { guard range.length > 0 else { return [] } var runs: [String] = [] storage.enumerateAttributes(in: range) { attributes, effectiveRange, _ in let font = attributes[.font] as? NSFont let foreground = attributes[.foregroundColor] as? NSColor let background = attributes[.backgroundColor] as? NSColor let paragraph = attributes[.paragraphStyle] as? NSParagraphStyle let hidden = foreground?.alphaComponent == 0 && (font?.pointSize ?? 0) < 1 runs.append([ "\(effectiveRange.location):\(effectiveRange.length)", fontDescription(font), "fg=\(rounded(foreground?.alphaComponent ?? 1))", "bg=\(rounded(background?.alphaComponent ?? 0))", "hidden=\(hidden)", paragraphDescription(paragraph) ].joined(separator: "/")) } return runs } private static func fontDescription(_ font: NSFont?) -> String { guard let font else { return "nil" } let isMono = font.fontDescriptor.symbolicTraits.contains(.monoSpace) return "\(isMono ? "mono" : "system"):\(rounded(font.pointSize))" } private static func paragraphDescription(_ paragraph: NSParagraphStyle?) -> String { guard let paragraph else { return "nil" } return [ "ls=\(rounded(paragraph.lineSpacing))", "psb=\(rounded(paragraph.paragraphSpacingBefore))", "ps=\(rounded(paragraph.paragraphSpacing))", "first=\(rounded(paragraph.firstLineHeadIndent))", "head=\(rounded(paragraph.headIndent))" ].joined(separator: ":") } private static func rounded(_ value: CGFloat) -> String { String(format: "%.3f", Double(value)) } } #endif