Inline images !95
11 files changed, +196 −5
Layout: unified · split
Sources/OrgApp/EmacsImport.swift +1 −1
| @@ -448,7 +448,7 @@ public struct EmacsImport: Sendable { | ||
| 448 | 448 | guard let n = value.integer else { return note("not a number") } |
| 449 | 449 | add(name, .integer(n)) |
| 450 | 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 | 452 | add(name, .bool(truthy)) |
| 453 | 453 | case "org-hide-drawer-startup", "org-hide-block-startup": |
| 454 | 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 | 82 | fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""), |
| 83 | 83 | Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""), |
| 84 | 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 | 87 | Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false), |
| 86 | 88 | comment: "single keys at the start of a heading line run commands (n, p, t, c, …)", legacy: ""), |
| 87 | 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 | 24 | public var fastTodoSelection: Bool |
| 25 | 25 | /// `org-use-speed-commands`: single keys at the start of a heading line run commands. |
| 26 | 26 | public var useSpeedCommands: Bool |
| 27 | /// `org-startup-with-inline-images`. | |
| 28 | public var startupWithInlineImages: Bool | |
| 27 | 29 | /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens. |
| 28 | 30 | public var hideDrawerStartup: Bool |
| 29 | 31 | /// `org-cycle-hide-block-startup`: blocks are folded when a file opens. |
| @@ -43,9 +45,10 @@ public struct EditingOptions: Sendable, Equatable, Codable { | ||
| 43 | 45 | hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false, |
| 44 | 46 | logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true, |
| 45 | 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 | 50 | self.useSpeedCommands = useSpeedCommands |
| 51 | self.startupWithInlineImages = startupWithInlineImages | |
| 49 | 52 | self.logReschedule = logReschedule |
| 50 | 53 | self.logRedeadline = logRedeadline |
| 51 | 54 | self.hideDrawerStartup = hideDrawerStartup |
Sources/OrgCore/Commands/InlineImages.swift added +45
| @@ -0,0 +1,45 @@ | ||
| 1 | import 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. | |
| 5 | public 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 | 732 | keys = "C-c C-e" |
| 733 | 733 | command = "app.export-dialog" |
| 734 | 734 | [[bind]] |
| 735 | keys = "C-c C-x C-v" | |
| 736 | command = "org.toggle-inline-images" | |
| 737 | [[bind]] | |
| 735 | 738 | keys = "C-c C-x C-c" |
| 736 | 739 | command = "org.columns" |
| 737 | 740 | [[bind]] |
Sources/OrgEditorAppKit/EditorCommands.swift +1
| @@ -41,6 +41,7 @@ public enum EditorCommands { | ||
| 41 | 41 | EditorCommand(id: "org.narrow.block", title: "Narrow to Block", action: .view), |
| 42 | 42 | EditorCommand(id: "org.widen", title: "Widen", action: .view), |
| 43 | 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 | 45 | EditorCommand(id: "org.table.toggle-column-width", title: "Shrink or Expand Table Column", action: .view), |
| 45 | 46 | EditorCommand(id: "org.table.shrink", title: "Shrink Table Columns with Widths", action: .view), |
| 46 | 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 | 151 | startup.wrapperFolds = Set(wrappers.map(\.start)) |
| 152 | 152 | if startup != FoldState() { setFolds(startup, wrappers: wrappers) } |
| 153 | 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 | 407 | case "org.cycle": cycleAtCaret() |
| 407 | 408 | case "org.cycle-global": cycleGlobal() |
| 408 | 409 | case "org.narrow.subtree", "org.narrow.block", "org.narrow.toggle", "org.widen": narrow(id) |
| 410 | case "org.toggle-inline-images": toggleImages() | |
| 409 | 411 | default: tableColumnWidth(id) |
| 410 | 412 | } |
| 411 | 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 | 685 | // MARK: - Narrowing |
| 612 | 686 | |
| 613 | 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 | 972 | if !showMarkup, options.prettyEntities { prettify(range, in: storage, revealed: revealed, scriptExclusions: scriptExclusions) } |
| 899 | 973 | applyShrunk(range, in: storage) |
| 900 | 974 | applyHighlights(range, in: storage) |
| 975 | applyImages(range, in: storage, revealed: revealed) | |
| 901 | 976 | } |
| 902 | 977 | |
| 903 | 978 | /// `org-pretty-entities`: entities drawn as their character over the entity's last |
| @@ -1265,6 +1340,9 @@ extension OrgEditor: NSTextLayoutManagerDelegate { | ||
| 1265 | 1340 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
| 1266 | 1341 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
| 1267 | 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 | 1346 | if let paragraph = textElement as? NSTextParagraph, EntityFragment.hasEntities(paragraph.attributedString) { |
| 1269 | 1347 | return EntityFragment(textElement: textElement, range: range) |
| 1270 | 1348 | } |
| @@ -1297,6 +1375,47 @@ final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { | ||
| 1297 | 1375 | extension NSAttributedString.Key { |
| 1298 | 1376 | /// The character an entity shows as, on the entity's last character. |
| 1299 | 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 | ||
| 1382 | final 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. | |
| 1393 | final 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 | 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 | 39 | @AppStorage("logReschedule") private var logReschedule = "nil" |
| 40 | 40 | @AppStorage("logRedeadline") private var logRedeadline = "nil" |
| 41 | 41 | @AppStorage("useSpeedCommands") private var useSpeedCommands = false |
| 42 | @AppStorage("startupWithInlineImages") private var startupWithInlineImages = false | |
| 42 | 43 | @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true |
| 43 | 44 | @AppStorage("hideBlockStartup") private var hideBlockStartup = false |
| 44 | 45 | |
| @@ -47,7 +48,7 @@ struct ContentView: View { | ||
| 47 | 48 | tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists, |
| 48 | 49 | fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer, |
| 49 | 50 | hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline, |
| 50 | useSpeedCommands: useSpeedCommands | |
| 51 | useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages | |
| 51 | 52 | ) |
| 52 | 53 | } |
| 53 | 54 | /// A block being edited apart (`org-edit-special`). |
Sources/Orgstar/EditorView.swift +1
| @@ -86,6 +86,7 @@ struct EditorView: NSViewRepresentable { | ||
| 86 | 86 | let editor = OrgEditor(document: document, frame: container.bounds, editable: document.isEditable, highlighter: Self.highlighter, mode: mode, options: options) |
| 87 | 87 | let path = session.path ?? "" |
| 88 | 88 | editor.onChange = { [weak session] in session?.bufferChanged(path) } |
| 89 | if !path.isEmpty { editor.baseDirectory = URL(fileURLWithPath: path).deletingLastPathComponent() } | |
| 89 | 90 | editor.onMessage = { [weak commands] in commands?.show($0) } |
| 90 | 91 | editor.onKeysPending = { [weak commands] keys, next in |
| 91 | 92 | commands?.pendingKeys = keys |
Sources/Orgstar/Preferences.swift +2 −2
| @@ -18,7 +18,7 @@ enum Preferences { | ||
| 18 | 18 | static func options( |
| 19 | 19 | tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, |
| 20 | 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 | 22 | ) -> EditingOptions { |
| 23 | 23 | EditingOptions( |
| 24 | 24 | tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, |
| @@ -27,7 +27,7 @@ enum Preferences { | ||
| 27 | 27 | logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer, |
| 28 | 28 | hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, |
| 29 | 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 @@ | ||
| 1 | import Foundation | |
| 2 | import Testing | |
| 3 | @testable import OrgCore | |
| 4 | ||
| 5 | struct 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 | } | |