Compare commits
No commits in common. "29bd87f28761a8d8591d55df18bf28f6936dfb1f" and "7da0368758bd6700f0bf21fe473b17e516fd2c84" have entirely different histories.
29bd87f287
...
7da0368758
|
@ -23,6 +23,7 @@
|
||||||
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */; };
|
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */; };
|
||||||
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649924BBF24100DF9B88 /* GeminiProtocol.swift */; };
|
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649924BBF24100DF9B88 /* GeminiProtocol.swift */; };
|
||||||
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649A24BBF24100DF9B88 /* GeminiRequest.swift */; };
|
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649A24BBF24100DF9B88 /* GeminiRequest.swift */; };
|
||||||
|
D62664A224BBF24100DF9B88 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649B24BBF24100DF9B88 /* GeminiResponse.swift */; };
|
||||||
D62664B124BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
D62664B124BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
||||||
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */; };
|
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */; };
|
||||||
D62664BA24BBF26A00DF9B88 /* GeminiFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
D62664BA24BBF26A00DF9B88 /* GeminiFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -43,9 +44,6 @@
|
||||||
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
||||||
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
||||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
|
||||||
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.swift */; };
|
|
||||||
D69F00B024BEA84D00E37622 /* NavigationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AF24BEA84D00E37622 /* NavigationManager.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -169,6 +167,7 @@
|
||||||
D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWParameters+Gemini.swift"; sourceTree = "<group>"; };
|
D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWParameters+Gemini.swift"; sourceTree = "<group>"; };
|
||||||
D626649924BBF24100DF9B88 /* GeminiProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiProtocol.swift; sourceTree = "<group>"; };
|
D626649924BBF24100DF9B88 /* GeminiProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiProtocol.swift; sourceTree = "<group>"; };
|
||||||
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiRequest.swift; sourceTree = "<group>"; };
|
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiRequest.swift; sourceTree = "<group>"; };
|
||||||
|
D626649B24BBF24100DF9B88 /* GeminiResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; };
|
||||||
D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GeminiFormat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GeminiFormat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeminiFormat.h; sourceTree = "<group>"; };
|
D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeminiFormat.h; sourceTree = "<group>"; };
|
||||||
D62664AB24BBF26A00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D62664AB24BBF26A00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -189,9 +188,6 @@
|
||||||
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
|
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
|
||||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
||||||
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||||
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = "<group>"; };
|
|
||||||
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; };
|
|
||||||
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -288,7 +284,6 @@
|
||||||
children = (
|
children = (
|
||||||
D626645E24BBF1C200DF9B88 /* AppDelegate.swift */,
|
D626645E24BBF1C200DF9B88 /* AppDelegate.swift */,
|
||||||
D626646024BBF1C200DF9B88 /* ContentView.swift */,
|
D626646024BBF1C200DF9B88 /* ContentView.swift */,
|
||||||
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */,
|
|
||||||
D626646224BBF1C300DF9B88 /* Assets.xcassets */,
|
D626646224BBF1C300DF9B88 /* Assets.xcassets */,
|
||||||
D626646724BBF1C300DF9B88 /* Main.storyboard */,
|
D626646724BBF1C300DF9B88 /* Main.storyboard */,
|
||||||
D626646A24BBF1C300DF9B88 /* Info.plist */,
|
D626646A24BBF1C300DF9B88 /* Info.plist */,
|
||||||
|
@ -315,10 +310,9 @@
|
||||||
D626649624BBF24100DF9B88 /* Message+Gemini.swift */,
|
D626649624BBF24100DF9B88 /* Message+Gemini.swift */,
|
||||||
D626649924BBF24100DF9B88 /* GeminiProtocol.swift */,
|
D626649924BBF24100DF9B88 /* GeminiProtocol.swift */,
|
||||||
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */,
|
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */,
|
||||||
|
D626649B24BBF24100DF9B88 /* GeminiResponse.swift */,
|
||||||
D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */,
|
D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */,
|
||||||
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */,
|
|
||||||
D626649724BBF24100DF9B88 /* GeminiConnection.swift */,
|
D626649724BBF24100DF9B88 /* GeminiConnection.swift */,
|
||||||
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */,
|
|
||||||
);
|
);
|
||||||
path = GeminiProtocol;
|
path = GeminiProtocol;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -671,7 +665,6 @@
|
||||||
files = (
|
files = (
|
||||||
D626646124BBF1C200DF9B88 /* ContentView.swift in Sources */,
|
D626646124BBF1C200DF9B88 /* ContentView.swift in Sources */,
|
||||||
D626645F24BBF1C200DF9B88 /* AppDelegate.swift in Sources */,
|
D626645F24BBF1C200DF9B88 /* AppDelegate.swift in Sources */,
|
||||||
D69F00B024BEA84D00E37622 /* NavigationManager.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -680,13 +673,12 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */,
|
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */,
|
||||||
|
D62664A224BBF24100DF9B88 /* GeminiResponse.swift in Sources */,
|
||||||
D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */,
|
D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */,
|
||||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */,
|
|
||||||
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */,
|
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */,
|
||||||
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */,
|
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */,
|
||||||
D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */,
|
D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */,
|
||||||
D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */,
|
D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */,
|
||||||
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1200"
|
|
||||||
version = "1.3">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D626645A24BBF1C200DF9B88"
|
|
||||||
BuildableName = "Gemini.app"
|
|
||||||
BlueprintName = "Gemini"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
||||||
<Testables>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D626647E24BBF22E00DF9B88"
|
|
||||||
BuildableName = "GeminiProtocolTests.xctest"
|
|
||||||
BlueprintName = "GeminiProtocolTests"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D62664AF24BBF26A00DF9B88"
|
|
||||||
BuildableName = "GeminiFormatTests.xctest"
|
|
||||||
BlueprintName = "GeminiFormatTests"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D62664D524BC081B00DF9B88"
|
|
||||||
BuildableName = "GeminiRendererTests.xctest"
|
|
||||||
BlueprintName = "GeminiRendererTests"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D626645A24BBF1C200DF9B88"
|
|
||||||
BuildableName = "Gemini.app"
|
|
||||||
BlueprintName = "Gemini"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "CFNETWORK_DIAGNOSTICS"
|
|
||||||
value = "3"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D626645A24BBF1C200DF9B88"
|
|
||||||
BuildableName = "Gemini.app"
|
|
||||||
BlueprintName = "Gemini"
|
|
||||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
|
@ -7,51 +7,31 @@
|
||||||
<key>Gemini.xcscheme_^#shared#^_</key>
|
<key>Gemini.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>GeminiFormat.xcscheme_^#shared#^_</key>
|
<key>GeminiFormat.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>GeminiProtocol.xcscheme_^#shared#^_</key>
|
<key>GeminiProtocol.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>GeminiRenderer.xcscheme_^#shared#^_</key>
|
<key>GeminiRenderer.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>D626645A24BBF1C200DF9B88</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>D626647E24BBF22E00DF9B88</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>D62664A724BBF26A00DF9B88</key>
|
<key>D62664A724BBF26A00DF9B88</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>primary</key>
|
<key>primary</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>D62664AF24BBF26A00DF9B88</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>D62664D524BC081B00DF9B88</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -14,12 +14,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
var window: NSWindow!
|
var window: NSWindow!
|
||||||
|
|
||||||
let homePage = URL(string: "gemini://gemini.circumlunar.space/")!
|
var connection: GeminiConnection!
|
||||||
private(set) lazy var navigationManager = NavigationManager(url: homePage)
|
let url = URL(string: "gemini://localhost:1965/overview.gmi")!
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
let contentView = ContentView(navigator: navigationManager)
|
let contentView = ContentView()
|
||||||
|
|
||||||
// Create the window and set the content view.
|
// Create the window and set the content view.
|
||||||
window = NSWindow(
|
window = NSWindow(
|
||||||
|
@ -32,10 +32,38 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.contentView = NSHostingView(rootView: contentView)
|
window.contentView = NSHostingView(rootView: contentView)
|
||||||
window.title = "Gemini"
|
window.title = "Gemini"
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
|
connection = GeminiConnection(endpoint: .url(url), delegate: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ aNotification: Notification) {
|
func applicationWillTerminate(_ aNotification: Notification) {
|
||||||
// Insert code here to tear down your application
|
// Insert code here to tear down your application
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alreadyReceived = false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AppDelegate: GeminiConnectionDelegate {
|
||||||
|
func connectionReady(_ connection: GeminiConnection) {
|
||||||
|
print("!! Ready")
|
||||||
|
let req = try! GeminiRequest(url: url)
|
||||||
|
connection.sendRequest(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader) {
|
||||||
|
if !alreadyReceived {
|
||||||
|
alreadyReceived = true
|
||||||
|
print("!! Status: \(header.status)")
|
||||||
|
print("!! Meta: '\(header.meta)'")
|
||||||
|
}
|
||||||
|
if let data = data {
|
||||||
|
print(String(data: data, encoding: .utf8)!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectionCompleted(_ connection: GeminiConnection) {
|
||||||
|
print("!! completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,89 +6,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GeminiFormat
|
|
||||||
import GeminiRenderer
|
|
||||||
import GeminiProtocol
|
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let navigator: NavigationManager
|
|
||||||
@State var task: GeminiDataTask?
|
|
||||||
@State var document: Document?
|
|
||||||
@State var errorMessage: String?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
Text("Hello, world!").padding()
|
||||||
HStack {
|
|
||||||
Button(action: navigator.back) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
}.disabled(navigator.backStack.isEmpty)
|
|
||||||
Button(action: navigator.forward) {
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
}.disabled(navigator.forwardStack.isEmpty)
|
|
||||||
TextField("URL", text: Binding<String>(get: {
|
|
||||||
self.navigator.currentURL.absoluteString
|
|
||||||
}, set: { (newValue) in
|
|
||||||
self.navigator.currentURL = URL(string: newValue)!
|
|
||||||
}))
|
|
||||||
}.padding([.leading, .trailing])
|
|
||||||
mainView.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
.frame(minWidth: 480, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
|
|
||||||
.onReceive(navigator.$currentURL, perform: self.urlChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var mainView: some View {
|
|
||||||
if let document = document {
|
|
||||||
DocumentView(document: document, changeURL: navigator.changeURL)
|
|
||||||
} else if let errorMessage = errorMessage {
|
|
||||||
VStack {
|
|
||||||
Text("An error occurred")
|
|
||||||
.font(.headline)
|
|
||||||
Text(errorMessage)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loadingView
|
|
||||||
.onAppear(perform: self.loadDocument)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var loadingView: some View {
|
|
||||||
if #available(macOS 10.16, iOS 14.0, *) {
|
|
||||||
ProgressView("Loading...")
|
|
||||||
} else {
|
|
||||||
Text("Loading...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadDocument() {
|
|
||||||
let url = navigator.currentURL
|
|
||||||
task = try! GeminiDataTask(url: url, completion: { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
self.errorMessage = error.localizedDescription
|
|
||||||
case let .success(response):
|
|
||||||
guard let text = response.bodyText else {
|
|
||||||
self.errorMessage = "Response had no body text"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.document = GeminiParser.parse(text: text, baseURL: url)
|
|
||||||
}
|
|
||||||
self.task = nil
|
|
||||||
})
|
|
||||||
task!.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func urlChanged(_ newValue: URL) {
|
|
||||||
self.task = nil
|
|
||||||
self.document = nil
|
|
||||||
self.errorMessage = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!))
|
ContentView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
//
|
|
||||||
// NavigationManager.swift
|
|
||||||
// Gemini
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/14/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class NavigationManager: ObservableObject {
|
|
||||||
|
|
||||||
@Published var currentURL: URL
|
|
||||||
@Published var backStack = [URL]()
|
|
||||||
@Published var forwardStack = [URL]()
|
|
||||||
|
|
||||||
init(url: URL) {
|
|
||||||
self.currentURL = url
|
|
||||||
}
|
|
||||||
|
|
||||||
func changeURL(_ url: URL) {
|
|
||||||
backStack.append(currentURL)
|
|
||||||
currentURL = url
|
|
||||||
forwardStack = []
|
|
||||||
}
|
|
||||||
|
|
||||||
func back() {
|
|
||||||
guard !backStack.isEmpty else { return }
|
|
||||||
forwardStack.insert(currentURL, at: 0)
|
|
||||||
currentURL = backStack.removeLast()
|
|
||||||
}
|
|
||||||
|
|
||||||
func forward() {
|
|
||||||
guard !forwardStack.isEmpty else { return }
|
|
||||||
backStack.append(currentURL)
|
|
||||||
currentURL = forwardStack.removeFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -11,32 +11,33 @@ import Network
|
||||||
public protocol GeminiConnectionDelegate: class {
|
public protocol GeminiConnectionDelegate: class {
|
||||||
func connectionReady(_ connection: GeminiConnection)
|
func connectionReady(_ connection: GeminiConnection)
|
||||||
func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader)
|
func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader)
|
||||||
func connection(_ connection: GeminiConnection, handleError error: GeminiConnection.Error)
|
|
||||||
func connectionCompleted(_ connection: GeminiConnection)
|
func connectionCompleted(_ connection: GeminiConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GeminiConnection {
|
public class GeminiConnection {
|
||||||
|
|
||||||
public typealias Error = NWError
|
|
||||||
|
|
||||||
public weak var delegate: GeminiConnectionDelegate?
|
public weak var delegate: GeminiConnectionDelegate?
|
||||||
|
|
||||||
private let connection: NWConnection
|
private var connection: NWConnection?
|
||||||
|
|
||||||
public init(endpoint: NWEndpoint, delegate: GeminiConnectionDelegate? = nil) {
|
public init(endpoint: NWEndpoint, delegate: GeminiConnectionDelegate? = nil) {
|
||||||
self.connection = NWConnection(to: endpoint, using: .gemini)
|
self.connection = NWConnection(to: endpoint, using: .gemini)
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
|
|
||||||
|
startConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func startConnection() {
|
private func startConnection() {
|
||||||
|
guard let connection = connection else { return }
|
||||||
|
|
||||||
connection.stateUpdateHandler = { (newState) in
|
connection.stateUpdateHandler = { (newState) in
|
||||||
switch newState {
|
switch newState {
|
||||||
case .ready:
|
case .ready:
|
||||||
print("\(self.connection) established")
|
print("\(connection) established")
|
||||||
self.delegate?.connectionReady(self)
|
self.delegate?.connectionReady(self)
|
||||||
case let .failed(error):
|
case let .failed(error):
|
||||||
print("\(self.connection) failed: \(error)")
|
print("\(connection) failed: \(error)")
|
||||||
self.connection.cancel()
|
connection.cancel()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -46,34 +47,56 @@ public class GeminiConnection {
|
||||||
connection.start(queue: .main)
|
connection.start(queue: .main)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func cancelConnection() {
|
var report: NWConnection.PendingDataTransferReport!
|
||||||
if connection.state != .cancelled {
|
|
||||||
connection.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func sendRequest(_ request: GeminiRequest) {
|
public func sendRequest(_ request: GeminiRequest) {
|
||||||
|
guard let connection = connection else { return }
|
||||||
|
|
||||||
let message = NWProtocolFramer.Message(geminiRequest: request)
|
let message = NWProtocolFramer.Message(geminiRequest: request)
|
||||||
let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message])
|
let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message])
|
||||||
// todo: should this really always be idempotent?
|
// todo: should this really always be idempotent?
|
||||||
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (_) in }))
|
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (_) in }))
|
||||||
receiveNextMessage()
|
receiveNextMessage()
|
||||||
|
report = connection.startDataTransferReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receiveNextMessage() {
|
private func receiveNextMessage() {
|
||||||
connection.receiveMessage { (data, context, isComplete, error) in
|
guard let connection = connection else { return }
|
||||||
if let error = error {
|
|
||||||
self.delegate?.connection(self, handleError: error)
|
// todo: should this really be .max
|
||||||
} else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
|
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, context, isComplete, error) in
|
||||||
|
// todo: handle error
|
||||||
|
if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
|
||||||
let header = message.geminiResponseHeader,
|
let header = message.geminiResponseHeader,
|
||||||
let delegate = self.delegate {
|
let delegate = self.delegate {
|
||||||
delegate.connection(self, receivedData: data, header: header)
|
delegate.connection(self, receivedData: data, header: header)
|
||||||
|
self.receiveNextMessage()
|
||||||
}
|
}
|
||||||
|
if isComplete {
|
||||||
guard isComplete else { fatalError("Connection should complete immediately") }
|
|
||||||
self.delegate?.connectionCompleted(self)
|
self.delegate?.connectionCompleted(self)
|
||||||
self.connection.cancel()
|
self.report.collect(queue: .main) { (report) in
|
||||||
|
print(report.debugDescription)
|
||||||
}
|
}
|
||||||
|
// connection.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// connection.receiveMessage { (data, context, isComplete, error) in
|
||||||
|
// if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
|
||||||
|
// let header = message.geminiResponseHeader,
|
||||||
|
// let delegate = self.delegate {
|
||||||
|
//// let response = GeminiResponse(status: header.status, meta: header.meta, body: data)
|
||||||
|
// delegate.connection(self, receivedData: data, header: header)
|
||||||
|
// }
|
||||||
|
// // todo: error handling
|
||||||
|
// if let error = error {
|
||||||
|
// print(error)
|
||||||
|
// } else if isComplete {
|
||||||
|
// self.delegate?.connectionCompleted(self)
|
||||||
|
// connection.cancel()
|
||||||
|
// } else {
|
||||||
|
// self.receiveNextMessage()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
//
|
|
||||||
// GeminiDataTask.swift
|
|
||||||
// GeminiProtocol
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/14/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
public class GeminiDataTask {
|
|
||||||
public let request: GeminiRequest
|
|
||||||
private let completion: (Result<GeminiResponse, Error>) -> Void
|
|
||||||
|
|
||||||
private let connection: GeminiConnection
|
|
||||||
|
|
||||||
public private(set) var completed: Bool = false
|
|
||||||
|
|
||||||
public init(request: GeminiRequest, completion: @escaping (Result<GeminiResponse, Error>) -> Void) {
|
|
||||||
self.request = request
|
|
||||||
self.completion = completion
|
|
||||||
self.connection = GeminiConnection(endpoint: .url(request.url))
|
|
||||||
self.connection.delegate = self
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init(url: URL, completion: @escaping (Result<GeminiResponse, Error>) -> Void) throws {
|
|
||||||
self.init(request: try GeminiRequest(url: url), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func resume() {
|
|
||||||
self.connection.startConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func cancel() {
|
|
||||||
connection.cancelConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension GeminiDataTask {
|
|
||||||
enum Error: Swift.Error {
|
|
||||||
case noData
|
|
||||||
case connectionError(NWError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GeminiDataTask: GeminiConnectionDelegate {
|
|
||||||
public func connectionReady(_ connection: GeminiConnection) {
|
|
||||||
connection.sendRequest(self.request)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader) {
|
|
||||||
guard !completed else {
|
|
||||||
print("GeminiRequestTask(\(self)) has already completed, shouldn't be receiving any data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completed = true
|
|
||||||
if let data = data {
|
|
||||||
let response = GeminiResponse(header: header, body: data)
|
|
||||||
self.completion(.success(response))
|
|
||||||
} else {
|
|
||||||
self.completion(.failure(.noData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func connection(_ connection: GeminiConnection, handleError error: GeminiConnection.Error) {
|
|
||||||
guard !completed else {
|
|
||||||
print("GeminiRequestTask(\(self)) has already completed, shouldn't be receiving any data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completed = true
|
|
||||||
self.completion(.failure(.connectionError(error)))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func connectionCompleted(_ connection: GeminiConnection) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -31,11 +31,11 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
|
|
||||||
func handleInput(framer: NWProtocolFramer.Instance) -> Int {
|
func handleInput(framer: NWProtocolFramer.Instance) -> Int {
|
||||||
while true {
|
while true {
|
||||||
var tempStatusCode: GeminiResponseHeader.StatusCode?
|
var tempStatusCode: GeminiResponse.StatusCode?
|
||||||
let parsedStatusCodeAndSpace = framer.parseInput(minimumIncompleteLength: 3, maximumLength: 3) { (buffer, isComplete) -> Int in
|
let parsedStatusCodeAndSpace = framer.parseInput(minimumIncompleteLength: 3, maximumLength: 3) { (buffer, isComplete) -> Int in
|
||||||
guard let buffer = buffer,
|
guard let buffer = buffer,
|
||||||
buffer.count == 3 else { return 0 }
|
buffer.count == 3 else { return 0 }
|
||||||
tempStatusCode = GeminiResponseHeader.StatusCode(buffer)
|
tempStatusCode = GeminiResponse.StatusCode(buffer)
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
guard parsedStatusCodeAndSpace, let statusCode = tempStatusCode else {
|
guard parsedStatusCodeAndSpace, let statusCode = tempStatusCode else {
|
||||||
|
@ -73,7 +73,8 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
let header = GeminiResponseHeader(status: statusCode, meta: meta)
|
let header = GeminiResponseHeader(status: statusCode, meta: meta)
|
||||||
|
|
||||||
let message = NWProtocolFramer.Message(geminiResponseHeader: header)
|
let message = NWProtocolFramer.Message(geminiResponseHeader: header)
|
||||||
if !framer.deliverInputNoCopy(length: .max, message: message, isComplete: true) {
|
if !framer.deliverInputNoCopy(length: 2 + 1 + meta.utf8.count + 2, message: message, isComplete: true) {
|
||||||
|
// todo: why return zero here?
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,7 +87,7 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension GeminiResponseHeader.StatusCode {
|
fileprivate extension GeminiResponse.StatusCode {
|
||||||
init?(_ buffer: UnsafeMutableRawBufferPointer) {
|
init?(_ buffer: UnsafeMutableRawBufferPointer) {
|
||||||
guard let str = String(bytes: buffer[...buffer.index(after: buffer.startIndex)], encoding: .utf8),
|
guard let str = String(bytes: buffer[...buffer.index(after: buffer.startIndex)], encoding: .utf8),
|
||||||
let value = Int(str, radix: 10) else { return nil }
|
let value = Int(str, radix: 10) else { return nil }
|
||||||
|
|
|
@ -11,18 +11,10 @@ public struct GeminiRequest {
|
||||||
public let url: URL
|
public let url: URL
|
||||||
|
|
||||||
public init(url: URL) throws {
|
public init(url: URL) throws {
|
||||||
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { throw Error.invalidURL }
|
if url.absoluteString.count > 1024 {
|
||||||
guard components.scheme == "gemini" else { throw Error.wrongProtocol }
|
|
||||||
|
|
||||||
if components.port == nil {
|
|
||||||
components.port = 1965
|
|
||||||
}
|
|
||||||
|
|
||||||
self.url = components.url!
|
|
||||||
|
|
||||||
if self.url.absoluteString.count > 1024 {
|
|
||||||
throw Error.urlTooLong
|
throw Error.urlTooLong
|
||||||
}
|
}
|
||||||
|
self.url = url
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Data {
|
var data: Data {
|
||||||
|
@ -34,8 +26,6 @@ public struct GeminiRequest {
|
||||||
|
|
||||||
public extension GeminiRequest {
|
public extension GeminiRequest {
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error {
|
||||||
case invalidURL
|
|
||||||
case wrongProtocol
|
|
||||||
case urlTooLong
|
case urlTooLong
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
//
|
//
|
||||||
// GeminiResponse.swift
|
// GeminiResponse.swift
|
||||||
// GeminiProtocol
|
// Gemini
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 7/14/20.
|
// Created by Shadowfacts on 7/12/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
public struct GeminiResponse {
|
public struct GeminiResponse {
|
||||||
public let header: GeminiResponseHeader
|
public let status: StatusCode
|
||||||
|
public let meta: String
|
||||||
public let body: Data?
|
public let body: Data?
|
||||||
|
|
||||||
public var status: GeminiResponseHeader.StatusCode { header.status }
|
}
|
||||||
public var meta: String { header.meta }
|
|
||||||
|
|
||||||
public var rawMimeType: String? {
|
public extension GeminiResponse {
|
||||||
|
// Helpers
|
||||||
|
var rawMimeType: String? {
|
||||||
guard status.isSuccess else { return nil }
|
guard status.isSuccess else { return nil }
|
||||||
return meta.trimmingCharacters(in: .whitespaces)
|
return meta.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
var mimeType: String? {
|
||||||
public var mimeType: String? {
|
|
||||||
guard let rawMimeType = rawMimeType else { return nil }
|
guard let rawMimeType = rawMimeType else { return nil }
|
||||||
return rawMimeType.split(separator: ";").first?.trimmingCharacters(in: .whitespaces)
|
return rawMimeType.split(separator: ";").first?.trimmingCharacters(in: .whitespaces)
|
||||||
}
|
}
|
||||||
public var mimeTypeParameters: [String: String]? {
|
var mimeTypeParameters: [String: String]? {
|
||||||
guard let rawMimeType = rawMimeType else { return nil }
|
guard let rawMimeType = rawMimeType else { return nil }
|
||||||
return rawMimeType.split(separator: ";").dropFirst().reduce(into: [String: String]()) { (parameters, parameter) in
|
return rawMimeType.split(separator: ";").dropFirst().reduce(into: [String: String]()) { (parameters, parameter) in
|
||||||
let parts = parameter.split(separator: "=").map { $0.trimmingCharacters(in: .whitespaces) }
|
let parts = parameter.split(separator: "=").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
@ -33,12 +34,12 @@ public struct GeminiResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@available(macOS 10.16, *)
|
@available(macOS 10.16, *)
|
||||||
public var utiType: UTType? {
|
var utiType: UTType? {
|
||||||
guard let mimeType = mimeType else { return nil }
|
guard let mimeType = mimeType else { return nil }
|
||||||
return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first
|
return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first
|
||||||
}
|
}
|
||||||
|
|
||||||
public var bodyText: String? {
|
var bodyText: String? {
|
||||||
guard let body = body, let parameters = mimeTypeParameters else { return nil }
|
guard let body = body, let parameters = mimeTypeParameters else { return nil }
|
||||||
let encoding: String.Encoding
|
let encoding: String.Encoding
|
||||||
switch parameters["charset"]?.lowercased() {
|
switch parameters["charset"]?.lowercased() {
|
||||||
|
@ -54,3 +55,81 @@ public struct GeminiResponse {
|
||||||
return String(data: body, encoding: encoding)
|
return String(data: body, encoding: encoding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension GeminiResponse {
|
||||||
|
enum StatusCode: Int {
|
||||||
|
// All statuses and subtypes
|
||||||
|
case input = 10
|
||||||
|
case sensitiveInput = 11
|
||||||
|
case success = 20
|
||||||
|
case temporaryRedirect = 30
|
||||||
|
case permanentRedirect = 31
|
||||||
|
case temporaryFailure = 40
|
||||||
|
case serverUnavailable = 41
|
||||||
|
case cgiError = 42
|
||||||
|
case proxyError = 43
|
||||||
|
case slowDown = 44
|
||||||
|
case permanentFailure = 50
|
||||||
|
case notFound = 51
|
||||||
|
case gone = 52
|
||||||
|
case proxyRequestRefused = 53
|
||||||
|
case badRequest = 59
|
||||||
|
case clientCertificateRequested = 60
|
||||||
|
case certificateNotAuthorised = 61
|
||||||
|
case certificateNotValid = 62
|
||||||
|
|
||||||
|
// Status type helpers
|
||||||
|
var isInput: Bool { rawValue / 10 == 1 }
|
||||||
|
var isSuccess: Bool { rawValue / 10 == 2 }
|
||||||
|
var isRedirect: Bool { rawValue / 10 == 3 }
|
||||||
|
var isTemporaryFailure: Bool { rawValue / 10 == 4 }
|
||||||
|
var isPermanentFailure: Bool { rawValue / 10 == 5 }
|
||||||
|
var isClientCertificateRequired: Bool { rawValue / 10 == 6 }
|
||||||
|
|
||||||
|
// Other helpers
|
||||||
|
var isFailure: Bool { isTemporaryFailure || isPermanentFailure }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GeminiResponse.StatusCode: CustomStringConvertible {
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case .input:
|
||||||
|
return "input"
|
||||||
|
case .sensitiveInput:
|
||||||
|
return "sensitiveInput"
|
||||||
|
case .success:
|
||||||
|
return "success"
|
||||||
|
case .temporaryRedirect:
|
||||||
|
return "temporaryRedirect"
|
||||||
|
case .permanentRedirect:
|
||||||
|
return "permanentRedirect"
|
||||||
|
case .temporaryFailure:
|
||||||
|
return "temporaryFailure"
|
||||||
|
case .serverUnavailable:
|
||||||
|
return "serverUnavailable"
|
||||||
|
case .cgiError:
|
||||||
|
return "cgiError"
|
||||||
|
case .proxyError:
|
||||||
|
return "proxyError"
|
||||||
|
case .slowDown:
|
||||||
|
return "slowDown"
|
||||||
|
case .permanentFailure:
|
||||||
|
return "permanentFailure"
|
||||||
|
case .notFound:
|
||||||
|
return "notFound"
|
||||||
|
case .gone:
|
||||||
|
return "gone"
|
||||||
|
case .proxyRequestRefused:
|
||||||
|
return "proxyRequestRefused"
|
||||||
|
case .badRequest:
|
||||||
|
return "badRequest"
|
||||||
|
case .clientCertificateRequested:
|
||||||
|
return "clientCertificateRequested"
|
||||||
|
case .certificateNotAuthorised:
|
||||||
|
return "certificateNotAuthorised"
|
||||||
|
case .certificateNotValid:
|
||||||
|
return "certificateNotValid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,84 +8,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct GeminiResponseHeader {
|
public struct GeminiResponseHeader {
|
||||||
public let status: StatusCode
|
public let status: GeminiResponse.StatusCode
|
||||||
public let meta: String
|
public let meta: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension GeminiResponseHeader {
|
|
||||||
enum StatusCode: Int {
|
|
||||||
// All statuses and subtypes
|
|
||||||
case input = 10
|
|
||||||
case sensitiveInput = 11
|
|
||||||
case success = 20
|
|
||||||
case temporaryRedirect = 30
|
|
||||||
case permanentRedirect = 31
|
|
||||||
case temporaryFailure = 40
|
|
||||||
case serverUnavailable = 41
|
|
||||||
case cgiError = 42
|
|
||||||
case proxyError = 43
|
|
||||||
case slowDown = 44
|
|
||||||
case permanentFailure = 50
|
|
||||||
case notFound = 51
|
|
||||||
case gone = 52
|
|
||||||
case proxyRequestRefused = 53
|
|
||||||
case badRequest = 59
|
|
||||||
case clientCertificateRequested = 60
|
|
||||||
case certificateNotAuthorised = 61
|
|
||||||
case certificateNotValid = 62
|
|
||||||
|
|
||||||
// Status type helpers
|
|
||||||
var isInput: Bool { rawValue / 10 == 1 }
|
|
||||||
var isSuccess: Bool { rawValue / 10 == 2 }
|
|
||||||
var isRedirect: Bool { rawValue / 10 == 3 }
|
|
||||||
var isTemporaryFailure: Bool { rawValue / 10 == 4 }
|
|
||||||
var isPermanentFailure: Bool { rawValue / 10 == 5 }
|
|
||||||
var isClientCertificateRequired: Bool { rawValue / 10 == 6 }
|
|
||||||
|
|
||||||
// Other helpers
|
|
||||||
var isFailure: Bool { isTemporaryFailure || isPermanentFailure }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GeminiResponseHeader.StatusCode: CustomStringConvertible {
|
|
||||||
public var description: String {
|
|
||||||
switch self {
|
|
||||||
case .input:
|
|
||||||
return "\(rawValue): input"
|
|
||||||
case .sensitiveInput:
|
|
||||||
return "\(rawValue): sensitiveInput"
|
|
||||||
case .success:
|
|
||||||
return "\(rawValue): success"
|
|
||||||
case .temporaryRedirect:
|
|
||||||
return "\(rawValue): temporaryRedirect"
|
|
||||||
case .permanentRedirect:
|
|
||||||
return "\(rawValue): permanentRedirect"
|
|
||||||
case .temporaryFailure:
|
|
||||||
return "\(rawValue): temporaryFailure"
|
|
||||||
case .serverUnavailable:
|
|
||||||
return "\(rawValue): serverUnavailable"
|
|
||||||
case .cgiError:
|
|
||||||
return "\(rawValue): cgiError"
|
|
||||||
case .proxyError:
|
|
||||||
return "\(rawValue): proxyError"
|
|
||||||
case .slowDown:
|
|
||||||
return "\(rawValue): slowDown"
|
|
||||||
case .permanentFailure:
|
|
||||||
return "\(rawValue): permanentFailure"
|
|
||||||
case .notFound:
|
|
||||||
return "\(rawValue): notFound"
|
|
||||||
case .gone:
|
|
||||||
return "\(rawValue): gone"
|
|
||||||
case .proxyRequestRefused:
|
|
||||||
return "\(rawValue): proxyRequestRefused"
|
|
||||||
case .badRequest:
|
|
||||||
return "\(rawValue): badRequest"
|
|
||||||
case .clientCertificateRequested:
|
|
||||||
return "\(rawValue): clientCertificateRequested"
|
|
||||||
case .certificateNotAuthorised:
|
|
||||||
return "\(rawValue): certificateNotAuthorised"
|
|
||||||
case .certificateNotValid:
|
|
||||||
return "\(rawValue): certificateNotValid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,19 +11,17 @@ import GeminiFormat
|
||||||
public struct DocumentView: View {
|
public struct DocumentView: View {
|
||||||
private let document: Document
|
private let document: Document
|
||||||
private let blocks: [RenderingBlock]
|
private let blocks: [RenderingBlock]
|
||||||
private let changeURL: ((URL) -> Void)?
|
|
||||||
|
|
||||||
public init(document: Document, changeURL: ((URL) -> Void)? = nil) {
|
public init(document: Document) {
|
||||||
self.document = document
|
self.document = document
|
||||||
self.blocks = document.renderingBlocks
|
self.blocks = document.renderingBlocks
|
||||||
self.changeURL = changeURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
MaybeLazyVStack(alignment: .leading) {
|
MaybeLazyVStack(alignment: .leading) {
|
||||||
ForEach(blocks.indices) { (index) in
|
ForEach(blocks.indices) { (index) in
|
||||||
RenderingBlockView(block: blocks[index], changeURL: changeURL)
|
RenderingBlockView(block: blocks[index])
|
||||||
}
|
}
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,6 @@ import GeminiFormat
|
||||||
|
|
||||||
struct RenderingBlockView: View {
|
struct RenderingBlockView: View {
|
||||||
let block: RenderingBlock
|
let block: RenderingBlock
|
||||||
let changeURL: ((URL) -> Void)?
|
|
||||||
|
|
||||||
init(block: RenderingBlock, changeURL: ((URL) -> Void)? = nil) {
|
|
||||||
self.block = block
|
|
||||||
self.changeURL = changeURL
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -26,15 +20,11 @@ struct RenderingBlockView: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
case let .link(url, text: linkText):
|
case let .link(url, text: linkText):
|
||||||
let text = linkText ?? url.absoluteString
|
let text = linkText ?? url.absoluteString
|
||||||
Button {
|
|
||||||
self.changeURL?(url)
|
|
||||||
} label: {
|
|
||||||
Text(verbatim: text)
|
Text(verbatim: text)
|
||||||
.font(.documentBody)
|
.font(.documentBody)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
.underline()
|
.underline()
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}.buttonStyle(LinkButtonStyle())
|
|
||||||
case let .preformatted(text, alt: _):
|
case let .preformatted(text, alt: _):
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
Text(verbatim: text)
|
Text(verbatim: text)
|
||||||
|
|
Loading…
Reference in New Issue