Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ It provides a user-friendly editor for creating complex email filtering rules wi
- 📚 Palette of commands, including core Sieve and provider-specific extensions
- 🖱️ Drag & drop support for reordering and nesting commands
- 🔄 Rendering of the visual script into valid Sieve source code
- 🛠️ Extensible architecture – easily add new commands (see [How To Add New Command](Source/Sieve%20Commands/HowToAddNewCommand.md))
- 🛠️ Extensible architecture – easily add new commands (see [How To Add New Command](Source/Yasba/Sieve%20Commands/HowToAddNewCommand.md))

## Architecture Overview

Expand All @@ -40,7 +40,7 @@ Yasba is designed with extensibility in mind. The main architectural components

Contributions are welcome!

Check out [How To Add New Command](HowToAddNewCommand.md) to learn how to extend Yasba with new Sieve commands.
Check out [How To Add New Command](Source/Yasba/Sieve%20Commands/HowToAddNewCommand.md) to learn how to extend Yasba with new Sieve commands.

## License

Expand Down
10 changes: 10 additions & 0 deletions Source/Tests/Sieve Commands/AddFlag/AddFlagCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Testing
@testable import Yasba

struct AddFlagCommandTests {
@Test func requirements() throws {
let command = AddFlagCommand(tag: "tag")

#expect(command.requirements == ["imap4flags"])
}
}
10 changes: 10 additions & 0 deletions Source/Tests/Sieve Commands/FileInto/FileIntoCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Testing
@testable import Yasba

struct FileIntoCommandTests {
@Test func requirements() throws {
let command = FileIntoCommand(mailbox: "inbox")

#expect(command.requirements == ["fileinto"])
}
}
16 changes: 16 additions & 0 deletions Source/Tests/Sieve Commands/IfCommand/IfCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Testing
@testable import Yasba

struct IfCommandTests {
@Test func childernRequirementsArePassed() throws {
let command = IfCommand(
quantifier: .any,
tests: [ ],
thenChildren: [ DummyLeaf("do1") ],
elseChildren: [ DummyLeaf("do2") ]
)

#expect(command.requirements == [DummyLeaf.requirement, DummyLeaf.requirement])
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Testing
@testable import Yasba

struct ProtonExpireCommandTests {
@Test func requirements() throws {
let command = ProtonExpireCommand(days: 1)

#expect(command.requirements == ["vnd.proton.expire"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Testing
@testable import Yasba

struct ProtonMarkAsReadCommandTests {
@Test func requirements() throws {
let command = ProtonMarkAsReadCommand()

#expect(command.requirements == ["imap4flags"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Testing
@testable import Yasba

struct ProtonSpamCommandTests {
@Test func requirements() throws {
let command = ProtonSpamCommand()

#expect(command.requirements == ["include", "environment", "variables", "relational", "comparator-i;ascii-numeric", "spamtest"])
}
}
5 changes: 3 additions & 2 deletions Source/Tests/TestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import Testing

/// Minimal non-container command used across tests. Identity is stable via `id`.
final class DummyLeaf: SieveCommand, SieveCommandValueEquatable {
static let requirement: String = "requirement"

let id: UUID
var isContainer: Bool { false }
let text: String

init(_ text: String, id: UUID = UUID()) {
self.text = text
self.id = id
}

var requirements: [String] { [] }
var requirements: [String] { [DummyLeaf.requirement] }

func isSemanticallyEqual(to other: any SieveCommand) -> Bool {
guard let other = other as? DummyLeaf else { return false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import Foundation
@Observable
final class AddFlagCommand: SieveCommand, SieveCommandValueEquatable {
let id = UUID()

var tag: String
var isContainer: Bool { false }
let requirements = ["imap4flags"]

init(tag: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import Foundation
@Observable
final class FileIntoCommand: SieveCommand, SieveCommandValueEquatable{
let id = UUID()

var mailbox: String
var isContainer: Bool { false }
var requirement: String = "fileinto"
let requirements = ["fileinto"]

init(mailbox: String) {
self.mailbox = mailbox
Expand Down
7 changes: 4 additions & 3 deletions Source/Yasba/Sieve Commands/Core/If/IfCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class IfCommand: SieveContainerCommand, SieveCommandValueEquatable {
var tests: [SieveTest]
var thenChildren: [AnySieveCommand]
var elseChildren: [AnySieveCommand]

var requirements: [String] {
return (thenChildren + elseChildren).flatMap { $0.requirements }
}

init(
quantifier: IfQuantifier = .any,
Expand All @@ -37,9 +41,6 @@ final class IfCommand: SieveContainerCommand, SieveCommandValueEquatable {
self.elseChildren = elseChildren
}

var displayName: String { "If" }
var isContainer: Bool { true }

func isSemanticallyEqual(to other: AnySieveCommand) -> Bool {
guard let other = other as? IfCommand else { return false }
return quantifier == other.quantifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ fileprivate enum TestTemplate: String, CaseIterable, Identifiable {
case address
case envelope
case size
case body
case exists

var id: String { rawValue }
Expand All @@ -143,7 +142,6 @@ fileprivate enum TestTemplate: String, CaseIterable, Identifiable {
case .address: return "Address"
case .envelope: return "Envelope"
case .size: return "Size"
case .body: return "Body"
case .exists: return "Exists"
}
}
Expand All @@ -158,8 +156,6 @@ fileprivate enum TestTemplate: String, CaseIterable, Identifiable {
return .envelope(part: .all, names: ["from"], match: .contains, keys: [""])
case .size:
return .size(.over, bytes: 1024)
case .body:
return .exists(["Subject"])
case .exists:
return .exists(["Subject"])
}
Expand Down
1 change: 0 additions & 1 deletion Source/Yasba/Sieve Commands/Core/Stop/StopCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Foundation
@Observable
final class StopCommand: SieveCommand, SieveCommandValueEquatable {
let id = UUID()
var isContainer: Bool { return false }

func isSemanticallyEqual(to other: AnySieveCommand) -> Bool {
return other is StopCommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import Foundation
@Observable
final class ProtonExpireCommand: SieveCommand, SieveCommandValueEquatable {
let id = UUID()

var days: Int
var isContainer: Bool { false }
let requirements = ["imap4flags"]
let requirements = ["vnd.proton.expire"]

init(days: Int) {
self.days = days
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import Foundation
@Observable
final class ProtonMarkAsReadCommand: SieveCommand, SieveCommandValueEquatable {
let id = UUID()

var isContainer: Bool { false }
let requirements = ["imap4flags"]

func isSemanticallyEqual(to other: AnySieveCommand) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import Foundation
@Observable
final class ProtonSpamCommand: SieveCommand, SieveCommandValueEquatable {
let id = UUID()

var isContainer: Bool { false }
let requirements = ["include", "environment", "variables", "relational", "comparator-i;ascii-numeric", "spamtest"]

func isSemanticallyEqual(to other: AnySieveCommand) -> Bool {
Expand Down
1 change: 0 additions & 1 deletion Source/Yasba/Sieve Commands/SieveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import Foundation
*/
protocol SieveCommand: AnyObject, Identifiable {
var id: UUID { get }
var isContainer: Bool { get }

var requirements: [String] { get }
}
Expand Down
7 changes: 7 additions & 0 deletions Source/Yasba/Views/Sieve Script/Row Token/RowToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ enum RowToken: Identifiable, Equatable {
static func == (lhs: RowToken, rhs: RowToken) -> Bool {
lhs.id == rhs.id
}

var canRemove: Bool {
switch self {
case .elseMarker, .endIf: return false
default: return true
}
}
}
10 changes: 5 additions & 5 deletions Source/Yasba/Views/Sieve Script/SieveScriptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ struct SieveScriptView: View {
}

var placeholderInsertPosition: Int? {
guard let g = dropGapIndex else { return nil }
if let pos = filteredIndices.firstIndex(where: { $0 >= g }) { return pos }
guard let gap = dropGapIndex else { return nil }
if let pos = filteredIndices.firstIndex(where: { $0 >= gap }) { return pos }
return filteredIndices.count
}

var placeholderLeading: CGFloat {
guard let g = dropGapIndex else { return 0 }
if let index = tokens.indices.first(where: { $0 >= g }) { return CGFloat(indents[index]) * 24 }
guard let gap = dropGapIndex else { return 0 }
if let index = tokens.indices.first(where: { $0 >= gap }) { return CGFloat(indents[index]) * 24 }
return CGFloat(indents.last ?? 0) * 24
}

Expand Down Expand Up @@ -139,7 +139,7 @@ struct SieveScriptView: View {
RowTokenToViewMapper.row(for: token)
Spacer(minLength: 0)
}
if hoveredRow == index {
if hoveredRow == index && token.canRemove {
Button {
viewModel.remove(at: index)
hoveredRow = nil
Expand Down
Loading