krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 4b4dc0135e

4b4dc0135e1d59d493a0a49865fcd155aeea59e0

parent: fce1110f7b

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-06 05:24 UTC

Inline images

C-c C-x C-v toggles org-toggle-inline-images: a line that is only a link to
an image file shows the image in its place, at #+ATTR_ORG :width or its own
width up to the window's. #+STARTUP: inlineimages / noinlineimages and
org-startup-with-inline-images (config, Emacs import) turn it on at open.
The link shows again on the caret's line.

Layout: unified · split

Sources/OrgApp/EmacsImport.swift +1 −1
@@ -448,7 +448,7 @@ public struct EmacsImport: Sendable {
448 guard let n = value.integer else { return note("not a number") } 448 guard let n = value.integer else { return note("not a number") }
449 add(name, .integer(n)) 449 add(name, .integer(n))
450 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities", 450 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities",
451 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands": 451 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands", "org-startup-with-inline-images":
452 add(name, .bool(truthy)) 452 add(name, .bool(truthy))
453 case "org-hide-drawer-startup", "org-hide-block-startup": 453 case "org-hide-drawer-startup", "org-hide-block-startup":
454 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy)) 454 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy))
Sources/OrgApp/UserConfig.swift +2
@@ -82,6 +82,8 @@ public enum UserConfig {
82 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""), 82 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""),
83 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""), 83 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""),
84 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""), 84 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""),
85 Setting(section: "", key: "org-startup-with-inline-images", defaultsKey: "startupWithInlineImages", kind: .bool, fallback: .bool(false),
86 comment: "show image links as images when a file opens; #+STARTUP: (no)inlineimages overrides", legacy: ""),
85 Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false), 87 Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false),
86 comment: "single keys at the start of a heading line run commands (n, p, t, c, …)", legacy: ""), 88 comment: "single keys at the start of a heading line run commands (n, p, t, c, …)", legacy: ""),
87 Setting(section: "", key: "org-cycle-hide-drawer-startup", defaultsKey: "hideDrawerStartup", kind: .bool, fallback: .bool(true), 89 Setting(section: "", key: "org-cycle-hide-drawer-startup", defaultsKey: "hideDrawerStartup", kind: .bool, fallback: .bool(true),
Sources/OrgCore/Commands/Command.swift +4 −1
@@ -24,6 +24,8 @@ public struct EditingOptions: Sendable, Equatable, Codable {
24 public var fastTodoSelection: Bool 24 public var fastTodoSelection: Bool
25 /// `org-use-speed-commands`: single keys at the start of a heading line run commands. 25 /// `org-use-speed-commands`: single keys at the start of a heading line run commands.
26 public var useSpeedCommands: Bool 26 public var useSpeedCommands: Bool
27 /// `org-startup-with-inline-images`.
28 public var startupWithInlineImages: Bool
27 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens. 29 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens.
28 public var hideDrawerStartup: Bool 30 public var hideDrawerStartup: Bool
29 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens. 31 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens.
@@ -43,9 +45,10 @@ public struct EditingOptions: Sendable, Equatable, Codable {
43 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false, 45 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false,
44 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true, 46 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true,
45 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil, 47 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil,
46 useSpeedCommands: Bool = false 48 useSpeedCommands: Bool = false, startupWithInlineImages: Bool = false
47 ) { 49 ) {
48 self.useSpeedCommands = useSpeedCommands 50 self.useSpeedCommands = useSpeedCommands
51 self.startupWithInlineImages = startupWithInlineImages
49 self.logReschedule = logReschedule 52 self.logReschedule = logReschedule
50 self.logRedeadline = logRedeadline 53 self.logRedeadline = logRedeadline
51 self.hideDrawerStartup = hideDrawerStartup 54 self.hideDrawerStartup = hideDrawerStartup
Sources/OrgCore/Commands/InlineImages.swift added +45
@@ -0,0 +1,45 @@
1import Foundation
2
3/// Image links shown as images (`org-display-inline-images`, Org 9.8.7): a line that is only a
4/// link to an image file, without a description, and the width `#+ATTR_ORG: :width` asks for.
5public enum InlineImages {
6 public struct Image: Sendable, Equatable {
7 public let path: String
8 public let width: Int?
9 }
10
11 static let extensions = "png|jpe?g|gif|svg|webp|tiff?|bmp|heic|avif"
12 static let pattern = try! NSRegularExpression(pattern: "^[ \\t]*\\[\\[(?:file:)?([^\\]\\n]+\\.(?:\(extensions)))\\]\\][ \\t]*$", options: .caseInsensitive)
13
14 /// The image on the line at `lineStart`, if the line is an image link.
15 public static func image(atLine lineStart: Int, in text: NSString) -> Image? {
16 let line = text.lineRange(for: NSRange(location: lineStart, length: 0))
17 let content = text.substring(with: line).trimmingCharacters(in: .newlines)
18 guard let m = pattern.firstMatch(in: content, range: NSRange(location: 0, length: (content as NSString).length)) else { return nil }
19 let path = (content as NSString).substring(with: m.range(at: 1))
20 guard !path.contains("://"), !path.hasPrefix("attachment:") else { return nil }
21 // `#+ATTR_ORG: :width N` in the affiliated keywords above.
22 var width: Int?
23 var at = line.location
24 while at > 0 {
25 let previous = text.lineRange(for: NSRange(location: at - 1, length: 0))
26 let keyword = text.substring(with: previous)
27 guard keyword.range(of: "^[ \\t]*#\\+[A-Za-z_]+:", options: .regularExpression) != nil else { break }
28 if width == nil, let w = keyword.firstMatch(of: /(?i)^[ \t]*#\+ATTR_ORG:.*:width[ \t]+([0-9]+)/) { width = Int(w.1) }
29 at = previous.location
30 }
31 return Image(path: path, width: width)
32 }
33
34 /// `#+STARTUP: inlineimages` or `noinlineimages`, if the file says.
35 public static func startup(_ tree: OrgTree) -> Bool? {
36 var shown: Bool?
37 for m in tree.root.text.matches(of: #/(?mi)^[ \t]*#\+STARTUP:(.*)$/#) {
38 for word in m.1.split(whereSeparator: \.isWhitespace) {
39 if word.lowercased() == "inlineimages" { shown = true }
40 if word.lowercased() == "noinlineimages" { shown = false }
41 }
42 }
43 return shown
44 }
45}
Sources/OrgCore/Keymap/Presets.swift +3
@@ -732,6 +732,9 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
732 keys = "C-c C-e" 732 keys = "C-c C-e"
733 command = "app.export-dialog" 733 command = "app.export-dialog"
734 [[bind]] 734 [[bind]]
735 keys = "C-c C-x C-v"
736 command = "org.toggle-inline-images"
737 [[bind]]
735 keys = "C-c C-x C-c" 738 keys = "C-c C-x C-c"
736 command = "org.columns" 739 command = "org.columns"
737 [[bind]] 740 [[bind]]
Sources/OrgEditorAppKit/EditorCommands.swift +1
@@ -41,6 +41,7 @@ public enum EditorCommands {
41 EditorCommand(id: "org.narrow.block", title: "Narrow to Block", action: .view), 41 EditorCommand(id: "org.narrow.block", title: "Narrow to Block", action: .view),
42 EditorCommand(id: "org.widen", title: "Widen", action: .view), 42 EditorCommand(id: "org.widen", title: "Widen", action: .view),
43 EditorCommand(id: "org.narrow.toggle", title: "Narrow to Subtree or Widen", action: .view), 43 EditorCommand(id: "org.narrow.toggle", title: "Narrow to Subtree or Widen", action: .view),
44 EditorCommand(id: "org.toggle-inline-images", title: "Show or Hide Inline Images", action: .view),
44 EditorCommand(id: "org.table.toggle-column-width", title: "Shrink or Expand Table Column", action: .view), 45 EditorCommand(id: "org.table.toggle-column-width", title: "Shrink or Expand Table Column", action: .view),
45 EditorCommand(id: "org.table.shrink", title: "Shrink Table Columns with Widths", action: .view), 46 EditorCommand(id: "org.table.shrink", title: "Shrink Table Columns with Widths", action: .view),
46 EditorCommand(id: "org.table.expand", title: "Expand Table Columns", action: .view), 47 EditorCommand(id: "org.table.expand", title: "Expand Table Columns", action: .view),
Sources/OrgEditorAppKit/OrgEditor.swift +119
@@ -151,6 +151,7 @@ public final class OrgEditor: NSObject {
151 startup.wrapperFolds = Set(wrappers.map(\.start)) 151 startup.wrapperFolds = Set(wrappers.map(\.start))
152 if startup != FoldState() { setFolds(startup, wrappers: wrappers) } 152 if startup != FoldState() { setFolds(startup, wrappers: wrappers) }
153 shrinkAtStartup() 153 shrinkAtStartup()
154 if InlineImages.startup(document.tree) ?? options.startupWithInlineImages { showsImages = true }
154 } 155 }
155 } 156 }
156 157
@@ -406,6 +407,7 @@ public final class OrgEditor: NSObject {
406 case "org.cycle": cycleAtCaret() 407 case "org.cycle": cycleAtCaret()
407 case "org.cycle-global": cycleGlobal() 408 case "org.cycle-global": cycleGlobal()
408 case "org.narrow.subtree", "org.narrow.block", "org.narrow.toggle", "org.widen": narrow(id) 409 case "org.narrow.subtree", "org.narrow.block", "org.narrow.toggle", "org.widen": narrow(id)
410 case "org.toggle-inline-images": toggleImages()
409 default: tableColumnWidth(id) 411 default: tableColumnWidth(id)
410 } 412 }
411 case .edit(let selector, let extending): 413 case .edit(let selector, let extending):
@@ -608,6 +610,78 @@ public final class OrgEditor: NSObject {
608 } 610 }
609 } 611 }
610 612
613 // MARK: - Inline images
614
615 /// `org-toggle-inline-images`: lines that are only an image link show the image.
616 public private(set) var showsImages = false
617 /// The file's folder, for relative image paths.
618 public var baseDirectory: URL? {
619 didSet { if showsImages, oldValue != baseDirectory { refreshImages() } }
620 }
621 private static let imageCache = NSCache<NSString, NSImage>()
622
623 private func toggleImages() {
624 showsImages.toggle()
625 refreshImages()
626 if showsImages {
627 let ns = textView.string as NSString
628 var count = 0
629 var offset = 0
630 while offset < ns.length {
631 let line = ns.lineRange(for: NSRange(location: offset, length: 0))
632 if let image = InlineImages.image(atLine: line.location, in: ns), loadImage(image.path) != nil { count += 1 }
633 offset = NSMaxRange(line)
634 }
635 onMessage?("\(count) images displayed inline")
636 } else {
637 onMessage?("Inline image display turned off")
638 }
639 }
640
641 private func refreshImages() {
642 restyleOutsideEditing(0..<utf16Length)
643 textLayoutManager.invalidateLayout(for: textLayoutManager.documentRange)
644 textView.needsDisplay = true
645 }
646
647 private func loadImage(_ path: String) -> NSImage? {
648 let expanded = (path as NSString).expandingTildeInPath
649 let url = expanded.hasPrefix("/") ? URL(fileURLWithPath: expanded) : (baseDirectory ?? URL(fileURLWithPath: FileManager.default.currentDirectoryPath)).appendingPathComponent(expanded)
650 let modified = (try? url.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate?.timeIntervalSince1970 ?? 0
651 let key = "\(url.path)|\(modified)" as NSString
652 if let cached = Self.imageCache.object(forKey: key) { return cached }
653 guard let image = NSImage(contentsOf: url) else { return nil }
654 Self.imageCache.setObject(image, forKey: key)
655 return image
656 }
657
658 /// Image lines in `range` show the image over the link, which is hidden unless the caret is
659 /// on the line.
660 private func applyImages(_ range: Range<Int>, in storage: NSTextStorage, revealed: NSRange) {
661 guard showsImages, isOrg else { return }
662 let text = storage.string as NSString
663 let maxWidth = max(80, (textView.textContainer?.size.width ?? 600) - 40)
664 var location = range.lowerBound
665 while location < range.upperBound, location < text.length {
666 let line = text.lineRange(for: NSRange(location: location, length: 0))
667 location = NSMaxRange(line)
668 guard let image = InlineImages.image(atLine: line.location, in: text), let picture = loadImage(image.path) else { continue }
669 var width = image.width.map(CGFloat.init) ?? picture.size.width
670 width = min(width, maxWidth)
671 let height = picture.size.width > 0 ? width * picture.size.height / picture.size.width : 0
672 let content = NSRange(location: line.location, length: line.length - (text.substring(with: line).hasSuffix("\n") ? 1 : 0))
673 // Invisible, keeping the line's height, so line numbers stay apart.
674 if line.location != revealed.location, content.length > 0 {
675 storage.addAttributes([.foregroundColor: NSColor.clear, .underlineStyle: 0], range: content)
676 }
677 storage.addAttribute(.orgImage, value: InlineImageBox(image: picture, size: CGSize(width: width, height: height)), range: NSRange(location: line.location, length: max(1, content.length)))
678 let style = (storage.attribute(.paragraphStyle, at: line.location, effectiveRange: nil) as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
679 // The image sits where the line is; the spacing makes room for the rest of it.
680 style.paragraphSpacing = max(0, height + 4 - theme.font.boundingRectForFont.height)
681 storage.addAttribute(.paragraphStyle, value: style, range: line)
682 }
683 }
684
611 // MARK: - Narrowing 685 // MARK: - Narrowing
612 686
613 /// The text shown while narrowed (`org-narrow-to-subtree`, `org-narrow-to-block`); the 687 /// The text shown while narrowed (`org-narrow-to-subtree`, `org-narrow-to-block`); the
@@ -898,6 +972,7 @@ public final class OrgEditor: NSObject {
898 if !showMarkup, options.prettyEntities { prettify(range, in: storage, revealed: revealed, scriptExclusions: scriptExclusions) } 972 if !showMarkup, options.prettyEntities { prettify(range, in: storage, revealed: revealed, scriptExclusions: scriptExclusions) }
899 applyShrunk(range, in: storage) 973 applyShrunk(range, in: storage)
900 applyHighlights(range, in: storage) 974 applyHighlights(range, in: storage)
975 applyImages(range, in: storage, revealed: revealed)
901 } 976 }
902 977
903 /// `org-pretty-entities`: entities drawn as their character over the entity's last 978 /// `org-pretty-entities`: entities drawn as their character over the entity's last
@@ -1265,6 +1340,9 @@ extension OrgEditor: NSTextLayoutManagerDelegate {
1265 let offset = manager.offset(from: manager.documentRange.location, to: range.location) 1340 let offset = manager.offset(from: manager.documentRange.location, to: range.location)
1266 if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } 1341 if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) }
1267 if shaded.containing(offset) != nil { return ShadedFragment(textElement: textElement, range: range) } 1342 if shaded.containing(offset) != nil { return ShadedFragment(textElement: textElement, range: range) }
1343 if let paragraph = textElement as? NSTextParagraph, ImageFragment.image(paragraph.attributedString) != nil {
1344 return ImageFragment(textElement: textElement, range: range)
1345 }
1268 if let paragraph = textElement as? NSTextParagraph, EntityFragment.hasEntities(paragraph.attributedString) { 1346 if let paragraph = textElement as? NSTextParagraph, EntityFragment.hasEntities(paragraph.attributedString) {
1269 return EntityFragment(textElement: textElement, range: range) 1347 return EntityFragment(textElement: textElement, range: range)
1270 } 1348 }
@@ -1297,6 +1375,47 @@ final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable {
1297extension NSAttributedString.Key { 1375extension NSAttributedString.Key {
1298 /// The character an entity shows as, on the entity's last character. 1376 /// The character an entity shows as, on the entity's last character.
1299 static let orgEntityDisplay = NSAttributedString.Key("OrgEntityDisplay") 1377 static let orgEntityDisplay = NSAttributedString.Key("OrgEntityDisplay")
1378 /// The image an image link's line shows under it.
1379 static let orgImage = NSAttributedString.Key("OrgImage")
1380}
1381
1382final class InlineImageBox: NSObject, @unchecked Sendable {
1383 let image: NSImage
1384 let size: CGSize
1385 init(image: NSImage, size: CGSize) {
1386 self.image = image
1387 self.size = size
1388 }
1389}
1390
1391/// An image link's line drawn as the image, with the paragraph spacing making room for its
1392/// height.
1393final class ImageFragment: NSTextLayoutFragment, @unchecked Sendable {
1394 static func image(_ string: NSAttributedString) -> InlineImageBox? {
1395 var found: InlineImageBox?
1396 string.enumerateAttribute(.orgImage, in: NSRange(location: 0, length: string.length)) { value, _, stop in
1397 if let box = value as? InlineImageBox { found = box; stop.pointee = true }
1398 }
1399 return found
1400 }
1401
1402 private var frame: CGRect? {
1403 guard let box = (textElement as? NSTextParagraph).flatMap({ Self.image($0.attributedString) }), let line = textLineFragments.first else { return nil }
1404 return CGRect(x: line.typographicBounds.minX, y: line.typographicBounds.minY + 2, width: box.size.width, height: box.size.height)
1405 }
1406
1407 override var renderingSurfaceBounds: CGRect {
1408 frame.map { super.renderingSurfaceBounds.union($0) } ?? super.renderingSurfaceBounds
1409 }
1410
1411 override func draw(at point: CGPoint, in context: CGContext) {
1412 super.draw(at: point, in: context)
1413 guard let box = (textElement as? NSTextParagraph).flatMap({ Self.image($0.attributedString) }), let frame else { return }
1414 NSGraphicsContext.saveGraphicsState()
1415 NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
1416 box.image.draw(in: frame.offsetBy(dx: point.x, dy: point.y), from: .zero, operation: .sourceOver, fraction: 1, respectFlipped: true, hints: nil)
1417 NSGraphicsContext.restoreGraphicsState()
1418 }
1300} 1419}
1301 1420
1302/// A paragraph with pretty entities: each is drawn as its character where the entity's last 1421/// A paragraph with pretty entities: each is drawn as its character where the entity's last
Sources/Orgstar/ContentView.swift +2 −1
@@ -39,6 +39,7 @@ struct ContentView: View {
39 @AppStorage("logReschedule") private var logReschedule = "nil" 39 @AppStorage("logReschedule") private var logReschedule = "nil"
40 @AppStorage("logRedeadline") private var logRedeadline = "nil" 40 @AppStorage("logRedeadline") private var logRedeadline = "nil"
41 @AppStorage("useSpeedCommands") private var useSpeedCommands = false 41 @AppStorage("useSpeedCommands") private var useSpeedCommands = false
42 @AppStorage("startupWithInlineImages") private var startupWithInlineImages = false
42 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true 43 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true
43 @AppStorage("hideBlockStartup") private var hideBlockStartup = false 44 @AppStorage("hideBlockStartup") private var hideBlockStartup = false
44 45
@@ -47,7 +48,7 @@ struct ContentView: View {
47 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists, 48 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists,
48 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer, 49 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer,
49 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline, 50 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline,
50 useSpeedCommands: useSpeedCommands 51 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages
51 ) 52 )
52 } 53 }
53 /// A block being edited apart (`org-edit-special`). 54 /// A block being edited apart (`org-edit-special`).
Sources/Orgstar/EditorView.swift +1
@@ -86,6 +86,7 @@ struct EditorView: NSViewRepresentable {
86 let editor = OrgEditor(document: document, frame: container.bounds, editable: document.isEditable, highlighter: Self.highlighter, mode: mode, options: options) 86 let editor = OrgEditor(document: document, frame: container.bounds, editable: document.isEditable, highlighter: Self.highlighter, mode: mode, options: options)
87 let path = session.path ?? "" 87 let path = session.path ?? ""
88 editor.onChange = { [weak session] in session?.bufferChanged(path) } 88 editor.onChange = { [weak session] in session?.bufferChanged(path) }
89 if !path.isEmpty { editor.baseDirectory = URL(fileURLWithPath: path).deletingLastPathComponent() }
89 editor.onMessage = { [weak commands] in commands?.show($0) } 90 editor.onMessage = { [weak commands] in commands?.show($0) }
90 editor.onKeysPending = { [weak commands] keys, next in 91 editor.onKeysPending = { [weak commands] keys, next in
91 commands?.pendingKeys = keys 92 commands?.pendingKeys = keys
Sources/Orgstar/Preferences.swift +2 −2
@@ -18,7 +18,7 @@ enum Preferences {
18 static func options( 18 static func options(
19 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, 19 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int,
20 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String, 20 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String,
21 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool 21 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool, startupWithInlineImages: Bool
22 ) -> EditingOptions { 22 ) -> EditingOptions {
23 EditingOptions( 23 EditingOptions(
24 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, 24 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree,
@@ -27,7 +27,7 @@ enum Preferences {
27 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer, 27 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer,
28 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, 28 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup,
29 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline), 29 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline),
30 useSpeedCommands: useSpeedCommands 30 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages
31 ) 31 )
32 } 32 }
33} 33}
Tests/OrgCoreTests/InlineImagesTests.swift added +16
@@ -0,0 +1,16 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct InlineImagesTests {
6 @Test func findsImageLines() {
7 let text = "#+STARTUP: inlineimages\n* Images\n[[file:pic.png]]\n#+NAME: x\n#+ATTR_ORG: :width 60\n[[./pic.png]]\n[[pic.png][desc]]\nAfter [[pic.png]] text\n[[https://x.org/a.png]]\n" as NSString
8 func at(_ s: String) -> InlineImages.Image? { InlineImages.image(atLine: text.range(of: s).location, in: text) }
9 #expect(at("[[file:pic") == InlineImages.Image(path: "pic.png", width: nil))
10 #expect(at("[[./pic") == InlineImages.Image(path: "./pic.png", width: 60))
11 #expect(at("[[pic.png][") == nil && at("After") == nil && at("[[https") == nil)
12 #expect(InlineImages.startup(OrgParser.parse(text as String)) == true)
13 #expect(InlineImages.startup(OrgParser.parse("#+STARTUP: overview noinlineimages\n")) == false)
14 #expect(InlineImages.startup(OrgParser.parse("* x\n")) == nil)
15 }
16}