Add GeminiFormat framework

This commit is contained in:
Shadowfacts 2020-07-12 23:09:22 -04:00
parent ef03073d58
commit a84a223c68
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 649 additions and 0 deletions

View File

@ -12,8 +12,59 @@
D626646324BBF1C300DF9B88 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D626646224BBF1C300DF9B88 /* Assets.xcassets */; };
D626646624BBF1C300DF9B88 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D626646524BBF1C300DF9B88 /* Preview Assets.xcassets */; };
D626646924BBF1C300DF9B88 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D626646724BBF1C300DF9B88 /* Main.storyboard */; };
D62664B124BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
D62664B824BBF26A00DF9B88 /* GeminiFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664B724BBF26A00DF9B88 /* GeminiFormatTests.swift */; };
D62664BA24BBF26A00DF9B88 /* GeminiFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */; settings = {ATTRIBUTES = (Public, ); }; };
D62664BD24BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
D62664BE24BBF26A00DF9B88 /* GeminiFormat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664C524BBF27300DF9B88 /* GeminiParser.swift */; };
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664C724BBF2C600DF9B88 /* Document.swift */; };
/* End PBXBuildFile section */
D626648324BBF22E00DF9B88 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D626645A24BBF1C200DF9B88;
remoteInfo = Gemini;
};
D62664B224BBF26A00DF9B88 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D62664A724BBF26A00DF9B88;
remoteInfo = GeminiFormat;
};
D62664B424BBF26A00DF9B88 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D626645A24BBF1C200DF9B88;
remoteInfo = Gemini;
};
D62664BB24BBF26A00DF9B88 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D62664A724BBF26A00DF9B88;
remoteInfo = GeminiFormat;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
D626649124BBF22E00DF9B88 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
D62664BE24BBF26A00DF9B88 /* GeminiFormat.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
D626645B24BBF1C200DF9B88 /* Gemini.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gemini.app; sourceTree = BUILT_PRODUCTS_DIR; };
D626645E24BBF1C200DF9B88 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -23,6 +74,14 @@
D626646824BBF1C300DF9B88 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
D626646A24BBF1C300DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D626646B24BBF1C300DF9B88 /* Gemini.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Gemini.entitlements; sourceTree = "<group>"; };
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>"; };
D62664AB24BBF26A00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D62664B024BBF26A00DF9B88 /* GeminiFormatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GeminiFormatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D62664B724BBF26A00DF9B88 /* GeminiFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiFormatTests.swift; sourceTree = "<group>"; };
D62664B924BBF26A00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D62664C524BBF27300DF9B88 /* GeminiParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiParser.swift; sourceTree = "<group>"; };
D62664C724BBF2C600DF9B88 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -30,6 +89,19 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D62664BD24BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */,
D62664A524BBF26A00DF9B88 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D62664AD24BBF26A00DF9B88 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D62664B124BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -40,6 +112,8 @@
isa = PBXGroup;
children = (
D626645D24BBF1C200DF9B88 /* Gemini */,
D62664A924BBF26A00DF9B88 /* GeminiFormat */,
D62664B624BBF26A00DF9B88 /* GeminiFormatTests */,
D626645C24BBF1C200DF9B88 /* Products */,
);
sourceTree = "<group>";
@ -48,6 +122,8 @@
isa = PBXGroup;
children = (
D626645B24BBF1C200DF9B88 /* Gemini.app */,
D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */,
D62664B024BBF26A00DF9B88 /* GeminiFormatTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -74,8 +150,39 @@
path = "Preview Content";
sourceTree = "<group>";
};
D62664A924BBF26A00DF9B88 /* GeminiFormat */ = {
isa = PBXGroup;
children = (
D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */,
D62664AB24BBF26A00DF9B88 /* Info.plist */,
D62664C724BBF2C600DF9B88 /* Document.swift */,
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
);
path = GeminiFormat;
sourceTree = "<group>";
};
D62664B624BBF26A00DF9B88 /* GeminiFormatTests */ = {
isa = PBXGroup;
children = (
D62664B724BBF26A00DF9B88 /* GeminiFormatTests.swift */,
D62664B924BBF26A00DF9B88 /* Info.plist */,
);
path = GeminiFormatTests;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
D62664A324BBF26A00DF9B88 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
D62664BA24BBF26A00DF9B88 /* GeminiFormat.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
D626645A24BBF1C200DF9B88 /* Gemini */ = {
isa = PBXNativeTarget;
@ -88,12 +195,50 @@
buildRules = (
);
dependencies = (
D62664BC24BBF26A00DF9B88 /* PBXTargetDependency */,
);
name = Gemini;
productName = Gemini;
productReference = D626645B24BBF1C200DF9B88 /* Gemini.app */;
productType = "com.apple.product-type.application";
};
D62664A724BBF26A00DF9B88 /* GeminiFormat */ = {
isa = PBXNativeTarget;
buildConfigurationList = D62664BF24BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormat" */;
buildPhases = (
D62664A324BBF26A00DF9B88 /* Headers */,
D62664A424BBF26A00DF9B88 /* Sources */,
D62664A524BBF26A00DF9B88 /* Frameworks */,
D62664A624BBF26A00DF9B88 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = GeminiFormat;
productName = GeminiFormat;
productReference = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */;
productType = "com.apple.product-type.framework";
};
D62664AF24BBF26A00DF9B88 /* GeminiFormatTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D62664C224BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormatTests" */;
buildPhases = (
D62664AC24BBF26A00DF9B88 /* Sources */,
D62664AD24BBF26A00DF9B88 /* Frameworks */,
D62664AE24BBF26A00DF9B88 /* Resources */,
);
buildRules = (
);
dependencies = (
D62664B324BBF26A00DF9B88 /* PBXTargetDependency */,
D62664B524BBF26A00DF9B88 /* PBXTargetDependency */,
);
name = GeminiFormatTests;
productName = GeminiFormatTests;
productReference = D62664B024BBF26A00DF9B88 /* GeminiFormatTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@ -106,6 +251,14 @@
D626645A24BBF1C200DF9B88 = {
CreatedOnToolsVersion = 12.0;
};
D62664A724BBF26A00DF9B88 = {
CreatedOnToolsVersion = 12.0;
LastSwiftMigration = 1200;
};
D62664AF24BBF26A00DF9B88 = {
CreatedOnToolsVersion = 12.0;
TestTargetID = D626645A24BBF1C200DF9B88;
};
};
};
buildConfigurationList = D626645624BBF1C200DF9B88 /* Build configuration list for PBXProject "Gemini" */;
@ -122,6 +275,8 @@
projectRoot = "";
targets = (
D626645A24BBF1C200DF9B88 /* Gemini */,
D62664A724BBF26A00DF9B88 /* GeminiFormat */,
D62664AF24BBF26A00DF9B88 /* GeminiFormatTests */,
);
};
/* End PBXProject section */
@ -137,6 +292,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D62664A624BBF26A00DF9B88 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D62664AE24BBF26A00DF9B88 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -149,8 +318,43 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D62664A424BBF26A00DF9B88 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D62664AC24BBF26A00DF9B88 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D62664B824BBF26A00DF9B88 /* GeminiFormatTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
D62664B324BBF26A00DF9B88 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D62664A724BBF26A00DF9B88 /* GeminiFormat */;
targetProxy = D62664B224BBF26A00DF9B88 /* PBXContainerItemProxy */;
};
D62664B524BBF26A00DF9B88 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D626645A24BBF1C200DF9B88 /* Gemini */;
targetProxy = D62664B424BBF26A00DF9B88 /* PBXContainerItemProxy */;
};
D62664BC24BBF26A00DF9B88 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D62664A724BBF26A00DF9B88 /* GeminiFormat */;
targetProxy = D62664BB24BBF26A00DF9B88 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
D626646724BBF1C300DF9B88 /* Main.storyboard */ = {
isa = PBXVariantGroup;
@ -281,6 +485,7 @@
D626646F24BBF1C300DF9B88 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements;
@ -305,6 +510,7 @@
D626647024BBF1C300DF9B88 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements;
@ -326,6 +532,105 @@
};
name = Release;
};
D62664C024BBF26A00DF9B88 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = GeminiFormat/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiFormat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
D62664C124BBF26A00DF9B88 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = GeminiFormat/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiFormat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
D62664C324BBF26A00DF9B88 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
INFOPLIST_FILE = GeminiFormatTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiFormatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gemini.app/Contents/MacOS/Gemini";
};
name = Debug;
};
D62664C424BBF26A00DF9B88 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
INFOPLIST_FILE = GeminiFormatTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiFormatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gemini.app/Contents/MacOS/Gemini";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -347,6 +652,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D62664BF24BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D62664C024BBF26A00DF9B88 /* Debug */,
D62664C124BBF26A00DF9B88 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D62664C224BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormatTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D62664C324BBF26A00DF9B88 /* Debug */,
D62664C424BBF26A00DF9B88 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = D626645324BBF1C200DF9B88 /* Project object */;

View File

@ -9,6 +9,11 @@
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>GeminiFormat.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,35 @@
//
// Document.swift
// GeminiFormat
//
// Created by Shadowfacts on 7/12/20.
//
import Foundation
struct Document {
let url: URL
var lines: [Line]
init(url: URL, lines: [Line] = []) {
self.url = url
self.lines = lines
}
}
extension Document {
enum Line: Equatable {
case text(String)
case link(URL, text: String?)
case preformattedText(String, alt: String?)
case heading(String, level: HeadingLevel)
case unorderedListItem(String)
case quote(String)
}
}
extension Document {
enum HeadingLevel: Int {
case h1 = 1, h2 = 2, h3 = 3
}
}

View File

@ -0,0 +1,18 @@
//
// GeminiFormat.h
// GeminiFormat
//
// Created by Shadowfacts on 7/12/20.
//
#import <Foundation/Foundation.h>
//! Project version number for GeminiFormat.
FOUNDATION_EXPORT double GeminiFormatVersionNumber;
//! Project version string for GeminiFormat.
FOUNDATION_EXPORT const unsigned char GeminiFormatVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <GeminiFormat/PublicHeader.h>

View File

@ -0,0 +1,110 @@
//
// GeminiParser.swift
// GeminiFormat
//
// Created by Shadowfacts on 7/12/20.
//
import Foundation
struct GeminiParser {
private init() {}
static func parse(text: String, baseURL: URL) -> Document {
var doc = Document(url: baseURL)
var preformattingState = PreformattingState.off
text.enumerateLines { (line, stop) in
if line.starts(with: "```") {
switch preformattingState {
case .off:
let alt: String?
if line.count > 3 {
alt = String(line[line.index(line.startIndex, offsetBy: 3)...])
} else {
alt = nil
}
preformattingState = .on(alt)
case .on(_):
preformattingState = .off
}
if case .off = preformattingState {
}
} else if case let .on(alt) = preformattingState {
doc.lines.append(.preformattedText(line, alt: alt))
} else if line.starts(with: "=>") {
// Link line
let urlStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: 2))
let urlEnd = line.firstWhitespaceIndex(after: urlStart)
let textStart = line.firstNonWhitespaceIndex(after: urlEnd)
let urlString = String(line[urlStart..<urlEnd])
// todo: if the URL initializer fails, should there be a .link line with a nil URL?
let url = URL(string: urlString, relativeTo: baseURL)!.absoluteURL
let text: String?
if textStart < line.endIndex {
text = String(line[textStart..<line.endIndex])
} else {
text = nil
}
doc.lines.append(.link(url, text: text))
} else if line.starts(with: "#") {
let level: Document.HeadingLevel
if line.starts(with: "###") {
level = .h3
} else if line.starts(with: "##") {
level = .h2
} else {
level = .h1
}
let headingStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: level.rawValue))
let headingText = String(line[headingStart...])
doc.lines.append(.heading(headingText, level: level))
} else if line.starts(with: "* ") {
let listItemStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: 2))
let listItemText = String(line[listItemStart...])
doc.lines.append(.unorderedListItem(listItemText))
} else if line.starts(with: ">") {
let quoteStartIndex = line.firstNonWhitespaceIndex(after: line.index(after: line.startIndex))
let quoteText = String(line[quoteStartIndex...])
doc.lines.append(.quote(quoteText))
} else {
doc.lines.append(.text(line))
}
}
return doc
}
}
fileprivate extension GeminiParser {
enum PreformattingState {
case off
case on(_ alt: String?)
}
}
fileprivate extension String {
func firstNonWhitespaceIndex(after index: String.Index) -> String.Index {
var index = index
// using .unicodeScalars.first should be fine, since all whitespace characters are single scalars
while index < self.endIndex, CharacterSet.whitespaces.contains(self[index].unicodeScalars.first!) {
index = self.index(after: index)
}
return index
}
func firstWhitespaceIndex(after index: String.Index) -> String.Index {
var index = index
// todo: could the first unicode scalar of a character be whitespace even though the whole character is not?
while index < self.endIndex, !CharacterSet.whitespaces.contains(self[index].unicodeScalars.first!) {
index = self.index(after: index)
}
return index
}
}

22
GeminiFormat/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -0,0 +1,114 @@
//
// GeminiFormatTests.swift
// GeminiFormatTests
//
// Created by Shadowfacts on 7/12/20.
//
import XCTest
@testable import GeminiFormat
class GeminiFormatTests: XCTestCase {
func assertParseLines(text: String, lines expected: [Document.Line], message: String, file: StaticString = #filePath, line: UInt = #line) {
let doc = GeminiParser.parse(text: text, baseURL: URL(string: "gemini://example.com")!)
XCTAssertEqual(doc.lines, expected, message, file: file, line: line)
}
func testParsePlainLines() {
assertParseLines(text: "test", lines: [.text("test")], message: "parse a plain text line")
assertParseLines(text: "one\ntwo", lines: [
.text("one"),
.text("two")
], message: "parse multiple, newline delmited plain lines")
assertParseLines(text: "one\r\ntwo", lines: [
.text("one"),
.text("two")
], message: "parse multiple, CRLF delmited plain lines")
}
func testParseLinkLines() {
assertParseLines(text: "=> gemini://blah.com", lines: [
.link(URL(string: "gemini://blah.com")!, text: nil)
], message: "parse a bare link line")
assertParseLines(text: "=> gemini://blah.com:1234/foo/bar?baz", lines: [
.link(URL(string: "gemini://blah.com:1234/foo/bar?baz")!, text: nil)
], message: "parse a more complex bare link line")
assertParseLines(text: "=> gemini://blah.com \t Link to example", lines: [
.link(URL(string: "gemini://blah.com")!, text: "Link to example")
], message: "parse a simple link line with associated text")
assertParseLines(text: "=> gemini://blah.com/foo Link to foo", lines: [
.link(URL(string: "gemini://blah.com/foo")!, text: "Link to foo")
], message: "parse a more complex link line with associated text")
assertParseLines(text: "=> https://example.com", lines: [
.link(URL(string: "https://example.com")!, text: nil)
], message: "parse a link with a different protocol")
assertParseLines(text: "=> /foo/bar/baz", lines: [
.link(URL(string: "gemini://example.com/foo/bar/baz")!, text: nil)
], message: "resolve a relative path link")
assertParseLines(text: "=>gemini://blah.com", lines: [
.link(URL(string: "gemini://blah.com")!, text: nil)
], message: "parse link without whitespace after =>")
}
func testParseHeadingLines() {
assertParseLines(text: "# test", lines: [
.heading("test", level: .h1)
], message: "level 1 heading")
assertParseLines(text: "#test", lines: [
.heading("test", level: .h1)
], message: "level 1 heading without whitespace")
assertParseLines(text: "#test\n## two\r\n###three", lines: [
.heading("test", level: .h1),
.heading("two", level: .h2),
.heading("three", level: .h3)
], message: "multiples headings with and without whitespace")
}
func testParseListItemLines() {
assertParseLines(text: "* test list item", lines: [
.unorderedListItem("test list item")
], message: "parse simple list item")
assertParseLines(text: "*test", lines: [
.text("*test")
], message: "don't parse list item without space after asterisk")
assertParseLines(text: "* one\n* two\n*three", lines: [
.unorderedListItem("one"),
.unorderedListItem("two"),
.text("*three")
], message: "parse multiple list items")
}
func testParseQuoteLines() {
assertParseLines(text: "> quote", lines: [
.quote("quote")
], message: "parse quote line")
assertParseLines(text: ">quote", lines: [
.quote("quote")
], message: "parse quote line without space after >")
assertParseLines(text: ">one\n> two\n>three", lines: [
.quote("one"),
.quote("two"),
.quote("three")
], message: "parse multiple quote lines")
}
func testParsePreformattedLines() {
assertParseLines(text: "```\nsomething\n```", lines: [
.preformattedText("something", alt: nil)
], message: "parse simple preformatted line")
assertParseLines(text: "```alt\nsomething\n```", lines: [
.preformattedText("something", alt: "alt")
], message: "parse simple preformatted line with alt")
assertParseLines(text: "```alt\nsomething\n```other", lines: [
.preformattedText("something", alt: "alt")
], message: "ignore extra text after closing ```")
assertParseLines(text: "```\n# not a heading\n* not a list item\n>not a quote\n=> /link not a link\n```", lines: [
.preformattedText("# not a heading", alt: nil),
.preformattedText("* not a list item", alt: nil),
.preformattedText(">not a quote", alt: nil),
.preformattedText("=> /link not a link", alt: nil),
], message: "don't parse special lines inside preformatted")
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>