Commit 45a41404 authored by tbren's avatar tbren
Browse files

Refactoring and improving CVU lookup. Initial testing of UINode

parent 7b18edf7
Showing with 623 additions and 216 deletions
+623 -216
......@@ -47,6 +47,11 @@
D7CA93882588611A00A36DD0 /* example in Resources */ = {isa = PBXBuildFile; fileRef = D7CA93872588611A00A36DD0 /* example */; };
D7CA938D2588612200A36DD0 /* ExprInterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CA938C2588612200A36DD0 /* ExprInterpreterTests.swift */; };
D7E89BF725A4154C003AEFD8 /* CVUParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E89BF625A4154C003AEFD8 /* CVUParserTests.swift */; };
D7E89C0525A6BEEA003AEFD8 /* CVUElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E89C0425A6BEEA003AEFD8 /* CVUElementView.swift */; };
D7E89C0A25A6D372003AEFD8 /* CVU_Stack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E89C0925A6D372003AEFD8 /* CVU_Stack.swift */; };
D7E89C1325A6EC19003AEFD8 /* UINodeResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E89C1225A6EC19003AEFD8 /* UINodeResolver.swift */; };
D7E89C1B25A7054C003AEFD8 /* HTMLHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E89C1A25A7054C003AEFD8 /* HTMLHelper.swift */; };
D7E89C2125A70588003AEFD8 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D7E89C2025A70588003AEFD8 /* SwiftSoup */; };
D7EBBBE02586CD1C00A343E6 /* AppController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EBBBDF2586CD1C00A343E6 /* AppController.swift */; };
D7EBBBEA2586CE5100A343E6 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = D7EBBBE92586CE5100A343E6 /* KeychainAccess */; };
D7EBBBEF2586CE7100A343E6 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EBBBEE2586CE7100A343E6 /* UIConstants.swift */; };
......@@ -137,6 +142,10 @@
D7CA93872588611A00A36DD0 /* example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = example; sourceTree = "<group>"; };
D7CA938C2588612200A36DD0 /* ExprInterpreterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExprInterpreterTests.swift; sourceTree = "<group>"; };
D7E89BF625A4154C003AEFD8 /* CVUParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVUParserTests.swift; sourceTree = "<group>"; };
D7E89C0425A6BEEA003AEFD8 /* CVUElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVUElementView.swift; sourceTree = "<group>"; };
D7E89C0925A6D372003AEFD8 /* CVU_Stack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVU_Stack.swift; sourceTree = "<group>"; };
D7E89C1225A6EC19003AEFD8 /* UINodeResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINodeResolver.swift; sourceTree = "<group>"; };
D7E89C1A25A7054C003AEFD8 /* HTMLHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLHelper.swift; sourceTree = "<group>"; };
D7EBBBDF2586CD1C00A343E6 /* AppController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppController.swift; sourceTree = "<group>"; };
D7EBBBEE2586CE7100A343E6 /* UIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = "<group>"; };
D7EBBBF32586CE7F00A343E6 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
......@@ -185,6 +194,7 @@
buildActionMask = 2147483647;
files = (
D7A4D0F3258C321900CADC75 /* Alamofire in Frameworks */,
D7E89C2125A70588003AEFD8 /* SwiftSoup in Frameworks */,
D7EBBBEA2586CE5100A343E6 /* KeychainAccess in Frameworks */,
D714F28A255AAD890072FE97 /* GRDB in Frameworks */,
);
......@@ -209,8 +219,11 @@
D714B16B257DD4870027ECF3 /* CVUValue.swift */,
D714B166257DD37D0027ECF3 /* CVUContext.swift */,
D7EBBC652586FA5400A343E6 /* UINode.swift */,
D7E89C1225A6EC19003AEFD8 /* UINodeResolver.swift */,
D7E89C0425A6BEEA003AEFD8 /* CVUElementView.swift */,
D7EBBC6A2586FBCE00A343E6 /* ViewArguments.swift */,
D732D98B25931B4500522C71 /* CVULookupController.swift */,
D7E89C0E25A6D376003AEFD8 /* CVUElements */,
);
path = CVU;
sourceTree = "<group>";
......@@ -253,6 +266,7 @@
D7EBBC0E2586CFAE00A343E6 /* SwiftUI.View.swift */,
D7EBBC1B2586D00F00A343E6 /* UIView+NextResponder.swift */,
D7EBBC522586F70B00A343E6 /* StringError.swift */,
D7E89C1A25A7054C003AEFD8 /* HTMLHelper.swift */,
);
path = Extensions;
sourceTree = "<group>";
......@@ -379,6 +393,14 @@
path = Database;
sourceTree = "<group>";
};
D7E89C0E25A6D376003AEFD8 /* CVUElements */ = {
isa = PBXGroup;
children = (
D7E89C0925A6D372003AEFD8 /* CVU_Stack.swift */,
);
path = CVUElements;
sourceTree = "<group>";
};
D7EBBBDE2586CD0800A343E6 /* Controllers */ = {
isa = PBXGroup;
children = (
......@@ -517,6 +539,7 @@
D714F289255AAD890072FE97 /* GRDB */,
D7EBBBE92586CE5100A343E6 /* KeychainAccess */,
D7A4D0F2258C321900CADC75 /* Alamofire */,
D7E89C2025A70588003AEFD8 /* SwiftSoup */,
);
productName = MemriDatabase;
productReference = D7F136F4254E645D00881B25 /* MemriDatabase.app */;
......@@ -577,6 +600,7 @@
D714F288255AAD890072FE97 /* XCRemoteSwiftPackageReference "GRDB" */,
D7EBBBE82586CE5100A343E6 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
D7A4D0F1258C321900CADC75 /* XCRemoteSwiftPackageReference "Alamofire" */,
D7E89C1F25A70588003AEFD8 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
);
productRefGroup = D7F136F5254E645D00881B25 /* Products */;
projectDirPath = "";
......@@ -644,6 +668,7 @@
D745D1622575C39D006BC24D /* SyncController.swift in Sources */,
D7EBBC4E2586F6C900A343E6 /* CVULexer.swift in Sources */,
D7EBBC532586F70B00A343E6 /* StringError.swift in Sources */,
D7E89C0525A6BEEA003AEFD8 /* CVUElementView.swift in Sources */,
D71F25C8258EF5AA0070D4EC /* AnyDecodable.swift in Sources */,
D7EBBC212586D05F00A343E6 /* UIHostingControllerNoSafeArea.swift in Sources */,
D7EBBC162586CFD200A343E6 /* KeyboardToolbar.swift in Sources */,
......@@ -662,11 +687,13 @@
D7F136F8254E645D00881B25 /* AppDelegate.swift in Sources */,
D7EBBBE02586CD1C00A343E6 /* AppController.swift in Sources */,
D7EBBC662586FA5400A343E6 /* UINode.swift in Sources */,
D7E89C0A25A6D372003AEFD8 /* CVU_Stack.swift in Sources */,
D7699CCD2590305A00F62AB3 /* ItemRecord.swift in Sources */,
D714B15C257DC1120027ECF3 /* CVUController.swift in Sources */,
D7EBBC0A2586CEFE00A343E6 /* SceneView.swift in Sources */,
D73A8063258C910D00F416D1 /* PodAPIRequests.swift in Sources */,
D73BA2532574CA2F0025C753 /* DemoData.swift in Sources */,
D7E89C1325A6EC19003AEFD8 /* UINodeResolver.swift in Sources */,
D7EBBC472586F6C900A343E6 /* CVUStringConvertible.swift in Sources */,
D71F25C7258EF5AA0070D4EC /* AnyCodable.swift in Sources */,
D7EBBC022586CEB600A343E6 /* AuthenticationScreen.swift in Sources */,
......@@ -677,6 +704,7 @@
D7EBBBEF2586CE7100A343E6 /* UIConstants.swift in Sources */,
D7EBBC6B2586FBCE00A343E6 /* ViewArguments.swift in Sources */,
D7EBBC482586F6C900A343E6 /* CVUParsedDefinition.swift in Sources */,
D7E89C1B25A7054C003AEFD8 /* HTMLHelper.swift in Sources */,
D76EA501255DF01500AE2E20 /* DatabaseTypes.swift in Sources */,
D7F136FA254E645D00881B25 /* SceneDelegate.swift in Sources */,
D71F25B3258EE7150070D4EC /* FileHelper.swift in Sources */,
......@@ -1031,6 +1059,14 @@
minimumVersion = 5.4.0;
};
};
D7E89C1F25A70588003AEFD8 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.3.2;
};
};
D7EBBBE82586CE5100A343E6 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess";
......@@ -1052,6 +1088,11 @@
package = D7A4D0F1258C321900CADC75 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
D7E89C2025A70588003AEFD8 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = D7E89C1F25A70588003AEFD8 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
D7EBBBE92586CE5100A343E6 /* KeychainAccess */ = {
isa = XCSwiftPackageProductDependency;
package = D7EBBBE82586CE5100A343E6 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
......
......@@ -27,6 +27,15 @@
"revision": "654d52d30f3dd4592e944c3e0bccb53178c992f6",
"version": "4.2.1"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup",
"state": {
"branch": null,
"revision": "774dc9c7213085db8aa59595e27c1cd22e428904",
"version": "2.3.2"
}
}
]
},
......
......@@ -75,7 +75,7 @@ EmailMessage[] {
}
Text {
text: "{.content.plainString}"
text: "{.content.plainString()}"
lineLimit: 2
removeWhiteSpace: true
maxChar: 100
......
......@@ -89,7 +89,7 @@ Message[] {
[renderer = list] {
spacing: 8
Text {
text: {{.content.plainString}}
text: {{.content.plainString()}}
}
}
......@@ -134,7 +134,7 @@ Message[] {
}
SmartText {
text: "{.message.photo ? '[Photo] ' : ''}{.message.content.plainString}"
text: "{.message.photo ? '[Photo] ' : ''}{.message.content.plainString()}"
color: secondary
font: 12 regular
padding: 6
......@@ -174,7 +174,7 @@ Message[] {
SmartText {
show: {{.content}}
text: {{.content.plainString}}
text: {{.content.plainString()}}
font: 16 regular
color: primary
maxLines: 100
......
......@@ -117,7 +117,7 @@ Note[] {
alignment: left
spacing: 5
Text {
text: {{.title.plainString or "Untitled"}}
text: {{.title.plainString() or "Untitled"}}
lineLimit: 1
font: 18 semibold
color: primary
......@@ -132,7 +132,7 @@ Note[] {
}
}
Text {
text: "{.content.plainString}"
text: "{.content.plainString()}"
lineLimit: 1
removeWhiteSpace: true
maxChar: 100
......@@ -159,7 +159,7 @@ Note[] {
alignment: center
Text {
text: "{.content.plainString}"
text: "{.content.plainString()}"
allowNil: true
maxChar: 100
color: #333
......@@ -210,7 +210,7 @@ Note[] {
TimelineItem {
icon: "square.and.pencil"
title: {{.title.plainString or "Untitled"}}
text: {{.content.plainString}}
text: {{.content.plainString()}}
}
}
}
......@@ -15,11 +15,26 @@ struct CVUContext {
/// This protocol defines the requirements for CVU-lookup functionality (eg. named variables)
protocol CVULookup {
/// Lookup a variable using its CVU string and return the value as a double
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> Double?
func resolve(value: CVUValue, context: CVUContext, db: Database) -> Double?
/// Lookup a variable using its CVU string and return the value as a string
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> String?
func resolve(value: CVUValue, context: CVUContext, db: Database) -> String?
/// Lookup a variable using its CVU string and return the value as a bool
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> Bool?
func resolve(value: CVUValue, context: CVUContext, db: Database) -> Bool?
/// Lookup a variable using its CVU string and return the value as an ItemRecord
func resolve(value: CVUValue, context: CVUContext, db: Database) -> ItemRecord?
/// Lookup a variable using its CVU string and return the value as an array of ItemRecords
func resolve(value: CVUValue, context: CVUContext, db: Database) -> [ItemRecord]
/// Lookup an edge from an ItemRecord
func resolve(edge: String, for item: ItemRecord, db: Database) -> ItemRecord?
/// Lookup an edge array from an ItemRecord
func resolve(edge: String, for item: ItemRecord, db: Database) -> [ItemRecord]
/// Lookup a property from an ItemRecord
func resolve(property: String, for item: ItemRecord, db: Database) -> PropertyDatabaseValue?
}
......@@ -6,18 +6,18 @@
//
import Foundation
import GRDB
class CVUController {
var definitions: [CVUParsedDefinition]
init() {
// do {
// definitions = try CVUController.parseCVU()
// } catch {
// print(error)
// definitions = []
// }
definitions = []
do {
definitions = try CVUController.parseCVU()
} catch {
print(error)
definitions = []
}
}
static func parseCVU() throws -> [CVUParsedDefinition] {
......@@ -47,10 +47,64 @@ class CVUController {
return try? String(contentsOf: $0)
}.joined(separator: "\n")
}
func definitionFor(type: CVUDefinitionType, selector: String? = nil, name: String? = nil) -> CVUParsedDefinition? {
CVUController.definitionFrom(definitions: definitions, type: type, selector: selector, name: name)
}
func rendererDefinitionFor(selector: String, rendererName: String) -> CVUParsedDefinition? {
guard let definition = definitionFor(type: .view, selector: selector) else { return nil }
return CVUController.definitionFrom(definitions: definition.parsed.definitions, type: .renderer, name: rendererName)
}
static func definitionFrom(definitions: [CVUParsedDefinition], type: CVUDefinitionType, selector: String? = nil, name: String? = nil) -> CVUParsedDefinition? {
let relevantDefinitions = definitions.filter { (def) -> Bool in
// Find a definition of the requested type
guard def.type == type else { return false }
// with matching name
if let name = name {
guard def.name == name else {
return false
}
}
// with matching selector or wildcard selector
if let selector = selector {
guard (def.selector == selector || def.selector == "*" || def.selector == nil) else {
return false
}
}
return true
}.sorted { (lhs, rhs) -> Bool in
if lhs.selector == "*" || lhs.selector == nil {
return true
} else if rhs.selector == "*" || rhs.selector == nil {
return false
} else {
// TODO: Improve this VERY crude way of determining selector specificity
return (lhs.selector?.count ?? 0) <= (rhs.selector?.count ?? 0)
}
}
guard let firstDefinition = relevantDefinitions.first else { return nil }
let mergedDefinition = relevantDefinitions.dropFirst().reduce(firstDefinition) { (old, latest) in
old.merge(with: latest)
}
return mergedDefinition
}
}
struct CVUView {
import SwiftUI
extension CVUController {
@ViewBuilder
func render(context: CVUContext, lookup: CVULookup) -> some View {
if let currentItem = context.currentItem,
let renderer = rendererDefinitionFor(selector: "\(currentItem.type)[]", rendererName: "list"),
let node = renderer.parsed.children.first {
CVUElementView(info: .init(context: context, lookup: lookup, node: node))
}
}
}
//
// CVUElementView.swift
// MemriDatabase
//
// Created by T Brennan on 7/1/21.
//
import SwiftUI
import GRDB
struct CVUElementView: View {
var info: UINodeResolver
@ViewBuilder
func resolvedComponent(db: Database) -> some View {
switch info.node.type {
case .HStack:
CVU_HStack(info: info)
case .VStack:
CVU_VStack(info: info)
case .ZStack:
CVU_ZStack(info: info)
case .Text:
if let text = info.resolver(for: "text", db: db)?.string {
Text(text)
}
case .Image:
if let resolver = info.resolver(for: "image", db: db),
let file = resolver.edge("file"),
let name = resolver.property(of: file, name: "filename")?.asString() {
Text(name)
}
default:
Text(info.node.type.rawValue)
}
}
@ViewBuilder
public var body: some View {
AppController.shared.databaseController.read { (db) in
resolvedComponent(db: db)
}
}
}
//struct CVUElementView_Previews: PreviewProvider {
// static var previews: some View {
// CVUElementView(type: .Text)
// }
//}
//
// CVU_Stack.swift
// Copyright © 2020 memri. All rights reserved.
import SwiftUI
struct CVU_HStack: View {
var info: UINodeResolver
var body: some View {
HStack { //alignment: nodeResolver.alignment().vertical, spacing: nodeResolver.spacing.x)
info.childrenInForEach
}
// .if(nodeResolver.bool(for: "fillWidth", defaultValue: false)) {
// $0.frame(maxWidth: .infinity, alignment: nodeResolver.alignment())
// }
}
}
struct CVU_VStack: View {
var info: UINodeResolver
var body: some View {
VStack { //(alignment: nodeResolver.alignment().horizontal, spacing: nodeResolver.spacing.y)
info.childrenInForEach
}
// .if(nodeResolver.bool(for: "fillHeight", defaultValue: false)) {
// $0.frame(maxHeight: .infinity, alignment: nodeResolver.alignment())
// }
}
}
struct CVU_ZStack: View {
var info: UINodeResolver
var body: some View {
ZStack { //(alignment: nodeResolver.alignment())
info.childrenInForEach
}
}
}
......@@ -8,7 +8,7 @@
import Foundation
import GRDB
class CVULookupController: CVULookup {
struct CVULookupController: CVULookup {
init(schema: Schema) {
self.schema = schema
}
......@@ -28,10 +28,10 @@ class CVULookupController: CVULookup {
guard let currentItem = context.currentItem else { return nil }
currentValue = .items([currentItem])
case .function(let args):
switch node.name {
switch node.name.lowercased() {
case "joined":
guard case let .values(values) = currentValue else { return nil }
if let separator = args.first?.resolve(lookup: self, context: context, type: String.self, db: db) {
if let exp = args.first, let separator = resolve(expression: exp, context: context, type: String.self, db: db) {
let joined = values.compactMap { $0.asString() }.joined(separator: separator)
currentValue = .values([.string(joined)])
} else {
......@@ -39,6 +39,14 @@ class CVULookupController: CVULookup {
let joined = ListFormatter.localizedString(byJoining: strings)
currentValue = .values([.string(joined)])
}
case "plainstring":
guard case let .values(values) = currentValue else { return nil }
let stripped = values.compactMap { value -> PropertyDatabaseValue? in
guard let htmlstring = value.asString() else { return nil }
return .string(HTMLHelper.getPlainText(html: htmlstring))
}
currentValue = .values(stripped)
default: return nil
}
case .lookup(let subexpression):
......@@ -57,7 +65,7 @@ class CVULookupController: CVULookup {
guard let exp = subexpression else { return items }
return items.filter { item in
let context = CVUContext(currentItem: item)
return exp.resolve(lookup: self, context: context, db: db) ?? false
return resolve(expression: exp, context: context, db: db) ?? false
}
}
......@@ -109,6 +117,62 @@ class CVULookupController: CVULookup {
}
}
func resolve(value: CVUValue, context: CVUContext, db: Database) -> Double? {
switch value {
case .constant(let constant):
return constant.asNumber()
case .expression(let expression):
return resolve(expression: expression, context: context, db: db)
default:
return nil
}
}
func resolve(value: CVUValue, context: CVUContext, db: Database) -> String? {
switch value {
case .constant(let constant):
return constant.asString()
case .expression(let expression):
return resolve(expression: expression, context: context, db: db)
default:
return nil
}
}
func resolve(value: CVUValue, context: CVUContext, db: Database) -> Bool? {
switch value {
case .constant(let constant):
return constant.asBool()
case .expression(let expression):
return resolve(expression: expression, context: context, db: db)
default:
return nil
}
}
func resolve(value: CVUValue, context: CVUContext, db: Database) -> ItemRecord? {
switch value {
case .constant:
return nil
case .expression(let expression):
return resolve(expression: expression, context: context, db: db)
default:
return nil
}
}
func resolve(value: CVUValue, context: CVUContext, db: Database) -> [ItemRecord] {
switch value {
case .expression(let expression):
return resolve(expression: expression, context: context, db: db)
default:
return []
}
}
/// Lookup a variable using its CVU string and return the value as a double
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> Double? {
guard let lookupResult: LookupStep = resolve(nodes: nodes, context: context, db: db) else { return nil }
......@@ -142,6 +206,17 @@ class CVULookupController: CVULookup {
}
}
/// Lookup using a CVU expression string and return the value as an item
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> ItemRecord? {
guard let lookupResult: LookupStep = resolve(nodes: nodes, context: context, db: db) else { return nil }
switch lookupResult {
case .items(let items):
return items.first
default:
return nil
}
}
/// Lookup using a CVU expression string and return the value as an array of items
func resolve(nodes: [CVUValue.LookupNode], context: CVUContext, db: Database) -> [ItemRecord] {
guard let lookupResult: LookupStep = resolve(nodes: nodes, context: context, db: db) else { return [] }
......@@ -152,4 +227,197 @@ class CVULookupController: CVULookup {
return []
}
}
/// Lookup an edge from an ItemRecord
func resolve(edge: String, for item: ItemRecord, db: Database) -> ItemRecord? {
item.edgeItem(edge, db: db)
}
/// Lookup an edge array from an ItemRecord
func resolve(edge: String, for item: ItemRecord, db: Database) -> [ItemRecord] {
item.edgeItems(edge, db: db)
}
/// Lookup a property from an ItemRecord
func resolve(property: String, for item: ItemRecord, db: Database) -> PropertyDatabaseValue? {
item.property(property, db: db)?.value(itemType: item.type, schema: schema)
}
func resolve(expression: CVUValue.ExpressionNode, context: CVUContext, type: ItemRecord.Type = ItemRecord.self, db: Database) -> ItemRecord? {
switch expression {
case let .lookup(nodes):
return resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = resolve(expression: condition, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return resolve(expression: trueExp, context: context, db: db)
} else {
return resolve(expression: falseExp, context: context, db: db)
}
case let .or(a, b):
return resolve(expression: a, context: context, type: ItemRecord.self, db: db) ?? resolve(expression: b, context: context, db: db)
default:
print("CVU Expression: \(self) unsupported for resolving to string")
return nil
}
}
func resolve(expression: CVUValue.ExpressionNode, context: CVUContext, type: [ItemRecord].Type = [ItemRecord].self, db: Database) -> [ItemRecord] {
switch expression {
case let .lookup(nodes):
return resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = resolve(expression: condition, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return resolve(expression: trueExp, context: context, db: db)
} else {
return resolve(expression: falseExp, context: context, db: db)
}
case let .and(a, b):
return resolve(expression: a, context: context, type: [ItemRecord].self, db: db) + resolve(expression: b, context: context, db: db)
default:
print("CVU Expression: \(self) unsupported for resolving to string")
return []
}
}
func resolve(expression: CVUValue.ExpressionNode, context: CVUContext, type: Double.Type = Double.self, db: Database) -> Double? {
switch expression {
case let .lookup(nodes):
return resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = resolve(expression: condition, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return resolve(expression: trueExp, context: context, db: db)
} else {
return resolve(expression: falseExp, context: context, db: db)
}
case let .or(a, b):
return resolve(expression: a, context: context, db: db) ?? resolve(expression: b, context: context, db: db)
case .negation(_):
print("CVU Expression error: Should not use ! operator on non-boolean value")
return nil
case let .addition(a, b):
return (resolve(expression: a, context: context, db: db) ?? 0) + (resolve(expression: b, context: context, db: db) ?? 0)
case let .subtraction(a, b):
return (resolve(expression: a, context: context, db: db) ?? 0) - (resolve(expression: b, context: context, db: db) ?? 0)
case let .constant(const):
return const.asNumber()
case let .multiplication(a, b):
return (resolve(expression: a, context: context, db: db) ?? 0) * (resolve(expression: b, context: context, db: db) ?? 0)
case let .division(a, b):
guard let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db),
rhs != 0
else { return nil }
return lhs / rhs
default:
print("CVU Expression: \(self) unsupported for resolving to double")
return nil
}
}
func resolve(expression: CVUValue.ExpressionNode, context: CVUContext, type: String.Type = String.self, db: Database) -> String? {
switch expression {
case let .lookup(nodes):
return resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = resolve(expression: condition, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return resolve(expression: trueExp, context: context, db: db)
} else {
return resolve(expression: falseExp, context: context, db: db)
}
case let .or(a, b):
return resolve(expression: a, context: context, type: String.self, db: db)?.nilIfBlank ?? resolve(expression: b, context: context, db: db)
case .negation(_):
print("CVU Expression error: Should not use ! operator on non-boolean value")
return nil
case let .addition(a, b):
return (resolve(expression: a, context: context, db: db) ?? "") + (resolve(expression: b, context: context, db: db) ?? "")
case .subtraction(_, _):
print("CVU Expression error: Should not use - operator on string value")
return nil
case let .constant(const):
return const.asString()
case let .stringMode(nodes: nodes):
return nodes.compactMap { resolve(expression: $0, context: context, db: db) }.joined(separator: "")
default:
print("CVU Expression: \(self) unsupported for resolving to string")
return nil
}
}
func resolve(expression: CVUValue.ExpressionNode, context: CVUContext, type: Bool.Type = Bool.self, db: Database) -> Bool? {
switch expression {
case let .lookup(nodes):
return resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = resolve(expression: condition, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return resolve(expression: trueExp, context: context, db: db)
} else {
return resolve(expression: falseExp, context: context, db: db)
}
case let .and(a, b):
return (resolve(expression: a, context: context, db: db) ?? false) && (resolve(expression: b, context: context, db: db) ?? false)
case let .or(a, b):
return (resolve(expression: a, context: context, db: db) ?? false) || (resolve(expression: b, context: context, db: db) ?? false)
case let .negation(x):
return resolve(expression: x, context: context, type: Bool.self, db: db).map { !$0 }
case .addition(_ ,_):
print("CVU Expression error: Should not use + operator on bool value")
return nil
case .subtraction(_, _):
print("CVU Expression error: Should not use - operator on bool value")
return nil
case let .constant(const):
return const.asBool()
case let .lessThan(a, b):
guard let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db)
else { return nil }
return lhs < rhs
case let .greaterThan(a, b):
guard let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db)
else { return nil }
return lhs > rhs
case let .lessThanOrEqual(a, b):
guard let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db)
else { return nil }
return lhs <= rhs
case let .greaterThanOrEqual(a, b):
guard let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db)
else { return nil }
return lhs >= rhs
case let .areEqual(a, b):
if let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db) {
return lhs == rhs
} else if let lhs = resolve(expression: a, context: context, type: String.self, db: db),
let rhs = resolve(expression: b, context: context, type: String.self, db: db) {
return lhs == rhs
} else if let lhs = resolve(expression: a, context: context, type: Bool.self, db: db),
let rhs = resolve(expression: b, context: context, type: Bool.self, db: db) {
return lhs == rhs
} else { return false }
case let .areNotEqual(a, b):
if let lhs = resolve(expression: a, context: context, type: Double.self, db: db),
let rhs = resolve(expression: b, context: context, type: Double.self, db: db) {
return lhs != rhs
} else if let lhs = resolve(expression: a, context: context, type: String.self, db: db),
let rhs = resolve(expression: b, context: context, type: String.self, db: db) {
return lhs != rhs
} else if let lhs = resolve(expression: a, context: context, type: Bool.self, db: db),
let rhs = resolve(expression: b, context: context, type: Bool.self, db: db) {
return lhs != rhs
} else { return true }
default:
print("CVU Expression: \(self) unsupported for resolving to bool")
return nil
}
}
}
......@@ -10,6 +10,7 @@ enum CVUDefinitionType {
case sessions
case renderer
case datasource
case language
case other
}
......@@ -17,10 +18,10 @@ enum CVUDefinitionDomain {
case user
}
struct CVUDefinitionContent: CVUStringConvertible {
struct CVUDefinitionContent: Equatable, CVUStringConvertible {
var definitions: [CVUParsedDefinition] = []
var children: [UINode] = []
var properties: [String: Any?] = [:]
var properties: [String: CVUValue] = [:]
func toCVUString(depth: Int, tab: String, includeInitialTab: Bool) -> String {
let tabs = Array(repeating: tab, count: depth).joined()
......@@ -39,9 +40,30 @@ struct CVUDefinitionContent: CVUStringConvertible {
return "\(includeInitialTab ? tabs : ""){\n\(propertiesString)\(newLineA ? "\n" : "")\(childrenString)\(newLineB ? "\n" : "")\(nestedDefinitions)\(tabs)}"
}
func merge(with other: CVUDefinitionContent) -> CVUDefinitionContent {
var result = self
for definition in other.definitions {
if let index = result.definitions.firstIndex(where: { $0.selector == definition.selector }) {
result.definitions[index] = result.definitions[index].merge(with: definition)
} else {
result.definitions.append(definition)
}
}
result.properties.merge(other.properties, uniquingKeysWith: { lhs, rhs in
if let lhs = lhs.getSubdefinition(), let rhs = rhs.getSubdefinition() {
return .subdefinition(lhs.merge(with: rhs))
} else {
return rhs // Prefer rhs properties
}
})
result.children = other.children // Override children
return result
}
}
public struct CVUParsedDefinition: CVUStringConvertible {
public struct CVUParsedDefinition: Equatable, CVUStringConvertible {
var type: CVUDefinitionType = .other
var domain: CVUDefinitionDomain = .user
var selector: String?
......@@ -49,7 +71,7 @@ public struct CVUParsedDefinition: CVUStringConvertible {
var parsed: CVUDefinitionContent = CVUDefinitionContent()
subscript(propName: String) -> Any? {
subscript(propName: String) -> CVUValue? {
get {
parsed.properties[propName].flatMap { $0 }
}
......@@ -61,10 +83,20 @@ public struct CVUParsedDefinition: CVUStringConvertible {
var selectorIsForList: Bool {
selector?.hasSuffix("[]") ?? false
}
var description: String {
toCVUString(depth: 0, tab: " ", includeInitialTab: true)
}
func toCVUString(depth: Int, tab: String, includeInitialTab: Bool) -> String {
let tabs = Array(repeating: tab, count: depth).joined()
let body = parsed.toCVUString(depth: depth, tab: tab, includeInitialTab: false)
return "\(includeInitialTab ? tabs : "")\(selector?.nilIfBlank.map { "\($0) "} ?? "")\(body)"
}
func merge(with other: CVUParsedDefinition) -> CVUParsedDefinition {
var result = self
result.parsed = parsed.merge(with: other.parsed)
return result
}
}
......@@ -13,36 +13,12 @@ enum CVUValue: Equatable {
case constant(Constant)
case array([CVUValue])
case dictionary([String: CVUValue])
case subdefinition(CVUDefinitionContent)
func resolve(lookup: CVULookup, context: CVUContext, db: Database) -> Double? {
switch self {
case .constant(let constant):
return constant.asNumber()
case .expression(let expression):
return expression.resolve(lookup: lookup, context: context, db: db)
default:
return nil
}
}
func resolve(lookup: CVULookup, context: CVUContext, db: Database) -> String? {
switch self {
case .constant(let constant):
return constant.asString()
case .expression(let expression):
return expression.resolve(lookup: lookup, context: context, db: db)
default:
return nil
}
}
func resolve(lookup: CVULookup, context: CVUContext, db: Database) -> Bool? {
func getSubdefinition() -> CVUDefinitionContent? {
switch self {
case .constant(let constant):
return constant.asBool()
case .expression(let expression):
return expression.resolve(lookup: lookup, context: context, db: db)
case .subdefinition(let x):
return x
default:
return nil
}
......@@ -90,145 +66,7 @@ extension CVUValue {
"\(self)"
}
func resolve(lookup: CVULookup, context: CVUContext, type: Double.Type = Double.self, db: Database) -> Double? {
switch self {
case let .lookup(nodes):
return lookup.resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = condition.resolve(lookup: lookup, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return trueExp.resolve(lookup: lookup, context: context, db: db)
} else {
return falseExp.resolve(lookup: lookup, context: context, db: db)
}
case let .or(a, b):
return a.resolve(lookup: lookup, context: context, db: db) ?? b.resolve(lookup: lookup, context: context, db: db)
case .negation(_):
print("CVU Expression error: Should not use ! operator on non-boolean value")
return nil
case let .addition(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? 0) + (b.resolve(lookup: lookup, context: context, db: db) ?? 0)
case let .subtraction(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? 0) - (b.resolve(lookup: lookup, context: context, db: db) ?? 0)
case let .constant(const):
return const.asNumber()
case let .multiplication(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? 0) * (b.resolve(lookup: lookup, context: context, db: db) ?? 0)
case let .division(a, b):
guard let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db),
rhs != 0
else { return nil }
return lhs / rhs
default:
print("CVU Expression: \(self) unsupported for resolving to double")
return nil
}
}
func resolve(lookup: CVULookup, context: CVUContext, type: String.Type = String.self, db: Database) -> String? {
switch self {
case let .lookup(nodes):
return lookup.resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = condition.resolve(lookup: lookup, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return trueExp.resolve(lookup: lookup, context: context, db: db)
} else {
return falseExp.resolve(lookup: lookup, context: context, db: db)
}
case let .or(a, b):
return a.resolve(lookup: lookup, context: context, type: String.self, db: db)?.nilIfBlank ?? b.resolve(lookup: lookup, context: context, db: db)
case .negation(_):
print("CVU Expression error: Should not use ! operator on non-boolean value")
return nil
case let .addition(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? "") + (b.resolve(lookup: lookup, context: context, db: db) ?? "")
case .subtraction(_, _):
print("CVU Expression error: Should not use - operator on string value")
return nil
case let .constant(const):
return const.asString()
case let .stringMode(nodes: nodes):
return nodes.compactMap { $0.resolve(lookup: lookup, context: context, db: db) }.joined(separator: "")
default:
print("CVU Expression: \(self) unsupported for resolving to string")
return nil
}
}
func resolve(lookup: CVULookup, context: CVUContext, type: Bool.Type = Bool.self, db: Database) -> Bool? {
switch self {
case let .lookup(nodes):
return lookup.resolve(nodes: nodes, context: context, db: db)
case let .conditional(condition, trueExp, falseExp):
let conditionResolved = condition.resolve(lookup: lookup, context: context, type: Bool.self, db: db) ?? false
if conditionResolved {
return trueExp.resolve(lookup: lookup, context: context, db: db)
} else {
return falseExp.resolve(lookup: lookup, context: context, db: db)
}
case let .and(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? false) && (b.resolve(lookup: lookup, context: context, db: db) ?? false)
case let .or(a, b):
return (a.resolve(lookup: lookup, context: context, db: db) ?? false) || (b.resolve(lookup: lookup, context: context, db: db) ?? false)
case let .negation(x):
return x.resolve(lookup: lookup, context: context, type: Bool.self, db: db).map { !$0 }
case .addition(_ ,_):
print("CVU Expression error: Should not use + operator on bool value")
return nil
case .subtraction(_, _):
print("CVU Expression error: Should not use - operator on bool value")
return nil
case let .constant(const):
return const.asBool()
case let .lessThan(a, b):
guard let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db)
else { return nil }
return lhs < rhs
case let .greaterThan(a, b):
guard let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db)
else { return nil }
return lhs > rhs
case let .lessThanOrEqual(a, b):
guard let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db)
else { return nil }
return lhs <= rhs
case let .greaterThanOrEqual(a, b):
guard let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db)
else { return nil }
return lhs >= rhs
case let .areEqual(a, b):
if let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db) {
return lhs == rhs
} else if let lhs = a.resolve(lookup: lookup, context: context, type: String.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: String.self, db: db) {
return lhs == rhs
} else if let lhs = a.resolve(lookup: lookup, context: context, type: Bool.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Bool.self, db: db) {
return lhs == rhs
} else { return false }
case let .areNotEqual(a, b):
if let lhs = a.resolve(lookup: lookup, context: context, type: Double.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Double.self, db: db) {
return lhs != rhs
} else if let lhs = a.resolve(lookup: lookup, context: context, type: String.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: String.self, db: db) {
return lhs != rhs
} else if let lhs = a.resolve(lookup: lookup, context: context, type: Bool.self, db: db),
let rhs = b.resolve(lookup: lookup, context: context, type: Bool.self, db: db) {
return lhs != rhs
} else { return true }
default:
print("CVU Expression: \(self) unsupported for resolving to bool")
return nil
}
}
}
}
......@@ -319,7 +157,10 @@ extension CVUValue: CVUStringConvertible {
"\(key): \(value.toCVUString(depth: depth, tab: tab, includeInitialTab: false))"
}
return "{\n\(strings.joined(separator: ",\n"))\n}"
case .subdefinition(let x):
return x.toCVUString(depth: depth, tab: tab, includeInitialTab: includeInitialTab)
}
}
}
......
......@@ -36,12 +36,12 @@ public enum UIElementFamily: String, CaseIterable {
case FileThumbnail = "filethumbnail"
}
public struct UINode {
public struct UINode: Equatable, Identifiable {
var type: UIElementFamily
var children: [UINode] = []
var properties: [String: Any?] = [:]
var properties: [String: CVUValue] = [:]
let id = UUID()
public let id = UUID()
}
extension UINode: CVUStringConvertible {
......
//
// UINodeResolver.swift
// MemriDatabase
//
// Created by T Brennan on 7/1/21.
//
import Foundation
import SwiftUI
import GRDB
struct UINodeResolver {
var context: CVUContext
var lookup: CVULookup
var node: UINode
var childrenInForEach: some View {
return ForEach(node.children) { child in
CVUElementView(info: UINodeResolver(context: context, lookup: lookup, node: child))
}
}
func resolver(`for` key: String, db: Database) -> CVUValueResolver? {
guard let value = node.properties[key] else { return nil }
return CVUValueResolver(context: context, lookup: lookup, value: value, db: db)
}
}
struct CVUValueResolver {
var context: CVUContext
var lookup: CVULookup
var value: CVUValue
var db: Database
var double: Double? {
lookup.resolve(value: value, context: context, db: db)
}
var string: String? {
lookup.resolve(value: value, context: context, db: db)
}
var bool: Bool? {
lookup.resolve(value: value, context: context, db: db)
}
func edge(_ name: String) -> ItemRecord? {
guard let item: ItemRecord = lookup.resolve(value: value, context: context, db: db) else { return nil }
return lookup.resolve(edge: name, for: item, db: db)
}
func property(_ name: String) -> PropertyDatabaseValue? {
guard let item: ItemRecord = lookup.resolve(value: value, context: context, db: db) else { return nil }
return lookup.resolve(property: name, for: item, db: db)
}
func property(of item: ItemRecord, name: String) -> PropertyDatabaseValue? {
return lookup.resolve(property: name, for: item, db: db)
}
}
......@@ -6,7 +6,7 @@ import Foundation
enum CVUParseErrors: Error {
case UnexpectedToken(CVUToken)
case UnknownDefinition(CVUToken)
case UnknownDefinition(String, CVUToken)
case ExpectedCharacter(Character, CVUToken)
case ExpectedDefinition(CVUToken)
case ExpectedIdentifier(CVUToken)
......@@ -37,9 +37,9 @@ enum CVUParseErrors: Error {
case let .UnexpectedToken(token):
parts = token.toParts()
message = "Unexpected \(displayToken(parts)) found \(loc(parts))"
case let .UnknownDefinition(token):
case let .UnknownDefinition(name, token):
parts = token.toParts()
message = "Unknown Definition type '\(displayToken(parts))' found \(loc(parts))"
message = "Unknown Definition for `\(name)` type '\(displayToken(parts))' found \(loc(parts))"
case let .ExpectedCharacter(char, token):
parts = token.toParts()
message =
......
......@@ -176,8 +176,9 @@ class CVUParser {
case "datasource": return CVUParsedDefinition(type: .datasource, selector: "[datasource = \(name)]",
name: name)
case "renderer": return CVUParsedDefinition(type: .renderer, selector: "[renderer = \(name)]", name: name)
case "language": return CVUParsedDefinition(type: .language, selector: "[language = \(name)]", name: name)
default:
throw CVUParseErrors.UnknownDefinition(typeToken)
throw CVUParseErrors.UnknownDefinition(type, typeToken)
}
}
else {
......@@ -195,7 +196,7 @@ class CVUParser {
func parseDict(_: String? = nil) throws -> CVUDefinitionContent {
var parsedContent = CVUDefinitionContent()
var stack = [Any?]()
var stack = [CVUValue]()
var lastKey: String?
var isArrayMode = false
......@@ -203,7 +204,7 @@ class CVUParser {
func setPropertyValue() {
if !stack.isEmpty, let lastKey = lastKey {
if !isArrayMode && stack.count == 1 { parsedContent.properties[lastKey] = stack[0] }
else if isArrayMode || stack.count > 0 { parsedContent.properties[lastKey] = stack }
else if isArrayMode || stack.count > 0 { parsedContent.properties[lastKey] = .array(stack) }
stack = []
}
......@@ -234,11 +235,6 @@ class CVUParser {
}
case .BracketClose:
if isArrayMode {
// Set value as an empty array if it has no elements
if stack.count == 0 {
stack.append([Any?]())
}
setPropertyValue()
isArrayMode = false
lastKey = nil
......@@ -251,7 +247,7 @@ class CVUParser {
throw CVUParseErrors.ExpectedIdentifier(lastToken!)
}
stack.append(try parseDict(lastKey))
stack.append(.subdefinition(try parseDict(lastKey)))
case .CurlyBracketClose:
setPropertyValue()
return parsedContent // DONE
......@@ -273,8 +269,6 @@ class CVUParser {
if case CVUToken.CurlyBracketOpen = nextToken {
_ = popCurrentToken()
properties = try parseDict(value)
} else {
print(nextToken)
}
addUIElement(type, properties)
}
......@@ -284,8 +278,7 @@ class CVUParser {
if case CVUToken.CurlyBracketOpen = nextToken {
_ = popCurrentToken()
let properties = try parseDict()
stack.append(CVUParsedDefinition(type: .other,
parsed: properties))
stack.append(.subdefinition(properties))
}
}
else if case CVUToken.CurlyBracketOpen = nextToken {
......
......@@ -13,11 +13,13 @@ class AppController: ObservableObject {
let databaseController: DatabaseController
let syncController: SyncController
let cvuController: CVUController
private init() {
let databaseController = DatabaseController()
self.databaseController = databaseController
self.syncController = SyncController(databaseController: databaseController)
self.cvuController = CVUController()
}
func onLaunch() {
......
......@@ -11,7 +11,7 @@ import Combine
struct DatabaseQueryConfig: Equatable {
/// A list of item types to include. Default is Empty -> ALL item types
var itemTypes: [String] = []
var itemTypes: [String] = ["Note"]
/// A list of item UIDs to include. Default is Empty -> don't filter on UID
var itemUIDs: [StringUUID] = []
......
......@@ -155,6 +155,7 @@ enum PropertyDatabaseValue: Equatable {
return value.format("0.###")
case .datetime(let value):
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: value)
case .blob(let value):
return String(data: value, encoding: .utf8)
......
......@@ -9,5 +9,5 @@ import Foundation
import SwiftUI
class SceneController: ObservableObject {
var currentContext: ViewContext = ViewContext(databaseController: AppController.shared.databaseController, query: DatabaseQuery(databaseController: AppController.shared.databaseController))
var currentContext: ViewContext = ViewContext(databaseController: AppController.shared.databaseController, cvuController: AppController.shared.cvuController, query: DatabaseQuery(databaseController: AppController.shared.databaseController))
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment