diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47042c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +xcuserdata diff --git a/Esse/Esse.xcodeproj/project.pbxproj b/Esse/Esse.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bc60ba0 --- /dev/null +++ b/Esse/Esse.xcodeproj/project.pbxproj @@ -0,0 +1,789 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + FA2748402A352CFC00B6189C /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA27483F2A352CFC00B6189C /* SidebarView.swift */; }; + FA2748422A352D4D00B6189C /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2748412A352D4D00B6189C /* Helpers.swift */; }; + FA2748462A374CFA00B6189C /* Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2748452A374CFA00B6189C /* Intent.swift */; }; + FA39AF8F2C5181EE0054D79D /* NeonPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = FA39AF8E2C5181EE0054D79D /* NeonPlugin */; }; + FA39AF912C51B6340054D79D /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA39AF902C51B6340054D79D /* TextView.swift */; }; + FA39AF932C53B00F0054D79D /* EsseCommandLine in CopyFiles */ = {isa = PBXBuildFile; fileRef = FA39AF922C53AED40054D79D /* EsseCommandLine */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + FA580F462AAFBB0000FF53EE /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA580F452AAFBB0000FF53EE /* MenuCommands.swift */; }; + FA73C8632A34AEF6004ABF10 /* EsseCore in Frameworks */ = {isa = PBXBuildFile; productRef = FA73C8622A34AEF6004ABF10 /* EsseCore */; }; + FA853DCF2A33AD71007D6889 /* EsseApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA853DCE2A33AD71007D6889 /* EsseApp.swift */; }; + FA853DD32A33AD72007D6889 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA853DD22A33AD72007D6889 /* Assets.xcassets */; }; + FA853DD72A33AD72007D6889 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA853DD62A33AD72007D6889 /* Preview Assets.xcassets */; }; + FA853DDF2A33ADBB007D6889 /* MacMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA853DDE2A33ADBB007D6889 /* MacMainView.swift */; }; + FA853DE22A33AEBE007D6889 /* DSFQuickActionBar in Frameworks */ = {isa = PBXBuildFile; platformFilters = (macos, ); productRef = FA853DE12A33AEBE007D6889 /* DSFQuickActionBar */; }; + FAA09A892ABF2FD700601BDC /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA09A882ABF2FD700601BDC /* FooterView.swift */; }; + FAA09A8B2ABF393E00601BDC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA09A8A2ABF393E00601BDC /* AppDelegate.swift */; }; + FAA09A932ABFAC9F00601BDC /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA09A922ABFAC9F00601BDC /* LibraryView.swift */; }; + FAA09A972AC08CD700601BDC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA09A962AC08CD700601BDC /* Notifications.swift */; }; + FAB46A422AC0E4BD00DC6369 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = FAB46A412AC0E4BD00DC6369 /* LaunchAtLogin */; }; + FAB46A442AC0E4ED00DC6369 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB46A432AC0E4ED00DC6369 /* Settings.swift */; }; + FAB46A4C2AC9DF7C00DC6369 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB46A4B2AC9DF7C00DC6369 /* main.swift */; }; + FAB46A552AC9E05000DC6369 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = FAB46A542AC9E05000DC6369 /* ArgumentParser */; }; + FAB46A572AC9E15600DC6369 /* EsseCore in Frameworks */ = {isa = PBXBuildFile; productRef = FAB46A562AC9E15600DC6369 /* EsseCore */; }; + FAD08B702B43430100AE4294 /* STTextView in Frameworks */ = {isa = PBXBuildFile; productRef = FAD08B6F2B43430100AE4294 /* STTextView */; }; + FAD08B722B434AF400AE4294 /* EsseDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD08B712B434AF400AE4294 /* EsseDocument.swift */; }; + FAD08B752B435D8D00AE4294 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD08B742B435D8D00AE4294 /* GeneralSettingsView.swift */; }; + FAD08B782B435E1100AE4294 /* EnumPickerVIew.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD08B772B435E1100AE4294 /* EnumPickerVIew.swift */; }; + FAD08B7A2B43626D00AE4294 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD08B792B43626D00AE4294 /* Appearance.swift */; }; + FAD08B7E2B43890B00AE4294 /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD08B7D2B43890B00AE4294 /* AboutSettingsView.swift */; }; + FAD22E132A34B8290001F65C /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD22E122A34B8290001F65C /* InspectorView.swift */; }; + FAFF320F2A33B1D800B4F8AC /* FilterCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFF320E2A33B1D800B4F8AC /* FilterCellView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + FA3D77152AC9F37000E56CE8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FA853DC32A33AD71007D6889 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FAB46A482AC9DF7C00DC6369; + remoteInfo = EsseCommandLine; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + FAB46A472AC9DF7C00DC6369 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + FAB46A5A2AC9E1D900DC6369 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 6; + files = ( + FA39AF932C53B00F0054D79D /* EsseCommandLine in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + FA27483F2A352CFC00B6189C /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + FA2748412A352D4D00B6189C /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + FA2748452A374CFA00B6189C /* Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Intent.swift; sourceTree = ""; }; + FA39AF902C51B6340054D79D /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; + FA39AF922C53AED40054D79D /* EsseCommandLine */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = EsseCommandLine; sourceTree = BUILT_PRODUCTS_DIR; }; + FA580F452AAFBB0000FF53EE /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; + FA73C85F2A33DBB4004ABF10 /* EsseCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EsseCore; sourceTree = ""; }; + FA853DCB2A33AD71007D6889 /* Esse.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Esse.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FA853DCE2A33AD71007D6889 /* EsseApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EsseApp.swift; sourceTree = ""; }; + FA853DD22A33AD72007D6889 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + FA853DD42A33AD72007D6889 /* Esse.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Esse.entitlements; sourceTree = ""; }; + FA853DD62A33AD72007D6889 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + FA853DDE2A33ADBB007D6889 /* MacMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacMainView.swift; sourceTree = ""; }; + FAA09A882ABF2FD700601BDC /* FooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = ""; }; + FAA09A8A2ABF393E00601BDC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + FAA09A8C2ABF3A0700601BDC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + FAA09A922ABFAC9F00601BDC /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; + FAA09A962AC08CD700601BDC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + FAB46A432AC0E4ED00DC6369 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + FAB46A4B2AC9DF7C00DC6369 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + FAB46A502AC9DFD300DC6369 /* EsseCommandLine.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EsseCommandLine.entitlements; sourceTree = ""; }; + FAD08B712B434AF400AE4294 /* EsseDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EsseDocument.swift; sourceTree = ""; }; + FAD08B742B435D8D00AE4294 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; + FAD08B772B435E1100AE4294 /* EnumPickerVIew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerVIew.swift; sourceTree = ""; }; + FAD08B792B43626D00AE4294 /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + FAD08B7D2B43890B00AE4294 /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = ""; }; + FAD22E122A34B8290001F65C /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; + FAFF320E2A33B1D800B4F8AC /* FilterCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterCellView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + FA853DC82A33AD71007D6889 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA853DE22A33AEBE007D6889 /* DSFQuickActionBar in Frameworks */, + FAD08B702B43430100AE4294 /* STTextView in Frameworks */, + FA39AF8F2C5181EE0054D79D /* NeonPlugin in Frameworks */, + FA73C8632A34AEF6004ABF10 /* EsseCore in Frameworks */, + FAB46A422AC0E4BD00DC6369 /* LaunchAtLogin in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FAB46A462AC9DF7C00DC6369 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FAB46A552AC9E05000DC6369 /* ArgumentParser in Frameworks */, + FAB46A572AC9E15600DC6369 /* EsseCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FA2748442A374CE900B6189C /* Intents */ = { + isa = PBXGroup; + children = ( + FA2748452A374CFA00B6189C /* Intent.swift */, + ); + path = Intents; + sourceTree = ""; + }; + FA73C85E2A33DBB4004ABF10 /* Packages */ = { + isa = PBXGroup; + children = ( + FA73C85F2A33DBB4004ABF10 /* EsseCore */, + ); + path = Packages; + sourceTree = ""; + }; + FA73C8612A34AEF6004ABF10 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + FA853DC22A33AD71007D6889 = { + isa = PBXGroup; + children = ( + FA73C85E2A33DBB4004ABF10 /* Packages */, + FA853DCD2A33AD71007D6889 /* Esse */, + FAB46A4A2AC9DF7C00DC6369 /* EsseCommandLine */, + FA853DCC2A33AD71007D6889 /* Products */, + FA73C8612A34AEF6004ABF10 /* Frameworks */, + ); + sourceTree = ""; + }; + FA853DCC2A33AD71007D6889 /* Products */ = { + isa = PBXGroup; + children = ( + FA853DCB2A33AD71007D6889 /* Esse.app */, + FA39AF922C53AED40054D79D /* EsseCommandLine */, + ); + name = Products; + sourceTree = ""; + }; + FA853DCD2A33AD71007D6889 /* Esse */ = { + isa = PBXGroup; + children = ( + FAA09A8C2ABF3A0700601BDC /* Info.plist */, + FA2748442A374CE900B6189C /* Intents */, + FA853DDD2A33AD98007D6889 /* macOS */, + FA853DCE2A33AD71007D6889 /* EsseApp.swift */, + FAA09A8A2ABF393E00601BDC /* AppDelegate.swift */, + FA853DD22A33AD72007D6889 /* Assets.xcassets */, + FA853DD42A33AD72007D6889 /* Esse.entitlements */, + FA853DD52A33AD72007D6889 /* Preview Content */, + ); + path = Esse; + sourceTree = ""; + }; + FA853DD52A33AD72007D6889 /* Preview Content */ = { + isa = PBXGroup; + children = ( + FA853DD62A33AD72007D6889 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + FA853DDD2A33AD98007D6889 /* macOS */ = { + isa = PBXGroup; + children = ( + FAFE30652A35246800539695 /* Main */, + FAD08B732B435D7A00AE4294 /* Settings */, + FAD08B762B435DF200AE4294 /* Helpers */, + FAA09A962AC08CD700601BDC /* Notifications.swift */, + FA580F452AAFBB0000FF53EE /* MenuCommands.swift */, + FAD08B712B434AF400AE4294 /* EsseDocument.swift */, + ); + path = macOS; + sourceTree = ""; + }; + FAB46A4A2AC9DF7C00DC6369 /* EsseCommandLine */ = { + isa = PBXGroup; + children = ( + FAB46A502AC9DFD300DC6369 /* EsseCommandLine.entitlements */, + FAB46A4B2AC9DF7C00DC6369 /* main.swift */, + ); + path = EsseCommandLine; + sourceTree = ""; + }; + FAD08B732B435D7A00AE4294 /* Settings */ = { + isa = PBXGroup; + children = ( + FAB46A432AC0E4ED00DC6369 /* Settings.swift */, + FAD08B742B435D8D00AE4294 /* GeneralSettingsView.swift */, + FAD08B7D2B43890B00AE4294 /* AboutSettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; + FAD08B762B435DF200AE4294 /* Helpers */ = { + isa = PBXGroup; + children = ( + FA2748412A352D4D00B6189C /* Helpers.swift */, + FAD08B772B435E1100AE4294 /* EnumPickerVIew.swift */, + FAD08B792B43626D00AE4294 /* Appearance.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + FAFE30652A35246800539695 /* Main */ = { + isa = PBXGroup; + children = ( + FA853DDE2A33ADBB007D6889 /* MacMainView.swift */, + FAFF320E2A33B1D800B4F8AC /* FilterCellView.swift */, + FAD22E122A34B8290001F65C /* InspectorView.swift */, + FAA09A882ABF2FD700601BDC /* FooterView.swift */, + FA27483F2A352CFC00B6189C /* SidebarView.swift */, + FAA09A922ABFAC9F00601BDC /* LibraryView.swift */, + FA39AF902C51B6340054D79D /* TextView.swift */, + ); + path = Main; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + FA853DCA2A33AD71007D6889 /* Esse */ = { + isa = PBXNativeTarget; + buildConfigurationList = FA853DDA2A33AD72007D6889 /* Build configuration list for PBXNativeTarget "Esse" */; + buildPhases = ( + FA853DC72A33AD71007D6889 /* Sources */, + FA853DC82A33AD71007D6889 /* Frameworks */, + FA853DC92A33AD71007D6889 /* Resources */, + FAB46A5A2AC9E1D900DC6369 /* CopyFiles */, + FAB46A5C2AC9E4EF00DC6369 /* Build Version from Git */, + ); + buildRules = ( + ); + dependencies = ( + FA3D77162AC9F37000E56CE8 /* PBXTargetDependency */, + ); + name = Esse; + packageProductDependencies = ( + FA853DE12A33AEBE007D6889 /* DSFQuickActionBar */, + FA73C8622A34AEF6004ABF10 /* EsseCore */, + FAB46A412AC0E4BD00DC6369 /* LaunchAtLogin */, + FAD08B6F2B43430100AE4294 /* STTextView */, + FA39AF8E2C5181EE0054D79D /* NeonPlugin */, + ); + productName = Esse; + productReference = FA853DCB2A33AD71007D6889 /* Esse.app */; + productType = "com.apple.product-type.application"; + }; + FAB46A482AC9DF7C00DC6369 /* EsseCommandLine */ = { + isa = PBXNativeTarget; + buildConfigurationList = FAB46A4D2AC9DF7C00DC6369 /* Build configuration list for PBXNativeTarget "EsseCommandLine" */; + buildPhases = ( + FAB46A452AC9DF7C00DC6369 /* Sources */, + FAB46A462AC9DF7C00DC6369 /* Frameworks */, + FAB46A472AC9DF7C00DC6369 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EsseCommandLine; + packageProductDependencies = ( + FAB46A542AC9E05000DC6369 /* ArgumentParser */, + FAB46A562AC9E15600DC6369 /* EsseCore */, + ); + productName = EsseCommandLine; + productReference = FA39AF922C53AED40054D79D /* EsseCommandLine */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FA853DC32A33AD71007D6889 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1600; + TargetAttributes = { + FA853DCA2A33AD71007D6889 = { + CreatedOnToolsVersion = 15.0; + }; + FAB46A482AC9DF7C00DC6369 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = FA853DC62A33AD71007D6889 /* Build configuration list for PBXProject "Esse" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = FA853DC22A33AD71007D6889; + packageReferences = ( + FA853DE02A33AEBE007D6889 /* XCRemoteSwiftPackageReference "DSFQuickActionBar" */, + FAB46A402AC0E4BD00DC6369 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, + FAB46A532AC9E05000DC6369 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, + FAD08B6C2B43430100AE4294 /* XCRemoteSwiftPackageReference "STTextView" */, + FA39AF8D2C5181EE0054D79D /* XCRemoteSwiftPackageReference "STTextView-Plugin-Neon" */, + ); + productRefGroup = FA853DCC2A33AD71007D6889 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FA853DCA2A33AD71007D6889 /* Esse */, + FAB46A482AC9DF7C00DC6369 /* EsseCommandLine */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FA853DC92A33AD71007D6889 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA853DD72A33AD72007D6889 /* Preview Assets.xcassets in Resources */, + FA853DD32A33AD72007D6889 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FAB46A5C2AC9E4EF00DC6369 /* Build Version from Git */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Version from Git"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "buildNumber=$(git rev-list --count HEAD)\n#/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"$PRODUCT_SETTINGS_PATH\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FA853DC72A33AD71007D6889 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FAFF320F2A33B1D800B4F8AC /* FilterCellView.swift in Sources */, + FAA09A972AC08CD700601BDC /* Notifications.swift in Sources */, + FAB46A442AC0E4ED00DC6369 /* Settings.swift in Sources */, + FA2748402A352CFC00B6189C /* SidebarView.swift in Sources */, + FA853DCF2A33AD71007D6889 /* EsseApp.swift in Sources */, + FAD08B7A2B43626D00AE4294 /* Appearance.swift in Sources */, + FAA09A932ABFAC9F00601BDC /* LibraryView.swift in Sources */, + FAD22E132A34B8290001F65C /* InspectorView.swift in Sources */, + FAA09A892ABF2FD700601BDC /* FooterView.swift in Sources */, + FAD08B7E2B43890B00AE4294 /* AboutSettingsView.swift in Sources */, + FA853DDF2A33ADBB007D6889 /* MacMainView.swift in Sources */, + FAA09A8B2ABF393E00601BDC /* AppDelegate.swift in Sources */, + FAD08B722B434AF400AE4294 /* EsseDocument.swift in Sources */, + FAD08B752B435D8D00AE4294 /* GeneralSettingsView.swift in Sources */, + FA2748422A352D4D00B6189C /* Helpers.swift in Sources */, + FAD08B782B435E1100AE4294 /* EnumPickerVIew.swift in Sources */, + FA580F462AAFBB0000FF53EE /* MenuCommands.swift in Sources */, + FA39AF912C51B6340054D79D /* TextView.swift in Sources */, + FA2748462A374CFA00B6189C /* Intent.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FAB46A452AC9DF7C00DC6369 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FAB46A4C2AC9DF7C00DC6369 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + FA3D77162AC9F37000E56CE8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FAB46A482AC9DF7C00DC6369 /* EsseCommandLine */; + targetProxy = FA3D77152AC9F37000E56CE8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + FA853DD82A33AD72007D6889 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + FA853DD92A33AD72007D6889 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; + FA853DDB2A33AD72007D6889 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = Esse/Esse.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 227; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "\"Esse/Preview Content\""; + DEVELOPMENT_TEAM = X93LWC49WV; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Esse/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Esse; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 2024.1; + PRODUCT_BUNDLE_IDENTIFIER = com.ameba.esse; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = NO; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + FA853DDC2A33AD72007D6889 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = Esse/Esse.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 227; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "\"Esse/Preview Content\""; + DEVELOPMENT_TEAM = X93LWC49WV; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Esse/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Esse; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 2024.1; + PRODUCT_BUNDLE_IDENTIFIER = com.ameba.esse; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = NO; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + FAB46A4E2AC9DF7C00DC6369 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = EsseCommandLine/EsseCommandLine.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = X93LWC49WV; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + OTHER_CODE_SIGN_FLAGS = "\"-i $(PRODUCT_BUNDLE_IDENTIFIER)\""; + PRODUCT_BUNDLE_IDENTIFIER = com.ameba.esse.cli; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + FAB46A4F2AC9DF7C00DC6369 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = EsseCommandLine/EsseCommandLine.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = X93LWC49WV; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + OTHER_CODE_SIGN_FLAGS = "\"-i $(PRODUCT_BUNDLE_IDENTIFIER)\""; + PRODUCT_BUNDLE_IDENTIFIER = com.ameba.esse.cli; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FA853DC62A33AD71007D6889 /* Build configuration list for PBXProject "Esse" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA853DD82A33AD72007D6889 /* Debug */, + FA853DD92A33AD72007D6889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FA853DDA2A33AD72007D6889 /* Build configuration list for PBXNativeTarget "Esse" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA853DDB2A33AD72007D6889 /* Debug */, + FA853DDC2A33AD72007D6889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FAB46A4D2AC9DF7C00DC6369 /* Build configuration list for PBXNativeTarget "EsseCommandLine" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FAB46A4E2AC9DF7C00DC6369 /* Debug */, + FAB46A4F2AC9DF7C00DC6369 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + FA39AF8D2C5181EE0054D79D /* XCRemoteSwiftPackageReference "STTextView-Plugin-Neon" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krzyzanowskim/STTextView-Plugin-Neon"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.0; + }; + }; + FA853DE02A33AEBE007D6889 /* XCRemoteSwiftPackageReference "DSFQuickActionBar" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/dagronf/DSFQuickActionBar.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.1; + }; + }; + FAB46A402AC0E4BD00DC6369 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; + requirement = { + branch = main; + kind = branch; + }; + }; + FAB46A532AC9E05000DC6369 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.3; + }; + }; + FAD08B6C2B43430100AE4294 /* XCRemoteSwiftPackageReference "STTextView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krzyzanowskim/STTextView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.8.22; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + FA39AF8E2C5181EE0054D79D /* NeonPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = FA39AF8D2C5181EE0054D79D /* XCRemoteSwiftPackageReference "STTextView-Plugin-Neon" */; + productName = NeonPlugin; + }; + FA73C8622A34AEF6004ABF10 /* EsseCore */ = { + isa = XCSwiftPackageProductDependency; + productName = EsseCore; + }; + FA853DE12A33AEBE007D6889 /* DSFQuickActionBar */ = { + isa = XCSwiftPackageProductDependency; + package = FA853DE02A33AEBE007D6889 /* XCRemoteSwiftPackageReference "DSFQuickActionBar" */; + productName = DSFQuickActionBar; + }; + FAB46A412AC0E4BD00DC6369 /* LaunchAtLogin */ = { + isa = XCSwiftPackageProductDependency; + package = FAB46A402AC0E4BD00DC6369 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; + productName = LaunchAtLogin; + }; + FAB46A542AC9E05000DC6369 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = FAB46A532AC9E05000DC6369 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; + FAB46A562AC9E15600DC6369 /* EsseCore */ = { + isa = XCSwiftPackageProductDependency; + productName = EsseCore; + }; + FAD08B6F2B43430100AE4294 /* STTextView */ = { + isa = XCSwiftPackageProductDependency; + package = FAD08B6C2B43430100AE4294 /* XCRemoteSwiftPackageReference "STTextView" */; + productName = STTextView; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = FA853DC32A33AD71007D6889 /* Project object */; +} diff --git a/Esse/Esse.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Esse/Esse.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Esse/Esse.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..134d93b --- /dev/null +++ b/Esse/Esse.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "dsfappearancemanager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/DSFAppearanceManager", + "state" : { + "revision" : "676cfd4e6aaa3cf6422990734c22544373db5c1a", + "version" : "3.3.0" + } + }, + { + "identity" : "dsfquickactionbar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/DSFQuickActionBar.git", + "state" : { + "revision" : "eebbaec76cf6ab8404971f952da90ae9be29adac", + "version" : "6.0.1" + } + }, + { + "identity" : "launchatlogin-modern", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern", + "state" : { + "branch" : "main", + "revision" : "9c41991631605c8ccfe0347bbcb5c659169f2ec5" + } + }, + { + "identity" : "neon", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Neon.git", + "state" : { + "revision" : "7df7d080a271cfa4dc87f94cccc024665a75047e", + "version" : "0.6.0" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "5ff7f3363f7a08f77e0d761e38e6add31c2136e1", + "version" : "1.8.1" + } + }, + { + "identity" : "sttextkitplus", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/STTextKitPlus", + "state" : { + "revision" : "73970cbd47bdf3d640afdad9a16edb1521b708a3", + "version" : "0.1.2" + } + }, + { + "identity" : "sttextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/STTextView", + "state" : { + "revision" : "8c4b880259f122c5ff4283adc698ac0d5d4b9027", + "version" : "0.9.6" + } + }, + { + "identity" : "sttextview-plugin-neon", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/STTextView-Plugin-Neon", + "state" : { + "revision" : "dc37eb270e2763f59dacc9559252fd1bb641a329", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter", + "state" : { + "revision" : "2599e95310b3159641469d8a21baf2d3d200e61f", + "version" : "0.8.0" + } + }, + { + "identity" : "tree-sitter-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/tree-sitter-xcframework", + "state" : { + "revision" : "8c11cc2d299a5afed6d8ae77b29c8204743b1c4f", + "version" : "0.208.5" + } + } + ], + "version" : 2 +} diff --git a/Esse/Esse.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist b/Esse/Esse.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..302eff2 --- /dev/null +++ b/Esse/Esse.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,27 @@ + + + + + SchemeUserState + + Esse.xcscheme_^#shared#^_ + + orderHint + 0 + + EsseCommandLine.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + FA853DCA2A33AD71007D6889 + + primary + + + + + diff --git a/Esse/Esse/AppDelegate.swift b/Esse/Esse/AppDelegate.swift new file mode 100644 index 0000000..86f5cab --- /dev/null +++ b/Esse/Esse/AppDelegate.swift @@ -0,0 +1,21 @@ +import Cocoa +import SwiftUI + +final class AppDelegate: NSObject, NSApplicationDelegate { + private var documentController: DocumentController! + + func applicationWillFinishLaunching(_: Notification) { + @AppStorage("appearance") var appearance: AppearanceOptions = .System + appearance.applyAppearance() + } + + func applicationDidFinishLaunching(_: Notification) { + NSWindow.allowsAutomaticWindowTabbing = true + documentController = DocumentController() + DispatchQueue.main.async { + if NSDocumentController.shared.documents.isEmpty { + self.documentController.createAndOpenDefaultDocument() + } + } + } +} diff --git a/Esse/Esse/Assets.xcassets/AccentColor.colorset/Contents.json b/Esse/Esse/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Esse/Esse/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/Contents.json b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b6b05dc --- /dev/null +++ b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,74 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "images" : [ + { + "idiom" : "ios-marketing", + "filename" : "icon_1024.png", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "scale" : "1x", + "size" : "16x16", + "filename" : "icon_16.png", + "idiom" : "mac" + }, + { + "scale" : "2x", + "size" : "16x16", + "filename" : "icon_32.png", + "idiom" : "mac" + }, + { + "idiom" : "mac", + "scale" : "1x", + "filename" : "icon_32.png", + "size" : "32x32" + }, + { + "size" : "32x32", + "filename" : "icon_64.png", + "idiom" : "mac", + "scale" : "2x" + }, + { + "scale" : "1x", + "idiom" : "mac", + "size" : "128x128", + "filename" : "icon_128.png" + }, + { + "scale" : "2x", + "filename" : "icon_256.png", + "idiom" : "mac", + "size" : "128x128" + }, + { + "filename" : "icon_256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_512.png", + "size" : "256x256", + "scale" : "2x", + "idiom" : "mac" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x", + "filename" : "icon_512.png" + }, + { + "filename" : "icon_1024.png", + "size" : "512x512", + "idiom" : "mac", + "scale" : "2x" + } + ] +} \ No newline at end of file diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 0000000..556670e Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_1024.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_128.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_128.png new file mode 100644 index 0000000..9949288 Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_128.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_16.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_16.png new file mode 100644 index 0000000..c5902fc Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_16.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_256.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_256.png new file mode 100644 index 0000000..ceec8eb Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_256.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_32.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_32.png new file mode 100644 index 0000000..8a63a72 Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_32.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_512.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_512.png new file mode 100644 index 0000000..652070f Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_512.png differ diff --git a/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_64.png b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_64.png new file mode 100644 index 0000000..4ad9169 Binary files /dev/null and b/Esse/Esse/Assets.xcassets/AppIcon.appiconset/icon_64.png differ diff --git a/Esse/Esse/Assets.xcassets/Contents.json b/Esse/Esse/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Esse/Esse/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Esse/Esse/Esse.entitlements b/Esse/Esse/Esse.entitlements new file mode 100644 index 0000000..a567037 --- /dev/null +++ b/Esse/Esse/Esse.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.icloud-container-identifiers + + com.apple.developer.icloud-services + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.ameba.esse + + com.apple.security.files.user-selected.read-write + + + diff --git a/Esse/Esse/EsseApp.swift b/Esse/Esse/EsseApp.swift new file mode 100644 index 0000000..0a94808 --- /dev/null +++ b/Esse/Esse/EsseApp.swift @@ -0,0 +1,53 @@ +import SwiftUI + +@main +struct EsseApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @AppStorage("showMenubarExtra") private var showMenuBarExtra = true + + var body: some Scene { + DocumentGroup(newDocument: EsseDocument()) { file in + MacMainView(document: file.$document) + } + .commands { + CustomFileCommands() + CustomViewCommands() + LibraryCommands() + } + Window("Library", id: "library") { + LibraryView() + } + Settings { + SettingsView() + } + } +} + +class DocumentController: ObservableObject { + func createAndOpenDefaultDocument() { + let document = EsseDocument() + let documentURL = getAppSupportDirectory().appendingPathComponent("Esse.txt") + do { + let data = String(document.text.characters).data(using: .utf8)! + try data.write(to: documentURL) + NSDocumentController.shared.openDocument(withContentsOf: documentURL, display: true) { _, _, _ in } + } catch { + print("Failed to create default document: \(error)") + } + } + + private func getAppSupportDirectory() -> URL { + let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) + let appSupportDirectory = paths[0] + + if !FileManager.default.fileExists(atPath: appSupportDirectory.path) { + do { + try FileManager.default.createDirectory(at: appSupportDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Failed to create Application Support directory: \(error)") + } + } + + return appSupportDirectory + } +} diff --git a/Esse/Esse/Info.plist b/Esse/Esse/Info.plist new file mode 100644 index 0000000..cf7efdd --- /dev/null +++ b/Esse/Esse/Info.plist @@ -0,0 +1,109 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.ameba.esse.open + CFBundleURLSchemes + + esse + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Esse + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundleGetInfoString + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + 218 + INIntentsSupported + + RunFunctionIntent + RunFunctionFileIntent + + LSApplicationCategoryType + public.app-category.utilities + LSRequiresIPhoneOS + + NSHumanReadableCopyright + Copyright ยฉ 2023 Ameba Labs. All rights reserved. + NSServices + + + NSKeyEquivalent + + default + ~@^E + + NSMenuItem + + default + Send to Esse + + NSMessage + textServiceHandler + NSPortName + Esse + NSRequiredContext + + NSSendTypes + + NSStringPboardType + + + + NSUbiquitousContainers + + iCloud.com.ameba.esse + + NSUbiquitousContainerIsDocumentScopePublic + + NSUbiquitousContainerName + Esse + NSUbiquitousContainerSupportedFolderLevels + Any + + + NSUserActivityTypes + + RunFunctionFileIntent + RunFunctionIntent + TextFunctionIntent + TextIntentIntent + + CFBundleDocumentTypes + + + CFBundleTypeName + Plain Text File + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + LSItemContentTypes + + public.plain-text + + + + + diff --git a/Esse/Esse/Intents/Intent.swift b/Esse/Esse/Intents/Intent.swift new file mode 100644 index 0000000..93a25f5 --- /dev/null +++ b/Esse/Esse/Intents/Intent.swift @@ -0,0 +1,55 @@ +import AppIntents +import EsseCore +import Foundation + +struct EsseFunctionEntity: AppEntity, Identifiable { + let id: String + let name: String + let desc: String + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(name)") + } + + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Function" + static var defaultQuery = FunctionQuery() +} + +struct FunctionQuery: EntityQuery { + func entities(for identifiers: [EsseFunctionEntity.ID]) async throws -> [EsseFunctionEntity] { + let filtered = Storage.sharedInstance.pAllFunctions.filter { identifiers.contains($0.id) } + return filtered.map { EsseFunctionEntity(id: $0.id, name: $0.title, desc: $0.desc) } + } + + func entities(matching string: String) async throws -> [EsseFunctionEntity] { + let filtered = Storage.sharedInstance.pAllFunctions.filter { $0.searchableText.score(word: string) > 0.4 }.sorted { $0.searchableText.score(word: string) > $1.searchableText.score(word: string) } + return filtered.map { EsseFunctionEntity(id: $0.id, name: $0.title, desc: $0.desc) } + } + + func suggestedEntities() async throws -> [EsseFunctionEntity] { + Storage.sharedInstance.pAllFunctions.map { EsseFunctionEntity(id: $0.id, name: $0.title, desc: $0.desc) } + } +} + +struct RunEsseFunction: AppIntent { + static var title: LocalizedStringResource = "Run Esse Function" + static var description = IntentDescription("Applies Esse function on provided input") + + @Parameter(title: "Function") + var function: EsseFunctionEntity + + @Parameter(title: "Input") + var input: String + + static var parameterSummary: some ParameterSummary { + Summary("Transform \(\.$input) with \(\.$function)") + } + + func perform() async throws -> some IntentResult & ReturnsValue { + guard let f = Storage.sharedInstance.pAllFunctions.first(where: { $0.id == function.id }) else { + return .result(value: "") + } + + return .result(value: f.run(input)) + } +} diff --git a/Esse/Esse/Preview Content/Preview Assets.xcassets/Contents.json b/Esse/Esse/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Esse/Esse/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Esse/Esse/macOS/EsseDocument.swift b/Esse/Esse/macOS/EsseDocument.swift new file mode 100644 index 0000000..7e5a82b --- /dev/null +++ b/Esse/Esse/macOS/EsseDocument.swift @@ -0,0 +1,29 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct EsseDocument: FileDocument { + var text: AttributedString + + init(text: AttributedString = AttributedString("")) { + self.text = text + } + + static var readableContentTypes: [UTType] { [.plainText] } + + init(configuration: ReadConfiguration) throws { + guard let data = configuration.file.regularFileContents, + let string = String(data: data, encoding: .utf8) + else { + throw CocoaError(.fileReadCorruptFile) + } + text = AttributedString(string) + } + + func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { + let string = String(text.characters) + guard let data = string.data(using: .utf8) else { + throw CocoaError(.fileWriteUnknown) + } + return .init(regularFileWithContents: data) + } +} diff --git a/Esse/Esse/macOS/Helpers/Appearance.swift b/Esse/Esse/macOS/Helpers/Appearance.swift new file mode 100644 index 0000000..52cea55 --- /dev/null +++ b/Esse/Esse/macOS/Helpers/Appearance.swift @@ -0,0 +1,18 @@ +import SwiftUI + +enum AppearanceOptions: String, CaseIterable { + case System + case Dark + case Light + + func applyAppearance() { + switch self { + case .System: + NSApp.appearance = nil + case .Dark: + NSApp.appearance = NSAppearance(named: .darkAqua) + case .Light: + NSApp.appearance = NSAppearance(named: .aqua) + } + } +} diff --git a/Esse/Esse/macOS/Helpers/EnumPickerVIew.swift b/Esse/Esse/macOS/Helpers/EnumPickerVIew.swift new file mode 100644 index 0000000..f3cf6e2 --- /dev/null +++ b/Esse/Esse/macOS/Helpers/EnumPickerVIew.swift @@ -0,0 +1,24 @@ +import SwiftUI + +public struct EnumPickerView: View { + @Binding public var selected: T + public var title: String? + + public let mapping: (T) -> V + + public var body: some View { + Picker(selection: $selected, label: Text(title ?? "")) { + ForEach(Array(T.allCases), id: \.self) { + mapping($0).tag($0) + } + } + } +} + +public extension EnumPickerView where T: RawRepresentable, T.RawValue == String, V == Text { + init(selected: Binding, title: String? = nil) { + self.init(selected: selected, title: title) { + Text($0.rawValue) + } + } +} diff --git a/Esse/Esse/macOS/Helpers/Helpers.swift b/Esse/Esse/macOS/Helpers/Helpers.swift new file mode 100644 index 0000000..bea6730 --- /dev/null +++ b/Esse/Esse/macOS/Helpers/Helpers.swift @@ -0,0 +1,33 @@ +import EsseCore + +extension Storage { + var sidebarItems: [SidebarItem] { + var out: [SidebarItem] = [] + for category in FunctionCategory.allCases { + var parentItem = SidebarItem(title: category.rawValue, textFunction: nil) + parentItem.children = pAllFunctions.filter { $0.category == category }.map { SidebarItem(id: $0.id, title: $0.title, textFunction: $0) } + out.append(parentItem) + } + return out + } + + func filterFunctions(searchTerm: String) -> [TextFunction] { + guard !searchTerm.isEmpty else { return [] } + let term = searchTerm.lowercased() + return pAllFunctions.filter { $0.searchableText.score(word: term) > 0.4 }.sorted { $0.searchableText.score(word: term) > $1.searchableText.score(word: term) } + } + + func filteredSidebarItems(searchTerm: String) -> [SidebarItem] { + let functions = filterFunctions(searchTerm: searchTerm) + + var out: [SidebarItem] = [] + for category in FunctionCategory.allCases { + var parentItem = SidebarItem(title: category.rawValue, textFunction: nil) + parentItem.children = functions.filter { $0.category == category }.map { SidebarItem(id: $0.id, title: $0.title, textFunction: $0) } + if !parentItem.children.isEmpty { + out.append(parentItem) + } + } + return out + } +} diff --git a/Esse/Esse/macOS/Main/FilterCellView.swift b/Esse/Esse/macOS/Main/FilterCellView.swift new file mode 100644 index 0000000..82b84ff --- /dev/null +++ b/Esse/Esse/macOS/Main/FilterCellView.swift @@ -0,0 +1,33 @@ +import EsseCore +import SwiftUI + +struct FilterCellView: View { + let textFunction: TextFunction + + var body: some View { + HStack { +// Image(systemName: "bolt") +// .font(.title) + VStack(alignment: .leading) { + HStack { + Text(textFunction.title) + .font(.title2) + .fontWeight(.medium) + Spacer() + } + HStack { + Text(textFunction.desc) + .font(.body) + .foregroundStyle(.secondary) + .italic() + Spacer() + } + } + }.environment(\.colorScheme, .dark) + } +} + +#Preview { + FilterCellView(textFunction: Storage.sharedInstance.pAllFunctions.randomElement()!) + .frame(width: 300).padding() +} diff --git a/Esse/Esse/macOS/Main/FooterView.swift b/Esse/Esse/macOS/Main/FooterView.swift new file mode 100644 index 0000000..a629de5 --- /dev/null +++ b/Esse/Esse/macOS/Main/FooterView.swift @@ -0,0 +1,179 @@ +import EsseCore +import SwiftUI + +struct FooterView: View { + @Binding var text: AttributedString + @Binding var transformedText: String + @Binding var functionTrigger: Bool + @Binding var isMultiEditorMode: Bool + @Binding var selectedFunctions: [TextFunction] + @State var floatingEnabled: Bool = false + @State var isHovering: Bool = false + @State var activeTootltip: String = "" + + @State var showSheet: Bool = false + + var isMainWindowFloating: Bool { + NSApp.mainWindow?.level == .floating + } + + var textString: String { + String(text.characters) + } + + var statsText: String { + "\(textString.lines().count) lines โ€ข \(textString.words().count) words โ€ข \(textString.count) characters" + } + + var body: some View { + HStack { + Text(isHovering ? activeTootltip : statsText) + .font(.callout) + .foregroundStyle(.secondary) + .padding(.leading) + + Spacer() + + if isMultiEditorMode { + Button(action: { + showSheet = true + }) { + Image(systemName: "function") + .font(.system(size: 19)) + } + .buttonStyle(.plain) + .opacity(functionTrigger ? 1 : 0.5) + .foregroundColor(functionTrigger ? .blue : .primary) + .animation(.bouncy, value: functionTrigger) + .onHover { hovering in + activeTootltip = "Show Selected Functions" + isHovering = hovering + } + .popover(isPresented: $showSheet, content: { + SelectedFunctionsView(functions: $selectedFunctions, isPresented: $showSheet) + }) + Divider() + } + + Button(action: { + NSApp.mainWindow?.level = (isMainWindowFloating ? .normal : .floating) + floatingEnabled.toggle() + }) { + Image(systemName: floatingEnabled ? "pin.circle.fill" : "pin.circle") + .font(.system(size: 19)) + } + .buttonStyle(.plain) + .opacity(0.5) + .onHover { hovering in + activeTootltip = floatingEnabled ? "Behave like a Normal Window" : "Float on Top of All Other Windows" + isHovering = hovering + } + + ShareLink(item: isMultiEditorMode ? transformedText : textString) { + Image(systemName: "arrow.up.circle.fill") + .font(.system(size: 19)) + } + .buttonStyle(.plain) + .opacity(0.5) + .padding(.trailing, 8) + .onHover { hovering in + activeTootltip = "Share All Text" + isHovering = hovering + } + } + } +} + +struct SelectedFunctionsView: View { + @Binding var functions: [TextFunction] + @Binding var isPresented: Bool + + var body: some View { + VStack { + if functions.isEmpty { + Text("No Functions Selected") + .font(.title2) + .foregroundStyle(.secondary) + } else { + List { + ForEach(Array(functions.enumerated()), id: \.element) { index, item in + HStack { + Text("\(index + 1).") + Text(item.title) + Spacer() + Button(action: { + delete(at: index) + }) { + Image(systemName: "x.circle") + .foregroundColor(.red) + .fontWeight(.bold) + } + .buttonStyle(.plain) + .opacity(0.5) + } + } + .onMove(perform: move) + } + Spacer() + HStack { + Button(action: { + functions.removeAll() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isPresented = false + } + }) { + Label( + title: { Text("Remove All") }, + icon: { Image(systemName: "trash") } + ) + } + .buttonStyle(.borderedProminent) + .opacity(0.5) + } + } + } + .padding(.bottom) + .frame(width: 300, height: 300) + } + + private func move(from source: IndexSet, to destination: Int) { + functions.move(fromOffsets: source, toOffset: destination) + } + + private func delete(at index: Int) { + functions.remove(at: index) + } +} + +struct ReorderableList: View { + @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4"] + + var body: some View { + List { + ForEach(items, id: \.self) { item in + Text(item) + Spacer() + Button("Delete") { + delete(item: item) + } + } + .onMove(perform: move) + } + } + + private func move(from source: IndexSet, to destination: Int) { + items.move(fromOffsets: source, toOffset: destination) + } + + private func delete(item: String) { + if let index = items.firstIndex(of: item) { + items.remove(at: index) + } + } +} + +#Preview { + FooterView(text: .constant("Hello, World! \n Oh, Yeah!"), transformedText: .constant("Hello, World! \n Oh, Yeah!"), functionTrigger: .constant(true), isMultiEditorMode: .constant(true), selectedFunctions: .constant([Storage.sharedInstance.pAllFunctions.randomElement()!])) + .frame(height: 15) + .padding() +} diff --git a/Esse/Esse/macOS/Main/InspectorView.swift b/Esse/Esse/macOS/Main/InspectorView.swift new file mode 100644 index 0000000..908ac34 --- /dev/null +++ b/Esse/Esse/macOS/Main/InspectorView.swift @@ -0,0 +1,74 @@ +import EsseCore +import SwiftUI + +struct InspectorView: View { + @Binding var textFunction: TextFunction? + @State var inputText: String = """ + Hereโ€™s to the Crazy Ones! + The misfits. + The rebels. + The troublemakers. + The round pegs in the square holes. + The ones who see things differently. + + Some numbers: 1233 + """ + @State var outputText: String = "" + var body: some View { + if textFunction == nil { + Text("Select a Function") + .font(.title2) + .foregroundStyle(.secondary) + } else { + Form { + Section(content: { + VStack(alignment: .leading) { + Text("") + Text(textFunction!.desc) + } + }, header: { + Text(textFunction!.title) + .font(.title) + .multilineTextAlignment(.leading) + }) + + Divider() + + Section(content: { + TextEditor(text: $inputText) + .scrollIndicators(.never) + .frame(height: 200) + .onChange(of: inputText) { value, _ in + outputText = textFunction!.run(value) + } + }, header: { + Text("Input") + .font(.title2) + .multilineTextAlignment(.leading) + }) + Section(content: { + TextEditor(text: $outputText) + .scrollIndicators(.never) + .frame(height: 200) + }, header: { + Text("Output") + .font(.title2) + .multilineTextAlignment(.leading) + }) + Spacer() + } + .formStyle(.automatic) + .onAppear { + outputText = textFunction?.run(inputText) ?? "" + } + .onChange(of: $textFunction.wrappedValue) { + outputText = textFunction?.run(inputText) ?? "" + } + } + } +} + +#Preview { + InspectorView(textFunction: .constant(Storage.sharedInstance.pAllFunctions.randomElement()!)) + .frame(width: 300, height: 800) +} diff --git a/Esse/Esse/macOS/Main/LibraryView.swift b/Esse/Esse/macOS/Main/LibraryView.swift new file mode 100644 index 0000000..1d156de --- /dev/null +++ b/Esse/Esse/macOS/Main/LibraryView.swift @@ -0,0 +1,43 @@ +import EsseCore +import SwiftUI + +struct SidebarItem: Identifiable, Hashable { + var id: String = UUID().uuidString + let title: String + var children: [SidebarItem] = [] + let isExpanded: Bool = false + let textFunction: TextFunction? +} + +struct LibraryView: View { + @State var searchTerm = "" + @State var selectedFunction: TextFunction? + + let sidebarItems: [SidebarItem] = Storage.sharedInstance.sidebarItems + + var body: some View { + NavigationView { + SidebarView(sidebarItems: sidebarItems, selectedFunction: $selectedFunction, searchTerm: $searchTerm) + .searchable(text: $searchTerm, placement: .sidebar) + .frame(minWidth: 320) + InspectorView(textFunction: $selectedFunction) + .padding() + } + .toolbar { + ToolbarItem(placement: .navigation) { + Button(action: toggleSidebar, label: { + Image(systemName: "sidebar.leading") + }) + } + } + .navigationTitle("Esse Library") + } + + private func toggleSidebar() { + NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) + } +} + +#Preview { + LibraryView() +} diff --git a/Esse/Esse/macOS/Main/MacMainView.swift b/Esse/Esse/macOS/Main/MacMainView.swift new file mode 100644 index 0000000..716e11d --- /dev/null +++ b/Esse/Esse/macOS/Main/MacMainView.swift @@ -0,0 +1,132 @@ +import AppKit +import DSFQuickActionBar +import EsseCore +import NeonPlugin +import STTextViewUI +import SwiftUI + +struct MacMainView: View { + @Binding var document: EsseDocument + @Environment(\.openWindow) private var openWindow + + @Environment(\.appearsActive) private var appearsActive + + @State private var nonEditableText: String = "" + + @State var searchTerm = "" + @State var quickSearchIsVisible = false + + @AppStorage("dualPaneModeEnabled") var isMultiEditorMode: Bool = false + @AppStorage("showLineNumbers") var showLineNumbers: Bool = true + @AppStorage("highlightSelectedLine") var highlightSelectedLine: Bool = true + + @State var selectedFunction: TextFunction? + @State var selectedFunctions: [TextFunction] = [] + @State var functionTrigger: Bool = false + @State private var selection: NSRange? + + var body: some View { + VStack { + GeometryReader { geometry in + HStack(spacing: 0) { + TextView(text: $document.text, + selection: $selection, + showLineNumbers: $showLineNumbers, + highlightSelectedLine: $highlightSelectedLine, + plugins: []) + .frame(width: isMultiEditorMode ? geometry.size.width / 2 : .infinity) + .onChange(of: document.text) { _, value in + nonEditableText = selectedFunctions.run(value: String(value.characters)) + } + if isMultiEditorMode { + TextEditor(text: $nonEditableText) + .multilineTextAlignment(.leading) + .frame(width: geometry.size.width / 2) + .font(.body) + } + } + } + .onChange(of: selectedFunction) { _, value in + guard let value else { return } + if isMultiEditorMode { + selectedFunctions.append(value) + } else { + document.text = AttributedString(value.run(String(document.text.characters))) + fireFunctionTrigger() + } + selectedFunction = nil + } + .onChange(of: selectedFunctions) { _, value in + if isMultiEditorMode { + nonEditableText = value.run(value: String(document.text.characters)) + fireFunctionTrigger() + } + } + .onReceive(NotificationCenter.default.publisher(for: .runFunctions), perform: { _ in + guard appearsActive else { return } + nonEditableText = selectedFunctions.run(value: String(document.text.characters)) + fireFunctionTrigger() + }) + .onReceive(NotificationCenter.default.publisher(for: .showCommandPallete), perform: { _ in + guard !quickSearchIsVisible, appearsActive else { return } + quickSearchIsVisible = true + }) + + FooterView(text: $document.text, + transformedText: $nonEditableText, + functionTrigger: $functionTrigger, + isMultiEditorMode: $isMultiEditorMode, + selectedFunctions: $selectedFunctions) + .frame(height: 15) + QuickActionBar( + location: .window, + visible: $quickSearchIsVisible, + showKeyboardShortcuts: true, + requiredClickCount: .single, + searchTerm: $searchTerm, + selectedItem: $selectedFunction, + placeholderText: "Quick Search", + itemsForSearchTerm: quickOpenFilter, + viewForItem: { textFunction, _ in + FilterCellView(textFunction: textFunction) + } + ) + } + .toolbar { + ToolbarItem { + Toggle(isOn: $isMultiEditorMode) { + Label("Editor Mode", systemImage: "rectangle.split.2x1") + } + .toggleStyle(.automatic) + } + ToolbarItem { + Button(action: { + quickSearchIsVisible = true + }) { + Image(systemName: "command") + } + } + ToolbarItem { + Button(action: { + openWindow(id: "library") + }) { + Image(systemName: "book") + } + } + } + } + + private func fireFunctionTrigger() { + functionTrigger = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + functionTrigger = false + } + } + + private func quickOpenFilter(_ task: DSFQuickActionBar.SearchTask) { + let searchTerm = task.searchTerm + var results: [TextFunction] = searchTerm.isEmpty ? Storage.sharedInstance.pAllFunctions : Storage.sharedInstance.filterFunctions(searchTerm: searchTerm) + results = results.filter { !selectedFunctions.contains($0) } + task.complete(with: results) + } +} diff --git a/Esse/Esse/macOS/Main/SidebarView.swift b/Esse/Esse/macOS/Main/SidebarView.swift new file mode 100644 index 0000000..cf75f06 --- /dev/null +++ b/Esse/Esse/macOS/Main/SidebarView.swift @@ -0,0 +1,33 @@ +import EsseCore +import SwiftUI + +struct SidebarView: View { + var sidebarItems: [SidebarItem] + @Binding var selectedFunction: TextFunction? + @State private var selectedItem: SidebarItem? + @State private var isExpanded: Bool = true + @Binding var searchTerm: String + + var body: some View { + List(selection: $selectedItem) { + ForEach(searchTerm.isEmpty ? sidebarItems : Storage.sharedInstance.filteredSidebarItems(searchTerm: searchTerm), id: \.self) { item in + DisclosureGroup(item.title, isExpanded: $isExpanded) { + ForEach(item.children, id: \.self) { subItem in + Text(subItem.title) + .tag(subItem) + .contentShape(Rectangle()) + } + } + } + }.onChange(of: selectedItem) { + selectedFunction = selectedItem?.textFunction + } + .onChange(of: searchTerm) { + isExpanded = !searchTerm.isEmpty + } + } +} + +#Preview { + SidebarView(sidebarItems: Storage.sharedInstance.sidebarItems, selectedFunction: .constant(Storage.sharedInstance.pAllFunctions.first), searchTerm: .constant("")) +} diff --git a/Esse/Esse/macOS/Main/TextView.swift b/Esse/Esse/macOS/Main/TextView.swift new file mode 100644 index 0000000..13d8825 --- /dev/null +++ b/Esse/Esse/macOS/Main/TextView.swift @@ -0,0 +1,310 @@ +//Taken and refactored from https://github.com/krzyzanowskim/STTextView/blob/main/Sources/STTextViewUI/TextView.swift + +import AppKit +import STTextView +import SwiftUI + +private struct FontEnvironmentKey: EnvironmentKey { + static var defaultValue: NSFont = .preferredFont(forTextStyle: .body) +} + +extension EnvironmentValues { + var font: NSFont { + get { self[FontEnvironmentKey.self] } + set { self[FontEnvironmentKey.self] = newValue } + } +} + +public struct TextView: SwiftUI.View { + @frozen + public struct Options: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Breaks the text as needed to fit within the bounding box. + public static let wrapLines = Options(rawValue: 1 << 0) + + /// Highlighted selected line + public static let highlightSelectedLine = Options(rawValue: 1 << 1) + } + + @Environment(\.colorScheme) private var colorScheme + @Binding private var text: AttributedString + @Binding private var selection: NSRange? + @Binding private var showLineNumbers: Bool + @Binding private var highlightSelectedLine: Bool + private let options: Options + private let plugins: [any STPlugin] + + /// Create a text edit view with a certain text that uses a certain options. + /// - Parameters: + /// - text: The attributed string content + /// - options: Editor options + /// - plugins: Editor plugins + public init( + text: Binding, + selection: Binding = .constant(nil), + showLineNumbers: Binding = .constant(false), + highlightSelectedLine: Binding = .constant(false), + plugins: [any STPlugin] = [] + ) { + _text = text + _selection = selection + _showLineNumbers = showLineNumbers + _highlightSelectedLine = highlightSelectedLine + self.options = [.highlightSelectedLine, .wrapLines] + self.plugins = plugins + } + + public var body: some View { + TextViewRepresentable( + text: $text, + selection: $selection, + showLineNumbers: $showLineNumbers, + highlightSelectedLine: $highlightSelectedLine, + options: options, + plugins: plugins + ) + .background(.background) + } +} + +private struct TextViewRepresentable: NSViewRepresentable { + @Environment(\.isEnabled) private var isEnabled + @Environment(\.font) private var font + @Environment(\.lineSpacing) private var lineSpacing + + @Binding private var text: AttributedString + @Binding private var selection: NSRange? + @Binding private var showLineNumbers: Bool + @Binding private var highlightSelectedLine: Bool + private let options: TextView.Options + private var plugins: [any STPlugin] + + init(text: Binding, selection: Binding, showLineNumbers: Binding, highlightSelectedLine: Binding, options: TextView.Options, plugins: [any STPlugin] = []) { + _text = text + _selection = selection + _showLineNumbers = showLineNumbers + _highlightSelectedLine = highlightSelectedLine + self.options = options + self.plugins = plugins + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollView = STTextView.scrollableTextView() + let textView = scrollView.documentView as! STTextView + textView.delegate = context.coordinator + textView.highlightSelectedLine = options.contains(.highlightSelectedLine) + textView.widthTracksTextView = options.contains(.wrapLines) + textView.setSelectedRange(NSRange()) + + context.coordinator.isUpdating = true + textView.setAttributedString(NSAttributedString(styledAttributedString(textView.typingAttributes))) + context.coordinator.isUpdating = false + + for plugin in plugins { + textView.addPlugin(plugin) + } + + scrollView.hasHorizontalScroller = false + + setupLineNumberView(scrollView, textView) + + return scrollView + } + + func updateNSView(_ scrollView: NSScrollView, context: Context) { + context.coordinator.parent = self + + let textView = scrollView.documentView as! STTextView + + do { + context.coordinator.isUpdating = true + if context.coordinator.isDidChangeText == false { + textView.setAttributedString(NSAttributedString(styledAttributedString(textView.typingAttributes))) + } + context.coordinator.isUpdating = false + context.coordinator.isDidChangeText = false + } + + if textView.selectedRange() != selection, let selection { + textView.setSelectedRange(selection) + } + + if textView.isEditable != isEnabled { + textView.isEditable = isEnabled + } + + if textView.isSelectable != isEnabled { + textView.isSelectable = isEnabled + } + + let wrapLines = options.contains(.wrapLines) + if wrapLines != textView.widthTracksTextView { + textView.widthTracksTextView = options.contains(.wrapLines) + } + textView.highlightSelectedLine = highlightSelectedLine + scrollView.rulersVisible = showLineNumbers + + if textView.font != font { + textView.font = font + } + } + + private func setupLineNumberView(_ scrollView: NSScrollView, _ textView: STTextView) { + let rulerView = STLineNumberRulerView(textView: textView) + rulerView.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) + rulerView.highlightSelectedLine = true + scrollView.verticalRulerView = rulerView + scrollView.rulersVisible = true + } + + func makeCoordinator() -> TextCoordinator { + TextCoordinator(parent: self) + } + + private func styledAttributedString(_ typingAttributes: [NSAttributedString.Key: Any]) -> AttributedString { + let paragraph = (typingAttributes[.paragraphStyle] as! NSParagraphStyle).mutableCopy() as! NSMutableParagraphStyle + if paragraph.lineSpacing != lineSpacing { + paragraph.lineSpacing = lineSpacing + var typingAttributes = typingAttributes + typingAttributes[.paragraphStyle] = paragraph + + let attributeContainer = AttributeContainer(typingAttributes) + var styledText = text + styledText.mergeAttributes(attributeContainer, mergePolicy: .keepNew) + return styledText + } + + return text + } + + class TextCoordinator: STTextViewDelegate { + var parent: TextViewRepresentable + var isUpdating: Bool = false + var isDidChangeText: Bool = false + var enqueuedValue: AttributedString? + + init(parent: TextViewRepresentable) { + self.parent = parent + } + + func textViewDidChangeText(_ notification: Notification) { + guard let textView = notification.object as? STTextView else { + return + } + + if !isUpdating { + let newTextValue = AttributedString(textView.attributedString()) + DispatchQueue.main.async { + self.isDidChangeText = true + self.parent.text = newTextValue + } + } + } + + func textViewDidChangeSelection(_ notification: Notification) { + guard let textView = notification.object as? STTextView else { + return + } + + Task { @MainActor in + self.isDidChangeText = true + self.parent.selection = textView.selectedRange() + } + } + } +} + +// struct STTextViewWrapper: NSViewRepresentable { +// @Binding var text: AttributedString +// var font: NSFont +// var highlightSelectedLine: Bool +// var showLineNumbers: Bool +// var plugins: [any STPlugin] // New parameter for plugins +// +// func makeNSView(context: Context) -> NSScrollView { +// let scrollView = STTextView.scrollableTextView() +// let textView = scrollView.documentView as! STTextView +// +// setupTextView(textView) +// setupScrollView(scrollView) +// +// if showLineNumbers { +// setupLineNumberView(scrollView, textView) +// } +// +// // Apply plugins +// for plugin in plugins { +// textView.addPlugin(plugin) +// } +// +// textView.delegate = context.coordinator +// +// return scrollView +// } +// +// func updateNSView(_ nsView: NSScrollView, context: Context) { +// guard let textView = nsView.documentView as? STTextView else { return } +// +// if textView.attributedString() != NSAttributedString(text) { +// textView.setAttributedString(NSAttributedString(text)) +// } +// +// // Update plugins if needed +// updatePlugins(textView) +// } +// +// func makeCoordinator() -> Coordinator { +// Coordinator(self) +// } +// +// private func setupTextView(_ textView: STTextView) { +// let paragraph = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle +// paragraph.lineHeightMultiple = 1.2 +// textView.typingAttributes[.paragraphStyle] = paragraph +// textView.font = font +// textView.isHorizontallyResizable = false +// textView.highlightSelectedLine = highlightSelectedLine +// textView.isIncrementalSearchingEnabled = true +// textView.showsInvisibleCharacters = false +// } +// +// private func setupScrollView(_ scrollView: NSScrollView) { +// scrollView.hasVerticalScroller = true +// scrollView.hasHorizontalScroller = false +// scrollView.drawsBackground = true +// } +// +// private func setupLineNumberView(_ scrollView: NSScrollView, _ textView: STTextView) { +// let rulerView = STLineNumberRulerView(textView: textView) +// rulerView.font = font +// rulerView.allowsMarkers = true +// rulerView.highlightSelectedLine = highlightSelectedLine +// scrollView.verticalRulerView = rulerView +// scrollView.rulersVisible = true +// } +// +// private func updatePlugins(_ textView: STTextView) { +// // Add new plugins +// for plugin in plugins { +// textView.addPlugin(plugin) +// } +// } +// +// class Coordinator: NSObject, STTextViewDelegate { +// var parent: STTextViewWrapper +// +// init(_ parent: STTextViewWrapper) { +// self.parent = parent +// } +// +// func textDidChange(_ notification: Notification) { +// guard let textView = notification.object as? STTextView else { return } +// parent.text = AttributedString(textView.attributedString()) +// } +// } +// } diff --git a/Esse/Esse/macOS/MenuCommands.swift b/Esse/Esse/macOS/MenuCommands.swift new file mode 100644 index 0000000..6969004 --- /dev/null +++ b/Esse/Esse/macOS/MenuCommands.swift @@ -0,0 +1,97 @@ +import SwiftUI +import EsseCore + +struct LibraryCommands: Commands { + @AppStorage("dualPaneModeEnabled") var isMultiEditorMode: Bool = false + @Environment(\.openWindow) private var openWindow + + var body: some Commands { + CommandMenu("Library") { + Button("Command Palette...", action: { + NotificationCenter.default.post(name: .showCommandPallete, object: nil) + }).keyboardShortcut("P", modifiers: [.command, .shift]) + + Button("Run", action: { + NotificationCenter.default.post(name: .runFunctions, object: nil) + }).keyboardShortcut("R", modifiers: [.command]) + + Divider() + + Button("Show Library...", action: { + openWindow(id: "library") + }).keyboardShortcut("L", modifiers: [.command, .shift]) + + Button("Open Scripts Folder", action: { + if let url = EsseCore.Sideload.sharedInstance.containerUrl { + NSWorkspace.shared.open(url) + } + }) + + Button("Get More Scripts", action: { + NSWorkspace.shared.open(URL(string: "https://github.com/amebalabs/Esse/tree/master/Scripts")!) + }) + } + } +} + +struct CustomViewCommands: Commands { + @AppStorage("dualPaneModeEnabled") var isMultiEditorMode: Bool = false + + var body: some Commands { + CommandGroup(after: CommandGroupPlacement.toolbar) { + Button(action: { + if let currentWindow = NSApp.keyWindow, + let windowController = currentWindow.windowController + { + windowController.newWindowForTab(nil) + if let newWindow = NSApp.keyWindow, currentWindow != newWindow { + currentWindow.addTabbedWindow(newWindow, ordered: .above) + } + } + }, label: { + Text("New Tab") + }) + .keyboardShortcut("t", modifiers: [.command]) + Button("Standard Mode", action: { + isMultiEditorMode = false + }) + .keyboardShortcut("1", modifiers: [.command]) + + Button("Two-pane Mode", action: { + isMultiEditorMode = true + }) + .keyboardShortcut("2", modifiers: [.command]) + + Divider() + } + } +} + +struct CustomFileCommands: Commands { + @AppStorage("userText") private var sourceText: String = "" + @Environment(\.openWindow) private var openWindow + + var body: some Commands { + CommandGroup(after: CommandGroupPlacement.newItem) { + Button("New from Clipboard", action: { + guard let clipboardText = NSPasteboard.general.string(forType: .string) else { + newWithText(text: "") + return + } + newWithText(text: clipboardText) + }) + .keyboardShortcut("N", modifiers: [.command, .shift]) + .disabled(!(NSPasteboard.general.types?.contains(.string) ?? false)) + } + } + + func openWindowIfNeeded() { + guard !NSApp.windows.contains(where: { $0.identifier?.rawValue.starts(with: "main") ?? false }) else { return } + openWindow(id: "main") + } + + func newWithText(text: String) { + sourceText = text + openWindowIfNeeded() + } +} diff --git a/Esse/Esse/macOS/Notifications.swift b/Esse/Esse/macOS/Notifications.swift new file mode 100644 index 0000000..bdb8c72 --- /dev/null +++ b/Esse/Esse/macOS/Notifications.swift @@ -0,0 +1,6 @@ +import Foundation + +extension Notification.Name { + static let runFunctions = Notification.Name("runFunctions") + static let showCommandPallete = Notification.Name("showCommandPallete") +} diff --git a/Esse/Esse/macOS/Settings/AboutSettingsView.swift b/Esse/Esse/macOS/Settings/AboutSettingsView.swift new file mode 100644 index 0000000..6a0e9d5 --- /dev/null +++ b/Esse/Esse/macOS/Settings/AboutSettingsView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct AboutSettingsView: View { + var body: some View { + VStack { + HStack { + Image(nsImage: NSImage(named: "AppIcon")!) + .resizable() + .renderingMode(.original) + .frame(width: 90, height: 90, alignment: .leading) + + VStack(alignment: .leading) { + if #available(macOS 11.0, *) { + Text("Esse") + .font(.title3) + .bold() + } + Text("Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""))") + .font(.subheadline) + .foregroundStyle(.secondary) + Text("ยฉ 2020-\(Calendar.current.component(.year, from: Date()).description) Ameba Labs. All rights reserved.") + .font(.footnote) + .foregroundStyle(.secondary) + .padding(.top, 10) + } + } + Divider() + HStack { + Spacer() + Button("Visit our Website", action: { + NSWorkspace.shared.open(URL(string: "https://esse.ameba.co")!) + }) + Button("Contact Us", action: { + NSWorkspace.shared.open(URL(string: "mailto:info@ameba.co")!) + }) + }.padding(.top, 10) + .padding(.bottom, 10) + }.padding(.trailing, 20) + .padding(.bottom, 10) + .frame(width: 410, height: 160) + } +} + +struct AboutSettingsView_Previews: PreviewProvider { + static var previews: some View { + AboutSettingsView() + } +} diff --git a/Esse/Esse/macOS/Settings/GeneralSettingsView.swift b/Esse/Esse/macOS/Settings/GeneralSettingsView.swift new file mode 100644 index 0000000..b16ba3e --- /dev/null +++ b/Esse/Esse/macOS/Settings/GeneralSettingsView.swift @@ -0,0 +1,31 @@ +import LaunchAtLogin +import SwiftUI + +struct GeneralSettingsView: View { + @AppStorage("appearance") private var appearance: AppearanceOptions = .System + @AppStorage("showLineNumbers") var showLineNumbers: Bool = true + @AppStorage("highlightSelectedLine") var highlightSelectedLine: Bool = true + + var body: some View { + Form { + LaunchAtLogin.Toggle() + Toggle(isOn: $showLineNumbers, label: { + Text("Show Line Numbers") + }) + Toggle(isOn: $highlightSelectedLine, label: { + Text("Highlight Selected Line") + }) + Spacer() + EnumPickerView(selected: $appearance, title: "Appearance") + } + .onChange(of: appearance) { _, value in + value.applyAppearance() + } + .padding(20) + .frame(width: 350, height: 100) + } +} + +#Preview { + GeneralSettingsView() +} diff --git a/Esse/Esse/macOS/Settings/Settings.swift b/Esse/Esse/macOS/Settings/Settings.swift new file mode 100644 index 0000000..bbb531c --- /dev/null +++ b/Esse/Esse/macOS/Settings/Settings.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct SettingsView: View { + private enum Tabs: Hashable { + case general, advanced + } + + var body: some View { + TabView { + GeneralSettingsView() + .tabItem { + Label("General", systemImage: "gear") + } + .tag(Tabs.general) + AboutSettingsView() + .tabItem { + Label("About", systemImage: "info") + } + } + .padding(20) + .frame(width: 400, height: 150) + } +} diff --git a/Esse/EsseCommandLine/EsseCommandLine.entitlements b/Esse/EsseCommandLine/EsseCommandLine.entitlements new file mode 100644 index 0000000..0536384 --- /dev/null +++ b/Esse/EsseCommandLine/EsseCommandLine.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + X93LWC49WV.com.ameba.esse + + com.apple.security.inherit + + + diff --git a/Esse/EsseCommandLine/main.swift b/Esse/EsseCommandLine/main.swift new file mode 100644 index 0000000..8059157 --- /dev/null +++ b/Esse/EsseCommandLine/main.swift @@ -0,0 +1,109 @@ +import ArgumentParser +import EsseCore +import Foundation + +let storage = Storage.sharedInstance + +struct Esse: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Swiss army knife of text transformation.", + version: "Esse version 2024.1", + subcommands: [List.self] + ) + + @Option(name: .short, help: "Transformation(s) to execute.") + var transformations: String? + + @Option(name: .short, help: "Text to transform.") + var input: String? + + mutating func run() throws { + var functions: [TextFunction] = [] + transformations?.split(separator: ",").forEach { id in + guard let f = storage.pAllFunctions.first(where: { $0.id.lowercased().contains(id.lowercased()) }) else { return } + functions.append(f) + } + + if let text = input { + print(runFunctions(text: text, functions: functions), terminator: "") + return + } + + let input = FileHandle.standardInput + if let text = String(bytes: input.availableData, encoding: .utf8) { + print(runFunctions(text: text, functions: functions), terminator: "") + return + } + } + + func runFunctions(text: String, functions: [TextFunction]) -> String { + let actions = functions.compactMap(\.actions).flatMap { $0 } + return actions.reduce(text) { $1($0) } + } +} + +extension Esse { + struct List: ParsableCommand, Decodable { + static var configuration = + CommandConfiguration(abstract: "Print list of available transformations.") + + @Flag(help: "Print only id.") + var onlyid: Int + + @Flag(help: "Alfred compatible output.") + var alfred: Int + + @Argument(help: "Transfromation.") + var transformation: String? + + mutating func run() { + var functions = storage.pAllFunctions + if let transformation { + functions = storage.pAllFunctions.filter { $0.id.lowercased().contains(transformation.lowercased()) } + } + + if functions.isEmpty { + print("No functions found.") + return + } + + if onlyid != 0 { + functions.forEach { print($0.id) } + return + } + + if alfred != 0 { + let alfredItems = functions.map(\.alfred) + let alfredOutput = TextFunction.AlfredOutput(items: alfredItems) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + guard let jsonData = try? encoder.encode(alfredOutput), let str = String(data: jsonData, encoding: .utf8) else { return } + print(str) + return + } + functions.forEach { print($0.description) } + } + } +} + +extension TextFunction { + struct AlfredOutput: Codable { + let items: [AlfredItem] + } + + struct AlfredItem: Codable { + let uid: String + let title: String + let subtitle: String + var match: String + var arg: String + var autocomplete: String + } + + var alfred: AlfredItem { + AlfredItem(uid: id, title: title, subtitle: desc, match: "\(title) \(desc)", arg: id, autocomplete: title) + } +} + +Esse.main() diff --git a/Esse/Packages/EsseCore/.gitignore b/Esse/Packages/EsseCore/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Esse/Packages/EsseCore/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Esse/Packages/EsseCore/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Esse/Packages/EsseCore/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Esse/Packages/EsseCore/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Esse/Packages/EsseCore/Package.swift b/Esse/Packages/EsseCore/Package.swift new file mode 100644 index 0000000..a03b4db --- /dev/null +++ b/Esse/Packages/EsseCore/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "EsseCore", + platforms: [.iOS(.v17), .macOS(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "EsseCore", + targets: ["EsseCore"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "EsseCore"), + .testTarget( + name: "EsseCoreTests", + dependencies: ["EsseCore"] + ), + ] +) diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/EmojiiMapping.swift b/Esse/Packages/EsseCore/Sources/EsseCore/EmojiiMapping.swift new file mode 100644 index 0000000..6f9546e --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/EmojiiMapping.swift @@ -0,0 +1,52 @@ +import Foundation + +typealias TextToEmojiMapping = [String: [String]] + +enum Emojii { + static let mapping: TextToEmojiMapping = parseEmojiiFile() + + private static func parseEmojiiFile() -> TextToEmojiMapping { + guard let path = Bundle(for: Storage.self).path(forResource: "emojis", ofType: "json"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let json = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonDictionary = json as? NSDictionary + else { + return [:] + } + var result: TextToEmojiMapping = [:] + for (key, value) in jsonDictionary { + if let key = key as? String, + let dictionary = value as? [String: AnyObject], + let emojiCharacter = dictionary["char"] as? String + { + // Dictionary keys from emojis.json have higher priority then keywords. + // That's why they're added at the beginning of the array. + addKey(key, value: emojiCharacter, atBeginning: true, to: &result) + + if let keywords = dictionary["keywords"] as? [String] { + for keyword in keywords { + addKey(keyword, value: emojiCharacter, atBeginning: false, to: &result) + } + } + } + } + return result + } + + private static func addKey(_ key: String, value: String, atBeginning: Bool, to dict: inout TextToEmojiMapping) { + // ignore short words because they're non-essential + guard key.lengthOfBytes(using: String.Encoding.utf8) > 2 else { + return + } + + if dict[key] == nil { + dict[key] = [] + } + + if atBeginning { + dict[key]?.insert(value, at: 0) + } else { + dict[key]?.append(value) + } + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/EsseCore.swift b/Esse/Packages/EsseCore/Sources/EsseCore/EsseCore.swift new file mode 100644 index 0000000..687536c --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/EsseCore.swift @@ -0,0 +1,132 @@ +import JavaScriptCore + +public typealias TextFunctionAction = (String) -> String + +public enum FunctionCategory: String, Codable, CaseIterable { + case Custom + case Cleaning + case Convert + case Case + case ASCII + case Extract + case QuotationMarks = "Quotation Marks" + case Other + case Developer +} + +public struct TextFunction { + public enum FunctionType { + case Standard + case Custom + case External + } + + public let id: String + public let type: FunctionType + public let author: String + public let title: String + public let desc: String + public let category: FunctionCategory + public var actions: [TextFunctionAction] = [] + public var externalFunction: String = "" + public var externalFileURL: URL? + public var functionIDs: [Int] = [] + public var searchableText: String { + (title + desc).lowercased() + } + + init(id: String, title: String, description: String, category: FunctionCategory = .Other, action: @escaping TextFunctionAction) { + self.id = id + type = .Standard + self.title = title + desc = description + author = "Ameba Labs" + self.category = category + actions.append(action) + } + + init(id: String, title: String, description: String, category: FunctionCategory = .Custom, actions: [TextFunctionAction]) { + self.id = id + type = .Custom + self.title = title + desc = description + author = "Ameba Labs" + self.category = category + self.actions = actions + } + + init(id: String, title: String, description: String, category: String, author: String, function: String, fileURL: URL) { + self.id = id + type = .External + self.title = title + desc = description + self.author = author + externalFileURL = fileURL + externalFunction = function + self.category = FunctionCategory(rawValue: category) ?? .Custom + actions = [ + { text -> String in + guard function != "" else { return text } + let context = JSContext() + context?.evaluateScript(function) + context?.exceptionHandler = { _, exception in + print(exception?.toString() ?? "") + } + let main = context?.objectForKeyedSubscript("main") + return main?.call(withArguments: [text])?.toString() ?? "" + }, + ] + } + + public func run(_ input: String) -> String { + actions.reduce(input) { $1($0) } + } + + public var description: String { + var out = "id: \(id) \n" + out = out + "title: \(title) \n" + out = out + "description: \(desc) \n" + out = out + "category: \(category) \n" + return out + } + + public var externalMemo: String { + var out = "Id: \(id) \n" + out = out + "Author: \(author) \n" + out = out + "File: \(externalFileURL?.lastPathComponent ?? "") \n" + return out + } +} + +extension TextFunction: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: TextFunction, rhs: TextFunction) -> Bool { + lhs.id == rhs.id + } +} + +public struct TextFunctionStorable: Codable { + let id: String + let title: String + let description: String + let category: FunctionCategory + var functionIDs: [String] = [] + + init(id: String, title: String, description: String, functionIDs: [String]) { + self.id = id + self.title = title + self.description = description + category = .Custom + self.functionIDs = functionIDs + } +} + +public extension [TextFunction] { + func run(value: String) -> String { + let actions = compactMap(\.actions).flatMap { $0 } + return actions.reduce(value) { $1($0) } + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Functions+CMOS.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Functions+CMOS.swift new file mode 100644 index 0000000..a8443db --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Functions+CMOS.swift @@ -0,0 +1,76 @@ +import JavaScriptCore + +func cmosJS(text: String) -> String { + let jsSource = CMOS + let context = JSContext() + context?.evaluateScript(jsSource) + + let testFunction = context?.objectForKeyedSubscript("TitleCapsEditor") + let out = testFunction?.call(withArguments: [text])?.toString() ?? "" + return out +} + +let CMOS = """ +function TitleCapsEditor(n) { +const m = 1; +const d = 2; +const c = 3; +const g = 4; +var j = g; +var l = ""; +var e = ["about", "above", "across", "after", "against", "along", "among", "around", "at", "before", "behind", "below", "beneath", "beside", "between", "beyond", "but", "by", "despite", "down", "during", "except", "for", "from", "in", "inside", "into", "like", "near", "of", "off", "on", "onto", "out", "outside", "over", "past", "per", "since", "through", "throughout", "till", "to", "toward", "under", "underneath", "until", "up", "upon", "via", "with", "within", "without"]; +var a = ["a", "an", "the"]; +var q = ["and", "but", "or", "nor", "for", "yet", "so"]; +var f = ["if", "en", "as", "vs.", "v[.]?"]; +var i = "(" + (e.concat(a).concat(q).concat(f)).join("|") + ")"; +var k = "([!\\"#$%&'()*+,./:;<=>?@[\\\\\\\\\\\\]^_`{|}~-]*)"; +return b(n); +function b(w) { +var v = [], +u = /[:.;?!] |(?: |^)[""]/g, +t = 0; +if (j == m) { +if (!l) { +l = w +} +return l +} +while (true) { +var s = u.exec(w); +v.push(w.substring(t, s ? s.index : w.length).replace(/\\b([A-Za-z][a-z.'"]*)\\b/g, function(x) { +return /[A-Za-z]\\.[A-Za-z]/.test(x) ? x : o(x) +}).replace(/\\b([A-Za-z]*[^\\u0000-\\u007F]+[A-Za-z]*)\\b/g, function(x) { +return o(x) +}).replace(RegExp("\\\\b" + i + "\\\\b", "ig"), function(x) { +if (j == d) { +return x.length >= 4 ? o(x) : r(x) +} else { +if (j == c) { +return x.length >= 5 ? o(x) : r(x) +} else { +return r(x) +} +} +}).replace(RegExp("^" + k + i + "\\\\b", "ig"), function(x, y, z) { +return y + o(z) +}).replace(RegExp("\\\\b" + i + k + "$", "ig"), o)); +t = u.lastIndex; +if (s) { +v.push(s[0]) +} else { +break +} +} +return v.join("").replace(/ V(s?)\\. /ig, " v$1. ").replace(/(['"])S\\b/ig, "$1s").replace(/\\b(AT&T|Q&A)\\b/ig, function(x) { +return x.toUpperCase() +}) +} +function r(s) { +return s.toLowerCase() +} +function o(s) { +return s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase() +} +} +; +""" diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Functions.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Functions.swift new file mode 100644 index 0000000..b112b2f --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Functions.swift @@ -0,0 +1,953 @@ +import CryptoKit +import Foundation + +// MARK: Case Functions + +enum CaseFunctions { + static let all: [TextFunction] = [ + lowerCase, + upperCase, + capitaliseWords, + sentenceCase, + camelCase, + snakeCase, + paskalCase, + randomCase, + kebabCase, + chmosCase, + ] + + static let lowerCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.lowerCase", title: "Lowercase", description: "Returns a version of the text with all letters converted to lowercase", category: .Case) { text -> String in + text.localizedLowercase + } + + static let upperCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.upperCase", title: "Uppercase", description: "Returns a version of the text with all letters converted to uppercase", category: .Case) { text -> String in + text.localizedUppercase + } + + static let sentenceCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.sentenceCase", title: "Sentence Case", description: "Replaces the first character in each sentence to its corresponding uppercase value", category: .Case) { text -> String in + var sentences = text.getUnits(of: .sentence) + return sentences.map { $0.capitaliseFirst() }.reduce("", +) + } + + static let capitaliseWords = TextFunction(id: "co.ameba.Esse.CaseFunctions.capitaliseWords", title: "Capitalize Words", description: "Replace the first character in each word changed to its corresponding uppercase value, and all remaining characters set to their corresponding lowercase values.", category: .Case) { text -> String in + text.localizedCapitalized + } + + static let camelCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.camelCase", title: "Camel Case", description: "Transforms by concatenating capitalized words but first character is lowercased", category: .Case) { text -> String in + guard text.count > 0 else { return "" } + + var words = text.words() + guard words.count > 0 else { return "" } + + words = words.map(\.localizedCapitalized) + words[0] = words[0].localizedLowercase + return words.reduce("", +) + } + + static let snakeCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.snakeCase", title: "Snake Case", description: "Transforms by separating words with underscore symbol (_) instead of a space", category: .Case) { text -> String in + guard text.count > 0 else { return "" } + + var words = text.words() + guard words.count > 0 else { return "" } + + return words.joined(separator: "_") + } + + static let kebabCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.kebabCase", title: "Kebab Case", description: "Transforms by separating words with dash symbol (-) instead of a space", category: .Case) { text -> String in + guard text.count > 0 else { return "" } + + var words = text.words() + guard words.count > 0 else { return "" } + + return words.joined(separator: "-") + } + + static let paskalCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.paskalCase", title: "Pascal Case", description: "Transforms by concatenating capitalized words", category: .Case) { text -> String in + guard text.count > 0 else { return "" } + + var words = text.words() + guard words.count > 0 else { return "" } + + return words.map(\.localizedCapitalized).joined(separator: "") + } + + static let randomCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.randomCase", title: "RaNdOm CasE", description: "Transforms by RaNdOmLy applying uppercase or lowercase to each character", category: .Case) { text -> String in + guard text.count > 0 else { return "" } + var newText = "" + for char in text { + if Bool.random() { + newText.append(String(char).uppercased()) + continue + } + newText.append(String(char).lowercased()) + } + return newText + } + + static let chmosCase = TextFunction(id: "co.ameba.Esse.CaseFunctions.chmosCase", title: "Chicago Manual of Style", description: "Do Not Capitalize Words Based on Length", category: .Case) { text -> String in + let text = [text].map { CleaningFunctions.collapseWhitespace.run($0) }.map { CaseFunctions.lowerCase.run($0) }.first + guard var cleaned = text, !cleaned.isEmpty else { return "" } + cleaned = cmosJS(text: cleaned) + + var words = cleaned.components(separatedBy: " ") + words[0] = capitaliseWords.run(words[0]) + words[words.endIndex - 1] = capitaliseWords.run(words[words.endIndex - 1]) + let lowercased: Set = ["about", "above", "across", "after", "against", "along", "among", "around", "at", "before", "behind", "below", "beneath", "beside", "between", "beyond", "but", "by", "despite", "down", "during", "except", "for", "from", "in", "inside", "into", "like", "near", "of", "off", "on", "onto", "out", "outside", "over", "past", "per", "since", "through", "throughout", "till", "to", "toward", "under", "underneath", "until", "up", "upon", "via", "with", "within", "without", "a", "an", "the", "and", "but", "or", "nor", "for", "yet", "so", "if", "en", "as", "vs.", "v."] + let exceptions: Set = ["macOS", "iPhone", "iPad", "MacBook", "iMac", "iPod", "MacPro", "iOS", "tvOS", "HomePod", "OmniFocus"] + + words = words.map { word in + let wordLowercased = word.lowercased() + for exception in exceptions { + if wordLowercased == exception.lowercased() { + return exception + } + } + + if lowercased.contains(word) { + return lowerCase.run(word) + } + + if word.contains("://") || word.contains("@") || wordLowercased.contains(".com") || wordLowercased.contains(".net") { + return lowerCase.run(word) + } + + return word + } + return words.joined(separator: " ") + } +} + +// MARK: ASCII + +enum ASCIIFunctions { + static let all: [TextFunction] = [ + ASCIICowSay, + signBunny, + ] + + static func speechBubble(_ text: String) -> String { + let maxStringLength = 40 + var out = "" + if text.count <= maxStringLength, !text.contains("\n") { + let border = String(repeating: "-", count: text.count + 2) + out = " " + border + "\n" + + "< " + text + " >" + "\n" + + " " + border + return out + } + var longestLine = 0 + var intermidiateResult = "" + for line in text.components(separatedBy: "\n") { + if line.count > maxStringLength { + for line in line.inserting(separator: "\n", every: maxStringLength).components(separatedBy: "\n") { + intermidiateResult = intermidiateResult + line + "\n" + } + longestLine = maxStringLength + continue + } + intermidiateResult = intermidiateResult + line + "\n" + longestLine = max(longestLine, line.count) + } + + for line in intermidiateResult.dropLast().components(separatedBy: "\n") { + out = out + "| " + line + String(repeating: " ", count: longestLine - line.count) + " |" + "\n" + } + let border = " " + String(repeating: "-", count: longestLine + 2) + out = [border, String(out.dropLast()), border].joined(separator: "\n") + + return out + } + + static func cowOne() -> String { + """ + \\ ^__^ + \\ (oo)\\_______ + (__)\\ )\\/\\ + ||----w | + || || + """ + } + + static func attachToBubble(_ text: String, bubble: String) -> String { + var out = "" + out = bubble + "\n" + let padding = String(repeating: " ", count: 3) + for line in text.components(separatedBy: "\n") { + out = out + padding + line + "\n" + } + return out + } + + static let ASCIICowSay = TextFunction(id: "co.ameba.Esse.ASCIIFunctions.ASCIICowSay", title: "Cowsay", description: "Cow says whatever you want! Non-monospaced font, may look odd...", category: .ASCII) { text -> String in + guard text != "" else { return "" } + return attachToBubble(cowOne(), bubble: speechBubble(text)) + } + + static func textSign(_ text: String) -> String { + let maxStringLength = 30 + var out = "" + + var longestLine = 0 + var intermidiateResult = "" + for line in text.components(separatedBy: "\n") { + if line.count > maxStringLength { + for line in line.inserting(separator: "\n", every: maxStringLength).components(separatedBy: "\n") { + intermidiateResult = intermidiateResult + line + "\n" + } + longestLine = maxStringLength + continue + } + intermidiateResult = intermidiateResult + line + "\n" + longestLine = max(longestLine, line.count) + } + + for line in intermidiateResult.dropLast().components(separatedBy: "\n") { + out = out + "| " + line + String(repeating: " ", count: longestLine - line.count) + " |" + "\n" + } + let upBorder = " " + String(repeating: "_", count: longestLine + 2) + let downBorder = "|" + String(repeating: "_", count: longestLine + 2) + "|" + out = [upBorder, String(out.dropLast()), downBorder].joined(separator: "\n") + + return out + } + + static func attachToSign(_ text: String, sign: String) -> String { + var out = "" + let shift = sign.components(separatedBy: "\n").first!.count >= 12 ? 5 : (14 - text.components(separatedBy: "\n").first!.count / 2) + let padding = String(repeating: " ", count: shift) + for line in sign.components(separatedBy: "\n") { + out = out + padding + line + "\n" + } + out = out + text + return out + } + + static func bunnyOne() -> String { + """ + (\\__/) || + (โ€ขใ……โ€ข) || + / ใ€€ ใฅ + """ + } + + static let signBunny = TextFunction(id: "co.ameba.Esse.ASCIIFunctions.signBunny", title: "Sign Bunny", description: "Bunny with an important message! Non-monospaced font, may look odd...", category: .ASCII) { text -> String in + guard text != "" else { return "" } + return attachToSign(bunnyOne(), sign: textSign(text)) + } +} + +// MARK: Quatation Marks + +enum QuotationMarksFunctions { + static let all: [TextFunction] = [ + curvedQuotes, + striaghtQuotes, + angleQuotes, + CJKQuotes, + singleToDoubleQuotes, + doubleToSingleQuotes, + smartWrapInQuotes, + wrapParagraphInQuotes, + removeSentenceQuotes, + ] + struct Quotes { + let openning: String + let closing: String + } + + enum QuotesTypes { + case Guillemet + case Straight + case Angle + case CJK + } + + private static let doubleQuotes: [QuotesTypes: Quotes] = [ + .Guillemet: Quotes(openning: "ยซ", closing: "ยป"), + .Straight: Quotes(openning: "\"", closing: "\""), + .Angle: Quotes(openning: "โ€œ", closing: "โ€"), + .CJK: Quotes(openning: "ใ€Œ", closing: "ใ€"), + ] + private static let singleQuotes: [QuotesTypes: Quotes] = [ + .Guillemet: Quotes(openning: "โ€น", closing: "โ€บ"), + .Straight: Quotes(openning: "\'", closing: "\'"), + .Angle: Quotes(openning: "โ€˜", closing: "โ€™"), + .CJK: Quotes(openning: "ใ€Œ", closing: "ใ€"), + ] + + static func replaceAllDoubleQuotes(with quoteType: QuotesTypes, input: String) -> String { + guard let quote = doubleQuotes[quoteType] else { return input } + var out = input + for dq in doubleQuotes.values { + out = out.replacingOccurrences(of: dq.openning, with: quote.openning).replacingOccurrences(of: dq.closing, with: quote.closing) + } + return out + } + + static func replaceAllSingleQuotes(with quoteType: QuotesTypes, input: String) -> String { + guard let quote = singleQuotes[quoteType] else { return input } + var out = input + for dq in singleQuotes.values { + out = out.replacingOccurrences(of: dq.openning, with: quote.openning).replacingOccurrences(of: dq.closing, with: quote.closing) + } + return out + } + + static let curvedQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.curvedQuotes", title: "Guillemet Quotes", description: "Replace all quotes with Guillemet(Angle) quotes", category: .QuotationMarks) { text -> String in + text.replaceAllQuotes(with: .Guillemet) + } + + static let striaghtQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.striaghtQuotes", title: "Straight Quotes", description: "Replace all quotes with Straight quotes", category: .QuotationMarks) { text -> String in + text.replaceAllQuotes(with: .Straight) + } + + static let angleQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.angleQuotes", title: "Curly Quotes", description: "Replace all quotes with Curly(Citation) quotes", category: .QuotationMarks) { text -> String in + text.replaceAllQuotes(with: .Angle) + } + + static let CJKQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.CJKQuotes", title: "CJK Quotes", description: "Replace all quotes with CJK Brackets", category: .QuotationMarks) { text -> String in + text.replaceAllQuotes(with: .CJK) + } + + static let singleToDoubleQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.singleToDoubleQuotes", title: "Single to Double Quotes", description: "Replace all single quotes with double quotes", category: .QuotationMarks) { text -> String in + var out = text + for (type, quote) in singleQuotes { + out = out.replacingOccurrences(of: quote.openning, with: doubleQuotes[type]!.openning).replacingOccurrences(of: quote.closing, with: doubleQuotes[type]!.closing) + } + return out + } + + static let doubleToSingleQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.doubleToSingleQuotes", title: "Double to Single Quotes", description: "Replace all double quotes with single quotes", category: .QuotationMarks) { text -> String in + var out = text + for (type, quote) in doubleQuotes { + out = out.replacingOccurrences(of: quote.openning, with: singleQuotes[type]!.openning).replacingOccurrences(of: quote.closing, with: singleQuotes[type]!.closing) + } + return out + } + + static let _wrapInQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions._wrapInQuotes", title: "Wrap in Quotes", description: "Wraps provided text in quotes", category: .QuotationMarks) { text -> String in + doubleQuotes[.Angle]!.openning + text + doubleQuotes[.Angle]!.closing + } + + static let smartWrapInQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.smartWrapInQuotes", title: "Wrap in Quotes", description: "Wraps provided text in quotes, replacing all existing double quotes with single quotes", category: .QuotationMarks) { text -> String in + + QuotationMarksFunctions._wrapInQuotes.run( + QuotationMarksFunctions.doubleToSingleQuotes.run(text) + ) + } + + static let wrapParagraphInQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.wrapParagraphInQuotes", title: "Wrap Paragraph in Quotes", description: "Wraps each paragraph in provided text in quotes, replacing all existing double quotes with single quotes", category: .QuotationMarks) { text -> String in + + text.components(separatedBy: .newlines).map { text in + let out = QuotationMarksFunctions._wrapInQuotes.run( + QuotationMarksFunctions.doubleToSingleQuotes.run(text) + ) + return out.count == 2 ? "" : out + }.joined(separator: "\n") + } + + static let removeSentenceQuotes = TextFunction(id: "co.ameba.Esse.QuotationMarksFunctions.removeSentenceQuotes", title: "Unquote Sentence", description: "Unquotes sentence, ignores quotes within the sentence.", category: .QuotationMarks) { text -> String in + let allQuotes = doubleQuotes.values.map(\.openning) + doubleQuotes.values.map(\.closing) + + singleQuotes.values.map(\.openning) + singleQuotes.values.map(\.closing) + return text.components(separatedBy: .newlines).map { text in + text.getUnits(of: .sentence).map { sentence -> String in + var out = sentence.trimmingCharacters(in: .whitespacesAndNewlines) + guard let first = out.first, let last = out.last else { return "" } + + if allQuotes.contains(String(first)), allQuotes.contains(String(last)) { + out = String(out.dropFirst()) + out = String(out.dropLast()) + } + return out + }.joined(separator: " ") + }.joined(separator: "\n") + } +} + +// MARK: Cleaning + +enum CleaningFunctions { + static let all: [TextFunction] = [ + removeSpaces, + removeQuotePrefixes, + removeEmptyLines, + removeLineNumbers, + removeDuplicateLines, + collapseWhitespace, + removeNewLines, + removeNonDigitCharacters, + removeDigitCharacters, + removeNonAlphaNumericCharacters, + removeNonAlphaNumericCharactersPlus, + // removeJunkFromURL + ] + + static let removeSpaces = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeSpaces", title: "Truncate Spaces", description: "Removes empty space in the beginning and end of the text", category: .Cleaning) { text -> String in + text.trimmingCharacters(in: .whitespacesAndNewlines) + } + + static let removeQuotePrefixes = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeQuotePrefixes", title: "Remove Quote Prefixes", description: "Cleans quotes marks(>>) from the beginning of each line in text", category: .Cleaning) { text -> String in + let components = text.components(separatedBy: .newlines) + return components.map { $0.trimmingCharacters(in: CharacterSet(charactersIn: ">")) }.joined(separator: "\n") + } + + static let removeEmptyLines = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeEmptyLines", title: "Remove Empty Lines", description: "Removes all empty lines from text", category: .Cleaning) { text -> String in + let components = text.components(separatedBy: .newlines) + return components.filter { !$0.isEmpty }.joined(separator: "\n") + } + + static let removeLineNumbers = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeLineNumbers", title: "Remove Line Numbers", description: "Removes line numbers from a numbered list", category: .Cleaning) { text -> String in + let components = text.components(separatedBy: .newlines) + return components.map { $0.trimmingCharacters(in: .decimalDigits) + .trimmingCharacters(in: CharacterSet(charactersIn: ".")) + .trimmingCharacters(in: .whitespaces) + }.joined(separator: "\n") + } + + static let removeDuplicateLines = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeDuplicateLines", title: "Remove Duplicate Lines", description: "Removes duplicate lines", category: .Cleaning) { text -> String in + text.toArray().removingDuplicates().joined(separator: "\n") + } + + static let collapseWhitespace = TextFunction(id: "co.ameba.Esse.CleaningFunctions.collapseWhitespace", title: "Remove White Space", description: "Truncates empty space, including empty lines, tabs and multiple spaces", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines) + .map { $0.components(separatedBy: .whitespaces) + .filter { !$0.isEmpty }.joined(separator: " ") + } + .joined(separator: "\n") + } + + static let removeNewLines = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeNewLines", title: "Remove New Lines", description: "Removes new lines, merging all in, separated by a space", category: .Cleaning) { text -> String in + let components = text.components(separatedBy: .newlines) + return components.filter { !$0.isEmpty }.joined(separator: " ") + } + + static let removeNonDigitCharacters = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeNonDigitCharacters", title: "Strip non Numeric Characters", description: "Removes all non numeric characters", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + line.replacingOccurrences(of: "[^\\d]", with: "", options: String.CompareOptions.regularExpression, range: line.startIndex ..< line.endIndex) + }.filter { !$0.isEmpty }.joined(separator: "\n") + } + + static let removeDigitCharacters = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeDigitCharacters", title: "Strip Numeric Characters", description: "Removes all numeric characters", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + line.words().map { $0.components(separatedBy: .decimalDigits).joined() }.joined(separator: " ") + }.filter { !$0.isEmpty }.joined(separator: "\n") + } + + static let removeNonAlphaNumericCharacters = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeNonAlphaNumericCharacters", title: "Strip non Alphanumeric Characters", description: "Removes all non alphanumeric characters, spaces and new lines stay in place", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + line.words().map { $0.components(separatedBy: CharacterSet.alphanumerics.inverted).joined() }.joined(separator: " ") + }.filter { !$0.isEmpty }.joined(separator: "\n") + } + + static let removeNonAlphaNumericCharactersPlus = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeNonAlphaNumericCharactersPlus", title: "Strip non Alphanumeric Characters Plus", description: "Removes all non alphanumeric characters, spaces, new lines and punctuation stay in place", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + line.filter { $0.isLetter || $0.isNumber || $0.isWhitespace || [".", ",", "!", ":", ";", "?", "@", "$", "%", "'", "/", "\\"].contains($0) } + }.filter { !$0.isEmpty }.joined(separator: "\n") + } + + static let removeJunkFromURL = TextFunction(id: "co.ameba.Esse.CleaningFunctions.removeJunkFromURL", title: "Clean URL from Junk", description: "Cleans clutter from URLs such as all sorts of UTM tracking and subdomains like m. for mobile sites", category: .Cleaning) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + line.components(separatedBy: CharacterSet.alphanumerics.inverted).joined() + }.filter { !$0.isEmpty }.joined(separator: "\n") + } +} + +// MARK: Convert + +enum ConvertFunctions { + static let all: [TextFunction] = [ + increaseIndent, + decreaseIndent, + sortLinesAscending, + sortLinesDescending, + shuffleWords, + shuffleSentences, + spellOutNumbers, + ] + + static let increaseIndent = TextFunction(id: "co.ameba.Esse.ConvertFunctions.increaseIndent", title: "Increase Indent", description: "Adds tab in the beginning of each line, increasing indentation", category: .Convert) { text -> String in + text.components(separatedBy: .newlines).map { "\t" + $0 }.joined(separator: "\n") + } + + static let decreaseIndent = TextFunction(id: "co.ameba.Esse.ConvertFunctions.decreaseIndent", title: "Decrease Indent", description: "Removes tab in the beginning of each line, decreasing indentation", category: .Convert) { text -> String in + text.components(separatedBy: .newlines) + .map { str in + if str.first == "\t" { + return String(str.dropFirst()) + } + return str + }.joined(separator: "\n") + } + + static let sortLinesAscending = TextFunction(id: "co.ameba.Esse.ConvertFunctions.sortLinesAscending", title: "Sort Lines Ascending", description: "Sorts lines in accessing order", category: .Convert) { text -> String in + var arr = text.toArray() + arr.sort { $0.localizedCompare($1) == ComparisonResult.orderedAscending } + return arr.joined(separator: "\n") + } + + static let sortLinesDescending = TextFunction(id: "co.ameba.Esse.ConvertFunctions.sortLinesDescending", title: "Sort Lines Descending", description: "Sorts lines in descending order", category: .Convert) { text -> String in + var arr = text.toArray() + arr.sort { $0.localizedCompare($1) == ComparisonResult.orderedDescending } + return arr.joined(separator: "\n") + } + + static let shuffleWords = TextFunction(id: "co.ameba.Esse.ConvertFunctions.shuffleWords", title: "Shuffle Words", description: "Randomly shuffles words", category: .Convert) { text -> String in + text.words().shuffled().joined(separator: " ") + } + + static let shuffleSentences = TextFunction(id: "co.ameba.Esse.ConvertFunctions.shuffleSentences", title: "Shuffle Sentences", description: "Randomly shuffles sentences", category: .Convert) { text -> String in + text.getUnits(of: .sentence).shuffled().joined(separator: " ") + } + + static let spellOutNumbers = TextFunction(id: "co.ameba.Esse.ConvertFunctions.spellOutNumbers", title: "Spell Out Numbers", description: "Converts all numbers into words, i.e. 9 ->'nine', 22 -> 'twenty two', etc.", category: .Convert) { text -> String in + text.components(separatedBy: .newlines).map { line -> String in + replaceNumberWithSpellOut(input: line) + }.joined(separator: "\n") + } + + static func replaceNumberWithSpellOut(input: String) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = NumberFormatter.Style.spellOut + + return input.components(separatedBy: .whitespaces).map { word -> String in + guard let number = Int(word), let spellOutText = formatter.string(for: number) else { return word } + return spellOutText + }.joined(separator: " ") + } +} + +enum ExtractFunctions { + static let all: [TextFunction] = [ + extractURL, + extractPhone, + extractDate, + extractEmail, + extractAddress, + ] + + static let extractURL = TextFunction(id: "co.ameba.Esse.ExtractFunctions.extractURL", title: "Extract URLs", description: "Extracts URLs from given text, outputs one URL per line.", category: .Extract) { text -> String in + var urls: [String] = [] + do { + let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { result, _, _ in + if let match = result, let url = match.url { + urls.append(url.absoluteString) + } + }) + } catch let error as NSError { + print(error.localizedDescription) + } + return urls.joined(separator: "\n") + } + + static let extractPhone = TextFunction(id: "co.ameba.Esse.ExtractFunctions.extractPhone", title: "Extract Phone Numbers", description: "Extracts phone numbers from given text, outputs one phone per line.", category: .Extract) { text -> String in + var phones: [String] = [] + do { + let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.phoneNumber.rawValue) + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { result, _, _ in + if let match = result, let phone = match.phoneNumber { + phones.append(phone) + } + }) + } catch let error as NSError { + print(error.localizedDescription) + } + return phones.joined(separator: "\n") + } + + static let extractDate = TextFunction(id: "co.ameba.Esse.ExtractFunctions.extractDate", title: "Extract Dates", description: "Extracts dates from given text, outputs one date per line.", category: .Extract) { text -> String in + var phones: [String] = [] + do { + let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { result, _, _ in + if let match = result, let date = match.date { + phones.append(text[match.range]) + } + }) + } catch let error as NSError { + print(error.localizedDescription) + } + return phones.joined(separator: "\n") + } + + static let extractAddress = TextFunction(id: "co.ameba.Esse.ExtractFunctions.extractAddress", title: "Extract Address", description: "Extracts addresses from given text, outputs one date per line.", category: .Extract) { text -> String in + var address: [String] = [] + do { + let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.address.rawValue) + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { result, _, _ in + if let match = result, let _ = match.addressComponents { + address.append(text[match.range]) + } + }) + } catch let error as NSError { + print(error.localizedDescription) + } + return address.joined(separator: "\n") + } + + static let extractEmail = TextFunction(id: "co.ameba.Esse.ExtractFunctions.extractEmail", title: "Extract Emails", description: "Extracts emails from given text, outputs one email per line.", category: .Extract) { text -> String in + text.components(separatedBy: CharacterSet.whitespacesAndNewlines).filter { $0.isValidEmail() }.joined(separator: "\n") + } +} + +// MARK: Developer + +enum DeveloperFunctions { + static let all: [TextFunction] = [ + prettyJSON, + prettySortedJSON, + htmlToPlainText, + urlDecoded, + urlEncoded, + minifyJSON, + sha256, + sha384, + sha512, + md5, + base64, + ] + + static let prettyJSON = TextFunction(id: "co.ameba.Esse.OtherFunctions.prettyJSON", title: "Prettify JSON", description: "Returns nicely formatted JSON. Returns nothing if input text is not valid JSON", category: .Developer) { text -> String in + guard let data = text.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonStr = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), + let out = String(data: jsonStr, encoding: .utf8) + else { return "" } + return out + } + + static let prettySortedJSON = TextFunction(id: "co.ameba.Esse.OtherFunctions.prettySortedJSON", title: "Prettify and Sort JSON", description: "Returns nicely formatted and sorted JSON. Returns nothing if input text is not valid JSON", category: .Developer) { text -> String in + guard let data = text.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonStr = try? JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys]), + let out = String(data: jsonStr, encoding: .utf8) + else { return "" } + return out + } + + static let minifyJSON = TextFunction(id: "co.ameba.Esse.OtherFunctions.minifyJSON", title: "Minify JSON", description: "Returns a minimized version of JSON, everething is in one string", category: .Developer) { text -> String in + guard let data = text.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonStr = try? JSONSerialization.data(withJSONObject: jsonObject, options: []), + let out = String(data: jsonStr, encoding: .utf8)? + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\t", with: "") + .replacingOccurrences(of: "\r", with: "") + else { return "" } + return out + } + + static let htmlToPlainText = TextFunction(id: "co.ameba.Esse.ConvertFunctions.htmlToPlainText", title: "HTML to Plain Text", description: "Converts provided HTML code to plain text", category: .Developer) { text -> String in + let data = Data(text.utf8) + #if !os(macOS) + if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) { + return attributedString.string + } + #endif + return "Something went wrong" + } + + static let urlEncoded = TextFunction(id: "co.ameba.Esse.ConvertFunctions.urlEncoded", title: "URL Encoded", description: "", category: .Developer) { text -> String in + text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + } + + static let urlDecoded = TextFunction(id: "co.ameba.Esse.ConvertFunctions.urlDecoded", title: "URL Decoded", description: "", category: .Developer) { text -> String in + text.removingPercentEncoding ?? "" + } + + static let sha256 = TextFunction(id: "co.ameba.Esse.ConvertFunctions.sha256", title: "SHA-256", description: "", category: .Developer) { text -> String in + SHA256.hash(data: Data(text.utf8)).description.components(separatedBy: " ")[2] + } + + static let sha384 = TextFunction(id: "co.ameba.Esse.ConvertFunctions.sha384", title: "SHA-384", description: "", category: .Developer) { text -> String in + SHA384.hash(data: Data(text.utf8)).description.components(separatedBy: " ")[2] + } + + static let sha512 = TextFunction(id: "co.ameba.Esse.ConvertFunctions.sha512", title: "SHA-512", description: "", category: .Developer) { text -> String in + SHA512.hash(data: Data(text.utf8)).description.components(separatedBy: " ")[2] + } + + static let md5 = TextFunction(id: "co.ameba.Esse.ConvertFunctions.md5", title: "MD5", description: "", category: .Developer) { text -> String in + Insecure.MD5.hash(data: Data(text.utf8)).description.components(separatedBy: " ")[2] + } + + static let base64 = TextFunction(id: "co.ameba.Esse.ConvertFunctions.base64", title: "Base64", description: "", category: .Developer) { text -> String in + Data(text.utf8).base64EncodedString() + } +} + +// MARK: Other + +enum OtherFunctions { + static let all: [TextFunction] = [ + hashTag, + reversed, + upsideDown, + circleLetters, + circleLettersFilled, + squareLetters, + rot13, + uniqueWords, + textStatistics, + addLineNumbers, + addLineNumbersDot, + addLineNumbersParentheses, + addLineBulletDash, + addLineBulletStar, + ] + + static let reversed = TextFunction(id: "co.ameba.Esse.OtherFunctions.reversed", title: "Reversed", description: "Returns input in reversed order.", category: .Other) { text -> String in + String(text.reversed()) + } + + static let hashTag = TextFunction(id: "co.ameba.Esse.OtherFunctions.hashTag", title: "Hashtags", description: "Adds hash sign(#) to each word.", category: .Other) { text -> String in + text.localizedLowercase.components(separatedBy: CharacterSet.whitespacesAndNewlines).map { "#\($0)" }.reduce("") { text, word -> String in + "\(text) \(word)" + } + } + + private static let lowerUpsideDown: [Character: Character] = [ + "a": "ษ", "b": "q", "c": "ษ”", "d": "p", "e": "ว", "f": "ษŸ", "g": "ฦƒ", "h": "ษฅ", "i": "ฤฑ", "j": "ษพ", "k": "สž", "l": "ืŸ", "m": "ษฏ", "n": "u", + "o": "o", "p": "d", "q": "b", "r": "ษน", "s": "s", "t": "ส‡", "u": "n", "v": "สŒ", "w": "ส", "x": "x", "y": "สŽ", "z": "z", + ] + + private static let upperUpsideDown: [Character: Character] = [ + "A": "โฑฏ", "B": "แ—บ", "C": "ฦ†", "D": "แ—ก", "E": "ฦŽ", "F": "แ–ต", "G": "โ…", "H": "H", "I": "I", "J": "แ’‹", "K": "โ‹Š", "L": "๊ž€", "M": "W", "N": "N", + "O": "O", "P": "ิ€", "Q": "๊น", "R": "แดš", "S": "S", "T": "โŠฅ", "U": "โˆฉ", "V": "ษ…", "W": "M", "X": "X", "Y": "โ…„", "Z": "Z", + ] + + private static let digitUpsideDown: [Character: Character] = [ + "0": "0", "1": "ะ†", "2": "แ˜”", "3": "ฦ", "4": "แ”ญ", "5": "5", "6": "9", "7": "โฑข", "8": "8", "9": "6", + ] + + private static let symbolsUpsideDown: [Character: Character] = [ + "!": "ยก", + "\"": "โ€ž", + "&": "โ…‹", + "'": ",", + ",": "'", + "?": "ยฟ", + ] + + static let upsideDown = TextFunction(id: "co.ameba.Esse.OtherFunctions.upsideDown", title: "Upside Down", description: "Transform text to upside down -> ส‡xวส‡", category: .Other) { text -> String in + String(text.reversed() + .map { char in + if let ch = lowerUpsideDown[char] { + return ch + } + if let ch = upperUpsideDown[char] { + return ch + } + if let ch = digitUpsideDown[char] { + return ch + } + if let ch = symbolsUpsideDown[char] { + return ch + } + return char + }) + } + + private static let circleLetter: [Character: Character] = [ + "A": "โ’ถ", "B": "โ’ท", "C": "โ’ธ", "D": "โ’น", "E": "โ’บ", "F": "โ’ป", "G": "โ’ผ", "H": "โ’ฝ", "I": "โ’พ", "J": "โ’ฟ", + "K": "โ“€", "L": "โ“", "M": "โ“‚", "N": "โ“ƒ", "O": "โ“„", "P": "โ“…", "Q": "โ“†", "R": "โ“‡", "S": "โ“ˆ", "T": "โ“‰", + "U": "โ“Š", "V": "โ“‹", "W": "โ“Œ", "X": "โ“", "Y": "โ“Ž", "Z": "โ“", + "a": "โ“", "b": "โ“‘", "c": "โ“’", "d": "โ““", "e": "โ“”", "f": "โ“•", "g": "โ“–", "h": "โ“—", "i": "โ“˜", "j": "โ“™", + "k": "โ“š", "l": "โ“›", "m": "โ“œ", "n": "โ“", "o": "โ“ž", "p": "โ“Ÿ", "q": "โ“ ", "r": "โ“ก", "s": "โ“ข", "t": "โ“ฃ", + "u": "โ“ค", "v": "โ“ฅ", "w": "โ“ฆ", "x": "โ“ง", "y": "โ“จ", "z": "โ“ฉ", + "0": "โ“ช", "1": "โ‘ ", "2": "โ‘ก", "3": "โ‘ข", "4": "โ‘ฃ", "5": "โ‘ค", "6": "โ‘ฅ", "7": "โ‘ฆ", "8": "โ‘ง", "9": "โ‘จ", + ] + static let circleLetters = TextFunction(id: "co.ameba.Esse.OtherFunctions.circleLetters", title: "Circle Letters: Empty", description: "All letters are placed in โ“”โ“œโ“Ÿโ“ฃโ“จ circles", category: .Other) { text -> String in + String(spacedString(string: text).map { char in + if let ch = circleLetter[char] { + return ch + } + return char + }) + } + + private static let circleLetterFilled: [Character: Character] = [ + "A": "๐Ÿ…", "B": "๐Ÿ…‘", "C": "๐Ÿ…’", "D": "๐Ÿ…“", "E": "๐Ÿ…”", "F": "๐Ÿ…•", "G": "๐Ÿ…–", "H": "๐Ÿ…—", "I": "๐Ÿ…˜", "J": "๐Ÿ…™", + "K": "๐Ÿ…š", "L": "๐Ÿ…›", "M": "๐Ÿ…œ", "N": "๐Ÿ…", "O": "๐Ÿ…ž", "P": "๐Ÿ…Ÿ", "Q": "๐Ÿ… ", "R": "๐Ÿ…ก", "S": "๐Ÿ…ข", "T": "๐Ÿ…ฃ", + "U": "๐Ÿ…ค", "V": "๐Ÿ…ฅ", "W": "๐Ÿ…ฆ", "X": "๐Ÿ…ง", "Y": "๐Ÿ…จ", "Z": "๐Ÿ…ฉ", + "0": "โ“ฟ", "1": "โžŠ", "2": "โž‹", "3": "โžŒ", "4": "โž", "5": "โžŽ", "6": "โž", "7": "โž", "8": "โž‘", "9": "โž’", + ] + + static let circleLettersFilled = TextFunction(id: "co.ameba.Esse.OtherFunctions.circleLettersFilled", title: "Circle Letters: Filled", description: "All letters are placed in filled circles", category: .Other) { text -> String in + String(spacedString(string: text).localizedUppercase + .map { char in + if let ch = circleLetterFilled[char] { + return ch + } + return char + }) + } + + private static let squareLetter: [Character: Character] = [ + "A": "๐Ÿ„ฐ", "B": "๐Ÿ„ฑ", "C": "๐Ÿ„ฒ", "D": "๐Ÿ„ณ", "E": "๐Ÿ„ด", "F": "๐Ÿ„ต", "G": "๐Ÿ„ถ", "H": "๐Ÿ„ท", "I": "๐Ÿ„ธ", "J": "๐Ÿ„น", + "K": "๐Ÿ„บ", "L": "๐Ÿ„ป", "M": "๐Ÿ„ผ", "N": "๐Ÿ„ฝ", "O": "๐Ÿ„พ", "P": "๐Ÿ„ฟ", "Q": "๐Ÿ…€", "R": "๐Ÿ…", "S": "๐Ÿ…‚", "T": "๐Ÿ…ƒ", + "U": "๐Ÿ…„", "V": "๐Ÿ……", "W": "๐Ÿ…†", "X": "๐Ÿ…‡", "Y": "๐Ÿ…ˆ", "Z": "๐Ÿ…‰", + "0": "0๏ธŽโƒฃ", "1": "1๏ธŽโƒฃ", "2": "2๏ธŽโƒฃ", "3": "3๏ธŽโƒฃ", "4": "4๏ธŽโƒฃ", "5": "5๏ธŽโƒฃ", "6": "6๏ธŽโƒฃ", "7": "7๏ธŽโƒฃ", "8": "8๏ธŽโƒฃ", "9": "9๏ธŽโƒฃ", + ] + + static let squareLetters = TextFunction(id: "co.ameba.Esse.OtherFunctions.squareLetters", title: "Square Letters", description: "All letters are placed in squares", category: .Other) { text -> String in + String(spacedString(string: text).localizedUppercase + .map { char in + if let ch = squareLetter[char] { + return ch + } + return char + }) + } + + private static let rot13Lookup: [Character: Character] = [ + "A": "N", "B": "O", "C": "P", "D": "Q", "E": "R", "F": "S", "G": "T", "H": "U", "I": "V", "J": "W", "K": "X", "L": "Y", + "M": "Z", "N": "A", "O": "B", "P": "C", "Q": "D", "R": "E", "S": "F", "T": "G", "U": "H", "V": "I", "W": "J", "X": "K", + "Y": "L", "Z": "M", "a": "n", "b": "o", "c": "p", "d": "q", "e": "r", "f": "s", "g": "t", "h": "u", "i": "v", "j": "w", + "k": "x", "l": "y", "m": "z", "n": "a", "o": "b", "p": "c", "q": "d", "r": "e", "s": "f", "t": "g", "u": "h", "v": "i", + "w": "j", "x": "k", "y": "l", "z": "m", + ] + + static let rot13 = TextFunction(id: "co.ameba.Esse.OtherFunctions.rot13", title: "ROT13", description: "ROT13 is a simple letter substitution cipher that replaces a letter with the 13th letter after it, in the alphabet", category: .Other) { text -> String in + String(text + .map { char in + if let ch = rot13Lookup[char] { + return ch + } + return char + }) + } + + static func spacedString(string: String) -> String { + var out = "" + for char in string.localizedUppercase { + out.append(char) + if char == Character("\t") || char == Character("\n") { + continue + } + out.append(" ") + } + return out + } + + static let emojify = TextFunction(id: "co.ameba.Esse.OtherFunctions.emojify", title: "Emojify", description: "Translates text to Emoji: 'Chickens and cows live on a farm.' -> '๐Ÿ” and ๐Ÿฎ live on a farm.'", category: .Other) { text -> String in + var result = "" + let lemmas = text.lemmas() + text.enumerateSubstrings(in: text.startIndex ..< text.endIndex, options: .byWords) { word, substringRange, enclosingEndingRange, _ in + guard let word else { return } + + var lemmaEmojii: String? + if let lemma = lemmas[word] { + lemmaEmojii = Emojii.mapping[lemma]?.first + } + let wordEmojii = Emojii.mapping[word.lowercased()]?.first + if lemmaEmojii != nil || wordEmojii != nil { + let resultEmojii = wordEmojii ?? (lemmaEmojii ?? "") + result += resultEmojii + if substringRange.upperBound != enclosingEndingRange.upperBound { + result += String(text[substringRange.upperBound ..< enclosingEndingRange.upperBound]) + } + } else { + result += String(text[enclosingEndingRange]) // substringRange.lowerBound...enclosingEndingRange.upperBound]) + } + } + return result // "Implement me!" + } + + static let uniqueWords = TextFunction(id: "co.ameba.Esse.OtherFunctions.uniqueWords", title: "Count Unique Words", description: "Counts unique words", category: .Other) { text -> String in + guard text.count > 0 else { return "" } + + var words = text.words() + guard words.count > 0 else { return "" } + + var counts: [String: Int] = [:] + for word in words { + counts[word] = (counts[word] ?? 0) + 1 + } + var output: [String] = counts.sorted { $0.value > $1.value }.map { "\($0.key):\($0.value)" } + output.insert("Total Unique Words:\(counts.count)", at: 0) + output.insert("Total Words:\(counts.values.reduce(0, +))", at: 0) + return output.joined(separator: "\n") + } + + static let textStatistics = TextFunction(id: "co.ameba.Esse.OtherFunctions.textStatistics", title: "Text Stats", description: "Returns basic statistics for provided text", category: .Other) { text -> String in + guard text.count > 0 else { return "" } + var out = "" + out = "Characters count: \(text.count)" + out += "\nParagraphs: \(text.getUnits(of: .paragraph).count)" + out += "\tSentences: \(text.getUnits(of: .sentence).count)" + out += "\nWords: \(text.getUnits(of: .word).count)" + out += "\t\tUnique words: \(Set(text.getUnits(of: .word)).count)" + out += "\nLetters: \(text.filter(\.isLetter).count)" + out += "\t\tDigits: \(text.filter(\.isNumber).count)" + out += "\nSpaces: \(text.filter(\.isWhitespace).count)" + out += "\t\tPunctuation: \(text.filter(\.isPunctuation).count)" + + return out + } + + static func addLineBullets(text: String, numbers: Bool = true, char: String = "") -> String { + guard text.count > 0 else { return "" } + var out = "" + for (i, text) in text.components(separatedBy: .newlines).enumerated() { + guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + out += "\(text) \n" + continue + } + if numbers { + out += "\(i + 1)\(char) \(text) \n" + } else { + out += "\(char) \(text) \n" + } + } + + return out + } + + static let addLineNumbers = TextFunction(id: "co.ameba.Esse.OtherFunctions.addLineNumbers", title: "List: Add Line Numbers (1 2 3)", description: "Numbers each line", category: .Other) { text -> String in + addLineBullets(text: text, numbers: true, char: "") + } + + static let addLineNumbersDot = TextFunction(id: "co.ameba.Esse.OtherFunctions.addLineNumbersDot", title: "List: Add Line Numbers (1. 2. 3.)", description: "Numbers each line, numbers followed by dot", category: .Other) { text -> String in + addLineBullets(text: text, numbers: true, char: ".") + } + + static let addLineNumbersParentheses = TextFunction(id: "co.ameba.Esse.OtherFunctions.addLineNumbersParentheses", title: "List: Add Line Numbers (1) 2) 3))", description: "Numbers each line, numbers followed by parentheses", category: .Other) { text -> String in + addLineBullets(text: text, numbers: true, char: ")") + } + + static let addLineBulletDash = TextFunction(id: "co.ameba.Esse.OtherFunctions.addLineBulletDash", title: "List: Add Line Bullet (-)", description: "Adds dash(-) to each line", category: .Other) { text -> String in + addLineBullets(text: text, numbers: false, char: "-") + } + + static let addLineBulletStar = TextFunction(id: "co.ameba.Esse.OtherFunctions.addLineBulletStar", title: "List: Add Line Bullet (*)", description: "Adds star(*) to each line", category: .Other) { text -> String in + addLineBullets(text: text, numbers: false, char: "*") + } +} + +// f characters: spaces, letters, numeric, alphanumeric/punctuation, words, sentences, lines, paragraphs. + +public let AllFunctions = ( + CaseFunctions.all + + QuotationMarksFunctions.all + + CleaningFunctions.all + + ConvertFunctions.all + + ExtractFunctions.all + + ASCIIFunctions.all + + OtherFunctions.all + + DeveloperFunctions.all).sorted(by: { $0.title < $1.title }) + +extension String { + func replaceAllDoubleQuotes(with quoteType: QuotationMarksFunctions.QuotesTypes) -> String { + QuotationMarksFunctions.replaceAllDoubleQuotes(with: quoteType, input: self) + } + + func replaceAllSingleQuotes(with quoteType: QuotationMarksFunctions.QuotesTypes) -> String { + QuotationMarksFunctions.replaceAllSingleQuotes(with: quoteType, input: self) + } + + func replaceAllQuotes(with quoteType: QuotationMarksFunctions.QuotesTypes) -> String { + QuotationMarksFunctions.replaceAllDoubleQuotes(with: quoteType, input: self).replaceAllSingleQuotes(with: quoteType) + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Resources/CMOS.js b/Esse/Packages/EsseCore/Sources/EsseCore/Resources/CMOS.js new file mode 100644 index 0000000..6a01939 --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Resources/CMOS.js @@ -0,0 +1,62 @@ +function TitleCapsEditor(n) { + const m = 1; + const d = 2; + const c = 3; + const g = 4; + var j = g; + var l = ""; + var e = ["about", "above", "across", "after", "against", "along", "among", "around", "at", "before", "behind", "below", "beneath", "beside", "between", "beyond", "but", "by", "despite", "down", "during", "except", "for", "from", "in", "inside", "into", "like", "near", "of", "off", "on", "onto", "out", "outside", "over", "past", "per", "since", "through", "throughout", "till", "to", "toward", "under", "underneath", "until", "up", "upon", "via", "with", "within", "without"]; + var a = ["a", "an", "the"]; + var q = ["and", "but", "or", "nor", "for", "yet", "so"]; + var f = ["if", "en", "as", "vs.", "v[.]?"]; + var i = "(" + (e.concat(a).concat(q).concat(f)).join("|") + ")"; + var k = "([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]*)"; + return b(n); + function b(w) { + var v = [], + u = /[:.;?!] |(?: |^)[""]/g, + t = 0; + if (j == m) { + if (!l) { + l = w + } + return l + } + while (true) { + var s = u.exec(w); + v.push(w.substring(t, s ? s.index : w.length).replace(/\b([A-Za-z][a-z.'"]*)\b/g, function(x) { + return /[A-Za-z]\.[A-Za-z]/.test(x) ? x : o(x) + }).replace(/\b([A-Za-z]*[^\u0000-\u007F]+[A-Za-z]*)\b/g, function(x) { + return o(x) + }).replace(RegExp("\\b" + i + "\\b", "ig"), function(x) { + if (j == d) { + return x.length >= 4 ? o(x) : r(x) + } else { + if (j == c) { + return x.length >= 5 ? o(x) : r(x) + } else { + return r(x) + } + } + }).replace(RegExp("^" + k + i + "\\b", "ig"), function(x, y, z) { + return y + o(z) + }).replace(RegExp("\\b" + i + k + "$", "ig"), o)); + t = u.lastIndex; + if (s) { + v.push(s[0]) + } else { + break + } + } + return v.join("").replace(/ V(s?)\. /ig, " v$1. ").replace(/(['"])S\b/ig, "$1s").replace(/\b(AT&T|Q&A)\b/ig, function(x) { + return x.toUpperCase() + }) + } + function r(s) { + return s.toLowerCase() + } + function o(s) { + return s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase() + } +} +; diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Resources/emojis.json b/Esse/Packages/EsseCore/Sources/EsseCore/Resources/emojis.json new file mode 100644 index 0000000..1ad9643 --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Resources/emojis.json @@ -0,0 +1,3462 @@ +{ + "grinning": { + "keywords": ["face", "smile", "happy", "joy"], + "char": "๐Ÿ˜€" + }, + "grin": { + "keywords": ["face", "happy", "smile", "joy"], + "char": "๐Ÿ˜" + }, + "joy": { + "keywords": ["face", "cry", "tears", "weep", "happy", "haha"], + "char": "๐Ÿ˜‚" + }, + "smiley": { + "keywords": ["face", "happy", "joy", "haha"], + "char": "๐Ÿ˜ƒ" + }, + "smile": { + "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like"], + "char": "๐Ÿ˜„" + }, + "sweat_smile": { + "keywords": ["face", "hot", "happy", "laugh"], + "char": "๐Ÿ˜…" + }, + "laughing": { + "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad"], + "char": "๐Ÿ˜†" + }, + "innocent": { + "keywords": ["face", "angel", "heaven", "halo"], + "char": "๐Ÿ˜‡" + }, + "smiling_imp": { + "keywords": ["devil", "horns"], + "char": "๐Ÿ˜ˆ" + }, + "imp": { + "keywords": ["devil", "angry", "horns"], + "char": "๐Ÿ‘ฟ" + }, + "wink": { + "keywords": ["face", "happy", "mischievous", "secret"], + "char": "๐Ÿ˜‰" + }, + "blush": { + "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"], + "char": "๐Ÿ˜Š" + }, + "relaxed": { + "keywords": ["face", "blush", "massage", "happiness"], + "char": "โ˜บ๏ธ" + }, + "yum": { + "keywords": ["happy", "joy", "tongue", "smile", "face", "silly"], + "char": "๐Ÿ˜‹" + }, + "relieved": { + "keywords": ["face", "relaxed", "phew", "massage", "happiness"], + "char": "๐Ÿ˜Œ" + }, + "heart_eyes": { + "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush"], + "char": "๐Ÿ˜" + }, + "sunglasses": { + "keywords": ["face", "cool", "smile", "summer", "beach"], + "char": "๐Ÿ˜Ž" + }, + "smirk": { + "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"], + "char": "๐Ÿ˜" + }, + "neutral_face": { + "keywords": ["indifference", "meh"], + "char": "๐Ÿ˜" + }, + "expressionless": { + "keywords": ["face", "indifferent", "-_-", "meh"], + "char": "๐Ÿ˜‘" + }, + "unamused": { + "keywords": ["indifference", "bored", "straight face", "serious"], + "char": "๐Ÿ˜’" + }, + "sweat": { + "keywords": ["face", "hot", "sad", "tired", "exercise"], + "char": "๐Ÿ˜“" + }, + "pensive": { + "keywords": ["face", "sad", "depressed", "okay", "upset"], + "char": "๐Ÿ˜”" + }, + "confused": { + "keywords": ["face", "indifference", "huh", "weird", "hmmm"], + "char": "๐Ÿ˜•" + }, + "confounded": { + "keywords": ["face", "confused", "sick", "unwell", "oops"], + "char": "๐Ÿ˜–" + }, + "kissing": { + "keywords": ["love", "like", "face", "3", "valentines", "infatuation"], + "char": "๐Ÿ˜—" + }, + "kissing_heart": { + "keywords": ["face", "love", "like", "affection", "valentines", "infatuation"], + "char": "๐Ÿ˜˜" + }, + "kissing_smiling_eyes": { + "keywords": ["face", "affection", "valentines", "infatuation"], + "char": "๐Ÿ˜™" + }, + "kissing_closed_eyes": { + "keywords": ["face", "love", "like", "affection", "valentines", "infatuation"], + "char": "๐Ÿ˜š" + }, + "stuck_out_tongue": { + "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile"], + "char": "๐Ÿ˜›" + }, + "stuck_out_tongue_winking_eye": { + "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile"], + "char": "๐Ÿ˜œ" + }, + "stuck_out_tongue_closed_eyes": { + "keywords": ["face", "prank", "playful", "mischievous", "smile"], + "char": "๐Ÿ˜" + }, + "disappointed": { + "keywords": ["face", "sad", "upset", "depressed"], + "char": "๐Ÿ˜ž" + }, + "worried": { + "keywords": ["face", "concern", "nervous"], + "char": "๐Ÿ˜Ÿ" + }, + "angry": { + "keywords": ["mad", "face", "annoyed", "frustrated"], + "char": "๐Ÿ˜ " + }, + "rage": { + "keywords": ["angry", "mad", "hate", "despise"], + "char": "๐Ÿ˜ก" + }, + "cry": { + "keywords": ["face", "tears", "sad", "depressed", "upset"], + "char": "๐Ÿ˜ข" + }, + "persevere": { + "keywords": ["face", "sick", "no", "upset", "oops"], + "char": "๐Ÿ˜ฃ" + }, + "triumph": { + "keywords": ["face", "gas", "phew", "proud", "pride"], + "char": "๐Ÿ˜ค" + }, + "disappointed_relieved": { + "keywords": ["face", "phew", "sweat", "nervous"], + "char": "๐Ÿ˜ฅ" + }, + "frowning": { + "keywords": ["face", "aw", "what"], + "char": "๐Ÿ˜ฆ" + }, + "anguished": { + "keywords": ["face", "stunned", "nervous"], + "char": "๐Ÿ˜ง" + }, + "fearful": { + "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"], + "char": "๐Ÿ˜จ" + }, + "weary": { + "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"], + "char": "๐Ÿ˜ฉ" + }, + "sleepy": { + "keywords": ["face", "tired", "rest", "nap"], + "char": "๐Ÿ˜ช" + }, + "tired_face": { + "keywords": ["sick", "whine", "upset", "frustrated"], + "char": "๐Ÿ˜ซ" + }, + "grimacing": { + "keywords": ["face", "grimace", "teeth"], + "char": "๐Ÿ˜ฌ" + }, + "sob": { + "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"], + "char": "๐Ÿ˜ญ" + }, + "open_mouth": { + "keywords": ["face", "surprise", "impressed", "wow"], + "char": "๐Ÿ˜ฎ" + }, + "hushed": { + "keywords": ["face", "woo", "shh"], + "char": "๐Ÿ˜ฏ" + }, + "cold_sweat": { + "keywords": ["face", "nervous"], + "char": "๐Ÿ˜ฐ" + }, + "scream": { + "keywords": ["face", "munch", "scared", "omg"], + "char": "๐Ÿ˜ฑ" + }, + "omg": { + "keywords": ["face", "munch", "scared"], + "char": "๐Ÿ˜ฑ" + }, + "astonished": { + "keywords": ["face", "xox", "surprised", "poisoned"], + "char": "๐Ÿ˜ฒ" + }, + "flushed": { + "keywords": ["face", "blush", "shy", "flattered"], + "char": "๐Ÿ˜ณ" + }, + "sleeping": { + "keywords": ["face", "tired", "sleepy", "night", "zzz"], + "char": "๐Ÿ˜ด" + }, + "dizzy_face": { + "keywords": ["spent", "unconscious", "xox"], + "char": "๐Ÿ˜ต" + }, + "no_mouth": { + "keywords": ["face", "hellokitty"], + "char": "๐Ÿ˜ถ" + }, + "mask": { + "keywords": ["face", "sick", "ill", "disease"], + "char": "๐Ÿ˜ท" + }, + "smile_cat": { + "keywords": ["animal", "cats"], + "char": "๐Ÿ˜ธ" + }, + "joy_cat": { + "keywords": ["animal", "cats", "haha", "happy", "tears"], + "char": "๐Ÿ˜น" + }, + "smiley_cat": { + "keywords": ["animal", "cats", "happy"], + "char": "๐Ÿ˜บ" + }, + "heart_eyes_cat": { + "keywords": ["animal", "love", "like", "affection", "cats", "valentines"], + "char": "๐Ÿ˜ป" + }, + "smirk_cat": { + "keywords": ["animal", "cats"], + "char": "๐Ÿ˜ผ" + }, + "kissing_cat": { + "keywords": ["animal", "cats"], + "char": "๐Ÿ˜ฝ" + }, + "pouting_cat": { + "keywords": ["animal", "cats"], + "char": "๐Ÿ˜พ" + }, + "crying_cat_face": { + "keywords": ["animal", "tears", "weep", "sad", "cats", "upset"], + "char": "๐Ÿ˜ฟ" + }, + "scream_cat": { + "keywords": ["animal", "cats", "munch", "scared"], + "char": "๐Ÿ™€" + }, + "footprints": { + "keywords": ["feet", "tracking", "walking", "beach"], + "char": "๐Ÿ‘ฃ" + }, + "bust_in_silhouette": { + "keywords": ["user", "person", "human"], + "char": "๐Ÿ‘ค" + }, + "busts_in_silhouette": { + "keywords": ["user", "person", "human", "group", "team"], + "char": "๐Ÿ‘ฅ" + }, + "baby": { + "keywords": ["child", "boy", "girl", "toddler"], + "char": "๐Ÿ‘ถ" + }, + "boy": { + "keywords": ["man", "male", "guy", "teenager"], + "char": "๐Ÿ‘ฆ" + }, + "girl": { + "keywords": ["female", "woman", "teenager"], + "char": "๐Ÿ‘ง" + }, + "man": { + "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"], + "char": "๐Ÿ‘จ" + }, + "woman": { + "keywords": ["female", "girls", "lady"], + "char": "๐Ÿ‘ฉ" + }, + "family": { + "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"], + "char": "๐Ÿ‘ช" + }, + "couple": { + "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"], + "char": "๐Ÿ‘ซ" + }, + "two_men_holding_hands": { + "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"], + "char": "๐Ÿ‘ฌ" + }, + "two_women_holding_hands": { + "keywords": ["pair", "friendship", "couple", "love", "like", "female", "people", "human"], + "char": "๐Ÿ‘ญ" + }, + "cop": { + "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"], + "char": "๐Ÿ‘ฎ" + }, + "dancers": { + "keywords": ["female", "bunny", "women", "girls"], + "char": "๐Ÿ‘ฏ" + }, + "bride_with_veil": { + "keywords": ["couple", "marriage", "wedding"], + "char": "๐Ÿ‘ฐ" + }, + "person_with_blond_hair": { + "keywords": ["man", "male", "boy", "blonde", "guy"], + "char": "๐Ÿ‘ฑ" + }, + "man_with_gua_pi_mao": { + "keywords": ["male", "boy"], + "char": "๐Ÿ‘ฒ" + }, + "man_with_turban": { + "keywords": ["male", "indian", "hinduism", "arabs"], + "char": "๐Ÿ‘ณ" + }, + "older_man": { + "keywords": ["human", "male", "men"], + "char": "๐Ÿ‘ด" + }, + "older_woman": { + "keywords": ["female", "women", "girl", "lady"], + "char": "๐Ÿ‘ต" + }, + "baby": { + "keywords": ["child", "boy", "girl", "toddler"], + "char": "๐Ÿ‘ถ" + }, + "construction_worker": { + "keywords": ["male", "human", "wip", "guy", "build"], + "char": "๐Ÿ‘ท" + }, + "princess": { + "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"], + "char": "๐Ÿ‘ธ" + }, + "guardsman": { + "keywords": ["uk", "gb", "british", "male", "guy", "royal"], + "char": "๐Ÿ’‚" + }, + "angel": { + "keywords": ["heaven", "wings", "halo"], + "char": "๐Ÿ‘ผ" + }, + "santa": { + "keywords": ["festival", "man", "male", "xmas", "father christmas"], + "char": "๐ŸŽ…" + }, + "ghost": { + "keywords": ["halloween", "spooky", "scary"], + "char": "๐Ÿ‘ป" + }, + "japanese_ogre": { + "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon"], + "char": "๐Ÿ‘น" + }, + "japanese_goblin": { + "keywords": ["red", "evil", "mask", "monster", "scary", "creepy"], + "char": "๐Ÿ‘บ" + }, + "hankey": { + "keywords": ["poop", "shitface", "fail", "turd"], + "char": "๐Ÿ’ฉ" + }, + "skull": { + "keywords": ["dead", "skeleton", "creepy"], + "char": "๐Ÿ’€" + }, + "alien": { + "keywords": ["UFO", "paul", "weird", "outer_space"], + "char": "๐Ÿ‘ฝ" + }, + "space_invader": { + "keywords": ["game", "arcade", "play"], + "char": "๐Ÿ‘พ" + }, + "bow": { + "keywords": ["man", "male", "boy"], + "char": "๐Ÿ™‡" + }, + "information_desk_person": { + "keywords": ["female", "girl", "woman", "human"], + "char": "๐Ÿ’" + }, + "no_good": { + "keywords": ["female", "girl", "woman", "nope"], + "char": "๐Ÿ™…" + }, + "ok_woman": { + "keywords": ["women", "girl", "female", "pink", "human"], + "char": "๐Ÿ™†" + }, + "raising_hand": { + "keywords": ["female", "girl", "woman"], + "char": "๐Ÿ™‹" + }, + "person_with_pouting_face": { + "keywords": ["female", "girl", "woman"], + "char": "๐Ÿ™Ž" + }, + "person_frowning": { + "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"], + "char": "๐Ÿ™" + }, + "massage": { + "keywords": ["female", "girl", "woman", "head"], + "char": "๐Ÿ’†" + }, + "haircut": { + "keywords": ["female", "girl", "woman"], + "char": "๐Ÿ’‡" + }, + "couple_with_heart": { + "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"], + "char": "๐Ÿ’‘" + }, + "couplekiss": { + "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"], + "char": "๐Ÿ’" + }, + "raised_hands": { + "keywords": ["gesture", "hooray", "yea", "celebration"], + "char": "๐Ÿ™Œ" + }, + "clap": { + "keywords": ["hands", "praise", "applause", "congrats", "yay"], + "char": "๐Ÿ‘" + }, + "ear": { + "keywords": ["face", "hear", "sound", "listen"], + "char": "๐Ÿ‘‚" + }, + "eyes": { + "keywords": ["look", "watch", "stalk", "peek", "see"], + "char": "๐Ÿ‘€" + }, + "nose": { + "keywords": ["smell", "sniff"], + "char": "๐Ÿ‘ƒ" + }, + "lips": { + "keywords": ["mouth", "kiss"], + "char": "๐Ÿ‘„" + }, + "kiss": { + "keywords": ["face", "lips", "love", "like", "affection", "valentines"], + "char": "๐Ÿ’‹" + }, + "tongue": { + "keywords": ["mouth", "playful"], + "char": "๐Ÿ‘…" + }, + "nail_care": { + "keywords": ["beauty", "manicure", "fashion"], + "char": "๐Ÿ’…" + }, + "wave": { + "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "palm"], + "char": "๐Ÿ‘‹" + }, + "+1": { + "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"], + "char": "๐Ÿ‘" + }, + "-1": { + "keywords": ["thumbsdown", "no", "dislike", "hand"], + "char": "๐Ÿ‘Ž" + }, + "point_up": { + "keywords": ["hand", "fingers", "direction"], + "char": "โ˜๏ธ" + }, + "point_up_2": { + "keywords": ["fingers", "hand", "direction"], + "char": "๐Ÿ‘†" + }, + "point_down": { + "keywords": ["fingers", "hand", "direction"], + "char": "๐Ÿ‘‡" + }, + "point_left": { + "keywords": ["direction", "fingers", "hand"], + "char": "๐Ÿ‘ˆ" + }, + "point_right": { + "keywords": ["fingers", "hand", "direction"], + "char": "๐Ÿ‘‰" + }, + "ok_hand": { + "keywords": ["fingers", "limbs", "perfect"], + "char": "๐Ÿ‘Œ" + }, + "v": { + "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"], + "char": "โœŒ๏ธ" + }, + "facepunch": { + "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"], + "char": "๐Ÿ‘Š" + }, + "fist": { + "keywords": ["fingers", "hand", "grasp"], + "char": "โœŠ" + }, + "hand": { + "keywords": ["fingers", "stop", "highfive", "palm", "ban", "raised_hand"], + "char": "โœ‹" + }, + "muscle": { + "keywords": ["arm", "flex", "hand", "summer", "strong"], + "char": "๐Ÿ’ช" + }, + "open_hands": { + "keywords": ["fingers", "butterfly"], + "char": "๐Ÿ‘" + }, + "pray": { + "keywords": ["please", "hope", "wish", "namaste", "highfive"], + "char": "๐Ÿ™" + }, + "seedling": { + "keywords": ["plant", "nature", "grass", "lawn", "spring"], + "char": "๐ŸŒฑ" + }, + "evergreen_tree": { + "keywords": ["plant", "nature"], + "char": "๐ŸŒฒ" + }, + "deciduous_tree": { + "keywords": ["plant", "nature"], + "char": "๐ŸŒณ" + }, + "palm_tree": { + "keywords": ["plant", "vegetable", "nature", "summer", "beach"], + "char": "๐ŸŒด" + }, + "cactus": { + "keywords": ["vegetable", "plant", "nature"], + "char": "๐ŸŒต" + }, + "tulip": { + "keywords": ["flowers", "plant", "nature", "summer", "spring"], + "char": "๐ŸŒท" + }, + "cherry_blossom": { + "keywords": ["nature", "plant", "spring", "flower"], + "char": "๐ŸŒธ" + }, + "rose": { + "keywords": ["flowers", "valentines", "love", "spring"], + "char": "๐ŸŒน" + }, + "hibiscus": { + "keywords": ["plant", "vegetable", "flowers", "beach"], + "char": "๐ŸŒบ" + }, + "sunflower": { + "keywords": ["nature", "plant", "fall"], + "char": "๐ŸŒป" + }, + "blossom": { + "keywords": ["nature", "flowers", "yellow"], + "char": "๐ŸŒผ" + }, + "bouquet": { + "keywords": ["flowers", "nature", "spring"], + "char": "๐Ÿ’" + }, + "ear_of_rice": { + "keywords": ["nature", "plant"], + "char": "๐ŸŒพ" + }, + "herb": { + "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"], + "char": "๐ŸŒฟ" + }, + "four_leaf_clover": { + "keywords": ["vegetable", "plant", "nature", "lucky"], + "char": "๐Ÿ€" + }, + "maple_leaf": { + "keywords": ["nature", "plant", "vegetable", "canada", "fall"], + "char": "๐Ÿ" + }, + "fallen_leaf": { + "keywords": ["nature", "plant", "vegetable", "leaves"], + "char": "๐Ÿ‚" + }, + "leaves": { + "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"], + "char": "๐Ÿƒ" + }, + "mushroom": { + "keywords": ["plant", "vegetable"], + "char": "๐Ÿ„" + }, + "chestnut": { + "keywords": ["food", "squirrel"], + "char": "๐ŸŒฐ" + }, + "rat": { + "keywords": ["animal", "mouse", "rodent"], + "char": "๐Ÿ€" + }, + "mouse2": { + "keywords": ["animal", "nature", "rodent"], + "char": "๐Ÿ" + }, + "mouse": { + "keywords": ["animal", "nature", "cheese"], + "char": "๐Ÿญ" + }, + "hamster": { + "keywords": ["animal", "nature"], + "char": "๐Ÿน" + }, + "ox": { + "keywords": ["animal", "cow", "beef"], + "char": "๐Ÿ‚" + }, + "water_buffalo": { + "keywords": ["animal", "nature", "ox", "cow"], + "char": "๐Ÿƒ" + }, + "cow2": { + "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"], + "char": "๐Ÿ„" + }, + "cow": { + "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"], + "char": "๐Ÿฎ" + }, + "tiger2": { + "keywords": ["animal", "nature", "roar"], + "char": "๐Ÿ…" + }, + "leopard": { + "keywords": ["animal", "nature"], + "char": "๐Ÿ†" + }, + "tiger": { + "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"], + "char": "๐Ÿฏ" + }, + "rabbit2": { + "keywords": ["animal", "nature", "pet", "magic", "spring"], + "char": "๐Ÿ‡" + }, + "rabbit": { + "keywords": ["animal", "nature", "pet", "spring", "magic"], + "char": "๐Ÿฐ" + }, + "cat2": { + "keywords": ["animal", "meow", "pet", "cats"], + "char": "๐Ÿˆ" + }, + "cat": { + "keywords": ["animal", "meow", "nature", "pet"], + "char": "๐Ÿฑ" + }, + "racehorse": { + "keywords": ["animal", "gamble", "luck"], + "char": "๐ŸŽ" + }, + "horse": { + "keywords": ["animal", "brown", "nature", "unicorn"], + "char": "๐Ÿด" + }, + "ram": { + "keywords": ["animal", "sheep", "nature"], + "char": "๐Ÿ" + }, + "sheep": { + "keywords": ["animal", "nature", "wool", "shipit"], + "char": "๐Ÿ‘" + }, + "goat": { + "keywords": ["animal", "nature"], + "char": "๐Ÿ" + }, + "rooster": { + "keywords": ["animal", "nature", "chicken"], + "char": "๐Ÿ“" + }, + "chicken": { + "keywords": ["animal", "cluck", "nature", "bird"], + "char": "๐Ÿ”" + }, + "baby_chick": { + "keywords": ["animal", "chicken", "bird"], + "char": "๐Ÿค" + }, + "hatching_chick": { + "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"], + "char": "๐Ÿฃ" + }, + "hatched_chick": { + "keywords": ["animal", "chicken", "baby", "bird"], + "char": "๐Ÿฅ" + }, + "bird": { + "keywords": ["animal", "nature", "fly", "tweet", "spring"], + "char": "๐Ÿฆ" + }, + "penguin": { + "keywords": ["animal", "nature"], + "char": "๐Ÿง" + }, + "elephant": { + "keywords": ["animal", "nature", "nose", "thailand", "circus"], + "char": "๐Ÿ˜" + }, + "dromedary_camel": { + "keywords": ["animal", "hot", "desert", "hump"], + "char": "๐Ÿช" + }, + "camel": { + "keywords": ["animal", "nature", "hot", "desert", "hump"], + "char": "๐Ÿซ" + }, + "boar": { + "keywords": ["animal", "nature"], + "char": "๐Ÿ—" + }, + "pig2": { + "keywords": ["animal", "nature"], + "char": "๐Ÿ–" + }, + "pig": { + "keywords": ["animal", "oink", "nature"], + "char": "๐Ÿท" + }, + "pig_nose": { + "keywords": ["animal", "oink"], + "char": "๐Ÿฝ" + }, + "dog2": { + "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"], + "char": "๐Ÿ•" + }, + "poodle": { + "keywords": ["dog", "animal", "101", "nature", "pet"], + "char": "๐Ÿฉ" + }, + "dog": { + "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"], + "char": "๐Ÿถ" + }, + "wolf": { + "keywords": ["animal", "nature", "wild"], + "char": "๐Ÿบ" + }, + "bear": { + "keywords": ["animal", "nature", "wild"], + "char": "๐Ÿป" + }, + "koala": { + "keywords": ["animal", "nature"], + "char": "๐Ÿจ" + }, + "panda_face": { + "keywords": ["animal", "nature"], + "char": "๐Ÿผ" + }, + "monkey_face": { + "keywords": ["animal", "nature", "circus"], + "char": "๐Ÿต" + }, + "see_no_evil": { + "keywords": ["monkey", "animal", "nature", "haha"], + "char": "๐Ÿ™ˆ" + }, + "hear_no_evil": { + "keywords": ["animal", "monkey", "nature"], + "char": "๐Ÿ™‰" + }, + "speak_no_evil": { + "keywords": ["monkey", "animal", "nature", "omg"], + "char": "๐Ÿ™Š" + }, + "monkey": { + "keywords": ["animal", "nature", "banana", "circus"], + "char": "๐Ÿ’" + }, + "dragon": { + "keywords": ["animal", "myth", "nature", "chinese", "green"], + "char": "๐Ÿ‰" + }, + "dragon_face": { + "keywords": ["animal", "myth", "nature", "chinese", "green"], + "char": "๐Ÿฒ" + }, + "crocodile": { + "keywords": ["animal", "nature", "reptile"], + "char": "๐ŸŠ" + }, + "snake": { + "keywords": ["animal", "evil", "nature", "hiss"], + "char": "๐Ÿ" + }, + "turtle": { + "keywords": ["animal", "slow", "nature", "tortoise"], + "char": "๐Ÿข" + }, + "frog": { + "keywords": ["animal", "nature", "croak"], + "char": "๐Ÿธ" + }, + "whale2": { + "keywords": ["animal", "nature", "sea", "ocean"], + "char": "๐Ÿ‹" + }, + "whale": { + "keywords": ["animal", "nature", "sea", "ocean"], + "char": "๐Ÿณ" + }, + "dolphin": { + "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"], + "char": "๐Ÿฌ" + }, + "octopus": { + "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"], + "char": "๐Ÿ™" + }, + "fish": { + "keywords": ["animal", "food", "nature"], + "char": "๐ŸŸ" + }, + "tropical_fish": { + "keywords": ["animal", "swim", "ocean", "beach"], + "char": "๐Ÿ " + }, + "blowfish": { + "keywords": ["animal", "nature", "food", "sea", "ocean"], + "char": "๐Ÿก" + }, + "shell": { + "keywords": ["nature", "sea", "beach"], + "char": "๐Ÿš" + }, + "snail": { + "keywords": ["slow", "animal", "shell"], + "char": "๐ŸŒ" + }, + "bug": { + "keywords": ["animal", "insect", "nature", "worm"], + "char": "๐Ÿ›" + }, + "ant": { + "keywords": ["animal", "insect", "nature", "bug"], + "char": "๐Ÿœ" + }, + "bee": { + "keywords": ["animal", "insect", "nature", "bug", "spring"], + "char": "๐Ÿ" + }, + "beetle": { + "keywords": ["animal", "insect", "nature", "bug"], + "char": "๐Ÿž" + }, + "feet": { + "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "paw_prints"], + "char": "๐Ÿพ" + }, + "zap": { + "keywords": ["thunder", "weather", "lightning bolt", "fast"], + "char": "โšก" + }, + "fire": { + "keywords": ["hot", "cook", "flame"], + "char": "๐Ÿ”ฅ" + }, + "crescent_moon": { + "keywords": ["night", "sleep", "sky", "evening", "magic"], + "char": "๐ŸŒ™" + }, + "sunny": { + "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"], + "char": "โ˜€๏ธ" + }, + "partly_sunny": { + "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"], + "char": "โ›…" + }, + "cloud": { + "keywords": ["weather", "sky"], + "char": "โ˜๏ธ" + }, + "droplet": { + "keywords": ["water", "drip", "faucet", "spring"], + "char": "๐Ÿ’ง" + }, + "sweat_drops": { + "keywords": ["water", "drip", "oops"], + "char": "๐Ÿ’ฆ" + }, + "umbrella": { + "keywords": ["rainy", "weather", "spring"], + "char": "โ˜”" + }, + "dash": { + "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"], + "char": "๐Ÿ’จ" + }, + "snowflake": { + "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"], + "char": "โ„๏ธ" + }, + "star2": { + "keywords": ["night", "sparkle", "awesome", "good", "magic"], + "char": "๐ŸŒŸ" + }, + "star": { + "keywords": ["night", "yellow"], + "char": "โญ" + }, + "stars": { + "keywords": ["night", "photo"], + "char": "๐ŸŒ " + }, + "sunrise_over_mountains": { + "keywords": ["view", "vacation", "photo"], + "char": "๐ŸŒ„" + }, + "sunrise": { + "keywords": ["morning", "view", "vacation", "photo"], + "char": "๐ŸŒ…" + }, + "rainbow": { + "keywords": ["nature", "happy", "unicorn", "photo", "sky", "spring"], + "char": "๐ŸŒˆ" + }, + "ocean": { + "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"], + "char": "๐ŸŒŠ" + }, + "volcano": { + "keywords": ["photo", "nature", "disaster"], + "char": "๐ŸŒ‹" + }, + "milky_way": { + "keywords": ["photo", "space", "stars"], + "char": "๐ŸŒŒ" + }, + "mount_fuji": { + "keywords": ["photo", "mountain", "nature", "japanese"], + "char": "๐Ÿ—ป" + }, + "japan": { + "keywords": ["nation", "country", "japanese", "asia"], + "char": "๐Ÿ—พ" + }, + "globe_with_meridians": { + "keywords": ["earth", "international", "world", "internet", "interweb"], + "char": "๐ŸŒ" + }, + "earth_africa": { + "keywords": ["globe", "world", "international"], + "char": "๐ŸŒ" + }, + "earth_americas": { + "keywords": ["globe", "world", "USA", "international"], + "char": "๐ŸŒŽ" + }, + "earth_asia": { + "keywords": ["globe", "world", "east", "international"], + "char": "๐ŸŒ" + }, + "new_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ‘" + }, + "waxing_crescent_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ’" + }, + "first_quarter_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ“" + }, + "moon": { + "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"], + "char": "๐ŸŒ”" + }, + "full_moon": { + "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ•" + }, + "waning_gibbous_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"], + "char": "๐ŸŒ–" + }, + "last_quarter_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ—" + }, + "waning_crescent_moon": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ˜" + }, + "new_moon_with_face": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒš" + }, + "full_moon_with_face": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ" + }, + "first_quarter_moon_with_face": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒ›" + }, + "last_quarter_moon_with_face": { + "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "char": "๐ŸŒœ" + }, + "sun_with_face": { + "keywords": ["nature", "morning", "sky"], + "char": "๐ŸŒž" + }, + "tomato": { + "keywords": ["fruit", "vegetable", "nature", "food"], + "char": "๐Ÿ…" + }, + "eggplant": { + "keywords": ["vegetable", "nature", "food", "aubergine"], + "char": "๐Ÿ†" + }, + "corn": { + "keywords": ["food", "vegetable", "plant"], + "char": "๐ŸŒฝ" + }, + "sweet_potato": { + "keywords": ["food", "nature"], + "char": "๐Ÿ " + }, + "grapes": { + "keywords": ["fruit", "food", "wine"], + "char": "๐Ÿ‡" + }, + "melon": { + "keywords": ["fruit", "nature", "food"], + "char": "๐Ÿˆ" + }, + "watermelon": { + "keywords": ["fruit", "food", "picnic", "summer"], + "char": "๐Ÿ‰" + }, + "tangerine": { + "keywords": ["food", "fruit", "nature"], + "char": "๐ŸŠ" + }, + "lemon": { + "keywords": ["fruit", "nature"], + "char": "๐Ÿ‹" + }, + "banana": { + "keywords": ["fruit", "food", "monkey"], + "char": "๐ŸŒ" + }, + "pineapple": { + "keywords": ["fruit", "nature", "food"], + "char": "๐Ÿ" + }, + "apple": { + "keywords": ["fruit", "mac", "school"], + "char": "๐ŸŽ" + }, + "green_apple": { + "keywords": ["fruit", "nature"], + "char": "๐Ÿ" + }, + "pear": { + "keywords": ["fruit", "nature", "food"], + "char": "๐Ÿ" + }, + "peach": { + "keywords": ["fruit", "nature", "food"], + "char": "๐Ÿ‘" + }, + "cherries": { + "keywords": ["food", "fruit"], + "char": "๐Ÿ’" + }, + "strawberry": { + "keywords": ["fruit", "food", "nature"], + "char": "๐Ÿ“" + }, + "hamburger": { + "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"], + "char": "๐Ÿ”" + }, + "pizza": { + "keywords": ["food", "party"], + "char": "๐Ÿ•" + }, + "meat_on_bone": { + "keywords": ["good", "food", "drumstick"], + "char": "๐Ÿ–" + }, + "poultry_leg": { + "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"], + "char": "๐Ÿ—" + }, + "rice_cracker": { + "keywords": ["food", "japanese"], + "char": "๐Ÿ˜" + }, + "rice_ball": { + "keywords": ["food", "japanese"], + "char": "๐Ÿ™" + }, + "rice": { + "keywords": ["food", "china", "asian"], + "char": "๐Ÿš" + }, + "curry": { + "keywords": ["food", "spicy", "hot", "indian"], + "char": "๐Ÿ›" + }, + "ramen": { + "keywords": ["food", "japanese", "noodle", "chipsticks"], + "char": "๐Ÿœ" + }, + "spaghetti": { + "keywords": ["food", "italian", "noodle"], + "char": "๐Ÿ" + }, + "bread": { + "keywords": ["food", "wheat", "breakfast", "toast"], + "char": "๐Ÿž" + }, + "fries": { + "keywords": ["chips", "snack", "fast food"], + "char": "๐ŸŸ" + }, + "dango": { + "keywords": ["food", "barbecue", "meat"], + "char": "๐Ÿก" + }, + "oden": { + "keywords": ["food", "japanese"], + "char": "๐Ÿข" + }, + "sushi": { + "keywords": ["food", "fish", "japanese", "rice"], + "char": "๐Ÿฃ" + }, + "fried_shrimp": { + "keywords": ["food", "animal", "appetizer", "summer"], + "char": "๐Ÿค" + }, + "fish_cake": { + "keywords": ["food", "japan", "sea", "beach"], + "char": "๐Ÿฅ" + }, + "icecream": { + "keywords": ["food", "hot", "dessert", "summer"], + "char": "๐Ÿฆ" + }, + "shaved_ice": { + "keywords": ["hot", "dessert", "summer"], + "char": "๐Ÿง" + }, + "ice_cream": { + "keywords": ["food", "hot", "dessert"], + "char": "๐Ÿจ" + }, + "doughnut": { + "keywords": ["food", "dessert", "snack", "sweet", "donut"], + "char": "๐Ÿฉ" + }, + "cookie": { + "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"], + "char": "๐Ÿช" + }, + "chocolate_bar": { + "keywords": ["food", "snack", "dessert", "sweet"], + "char": "๐Ÿซ" + }, + "candy": { + "keywords": ["snack", "dessert", "sweet"], + "char": "๐Ÿฌ" + }, + "lollipop": { + "keywords": ["food", "snack", "candy", "sweet"], + "char": "๐Ÿญ" + }, + "custard": { + "keywords": ["dessert", "food"], + "char": "๐Ÿฎ" + }, + "honey_pot": { + "keywords": ["bees", "sweet", "kitchen"], + "char": "๐Ÿฏ" + }, + "cake": { + "keywords": ["food", "dessert"], + "char": "๐Ÿฐ" + }, + "bento": { + "keywords": ["food", "japanese", "box"], + "char": "๐Ÿฑ" + }, + "stew": { + "keywords": ["food", "meat", "soup"], + "char": "๐Ÿฒ" + }, + "egg": { + "keywords": ["food", "breakfast", "kitchen"], + "char": "๐Ÿณ" + }, + "fork_and_knife": { + "keywords": ["cutlery", "kitchen"], + "char": "๐Ÿด" + }, + "tea": { + "keywords": ["drink", "bowl", "breakfast", "green", "british"], + "char": "๐Ÿต" + }, + "coffee": { + "keywords": ["drink", "beverage", "cafe", "espresso"], + "char": "โ˜•" + }, + "sake": { + "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"], + "char": "๐Ÿถ" + }, + "wine_glass": { + "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"], + "char": "๐Ÿท" + }, + "cocktail": { + "keywords": ["drink", "drunk", "alcohol", "beverage", "booze"], + "char": "๐Ÿธ" + }, + "tropical_drink": { + "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze"], + "char": "๐Ÿน" + }, + "beer": { + "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"], + "char": "๐Ÿบ" + }, + "beers": { + "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"], + "char": "๐Ÿป" + }, + "baby_bottle": { + "keywords": ["food", "container", "milk"], + "char": "๐Ÿผ" + }, + "ribbon": { + "keywords": ["decoration", "pink", "girl", "bowtie"], + "char": "๐ŸŽ€" + }, + "gift": { + "keywords": ["present", "birthday", "christmas", "xmas"], + "char": "๐ŸŽ" + }, + "birthday": { + "keywords": ["party", "cake", "celebration"], + "char": "๐ŸŽ‚" + }, + "jack_o_lantern": { + "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"], + "char": "๐ŸŽƒ" + }, + "christmas_tree": { + "keywords": ["festival", "vacation", "december", "xmas", "celebration"], + "char": "๐ŸŽ„" + }, + "tanabata_tree": { + "keywords": ["plant", "nature", "branch", "summer"], + "char": "๐ŸŽ‹" + }, + "bamboo": { + "keywords": ["plant", "nature", "vegetable", "panda"], + "char": "๐ŸŽ" + }, + "rice_scene": { + "keywords": ["photo", "japan", "asia", "tsukimi"], + "char": "๐ŸŽ‘" + }, + "fireworks": { + "keywords": ["photo", "festival", "carnival", "congratulations"], + "char": "๐ŸŽ†" + }, + "sparkler": { + "keywords": ["stars", "night", "shine"], + "char": "๐ŸŽ‡" + }, + "tada": { + "keywords": ["party", "contulations", "birthday", "magic", "circus"], + "char": "๐ŸŽ‰" + }, + "confetti_ball": { + "keywords": ["festival", "party", "birthday", "circus"], + "char": "๐ŸŽŠ" + }, + "balloon": { + "keywords": ["party", "celebration", "birthday", "circus"], + "char": "๐ŸŽˆ" + }, + "dizzy": { + "keywords": ["star", "sparkle", "shoot", "magic"], + "char": "๐Ÿ’ซ" + }, + "sparkles": { + "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"], + "char": "โœจ" + }, + "boom": { + "keywords": ["bomb", "explode", "explosion", "collision", "blown"], + "char": "๐Ÿ’ฅ" + }, + "mortar_board": { + "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"], + "char": "๐ŸŽ“" + }, + "crown": { + "keywords": ["king", "kod", "leader", "royalty", "lord"], + "char": "๐Ÿ‘‘" + }, + "dolls": { + "keywords": ["japanese", "toy", "kimono"], + "char": "๐ŸŽŽ" + }, + "flags": { + "keywords": ["fish", "japanese", "koinobori", "carp", "banner"], + "char": "๐ŸŽ" + }, + "wind_chime": { + "keywords": ["nature", "ding", "spring", "bell"], + "char": "๐ŸŽ" + }, + "crossed_flags": { + "keywords": ["japanese", "nation", "country", "border"], + "char": "๐ŸŽŒ" + }, + "izakaya_lantern": { + "keywords": ["light", "paper", "halloween", "spooky"], + "char": "๐Ÿฎ" + }, + "ring": { + "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem"], + "char": "๐Ÿ’" + }, + "heart": { + "keywords": ["love", "like", "valentines"], + "char": "โค๏ธ" + }, + "love": { + "keywords": ["like", "valentines"], + "char": "โค๏ธ" + }, + "broken_heart": { + "keywords": ["sad", "sorry", "break"], + "char": "๐Ÿ’”" + }, + "love_letter": { + "keywords": ["email", "like", "affection", "envelope", "valentines"], + "char": "๐Ÿ’Œ" + }, + "two_hearts": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’•" + }, + "revolving_hearts": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’ž" + }, + "heartbeat": { + "keywords": ["love", "like", "affection", "valentines", "pink"], + "char": "๐Ÿ’“" + }, + "heartpulse": { + "keywords": ["like", "love", "affection", "valentines", "pink"], + "char": "๐Ÿ’—" + }, + "sparkling_heart": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’–" + }, + "cupid": { + "keywords": ["love", "like", "heart", "affection", "valentines"], + "char": "๐Ÿ’˜" + }, + "gift_heart": { + "keywords": ["love", "valentines"], + "char": "๐Ÿ’" + }, + "heart_decoration": { + "keywords": ["purple-square", "love", "like"], + "char": "๐Ÿ’Ÿ" + }, + "purple_heart": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’œ" + }, + "yellow_heart": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’›" + }, + "green_heart": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’š" + }, + "blue_heart": { + "keywords": ["love", "like", "affection", "valentines"], + "char": "๐Ÿ’™" + }, + "runner": { + "keywords": ["man", "walking", "exercise", "race", "running"], + "char": "๐Ÿƒ" + }, + "walking": { + "keywords": ["human", "feet", "steps"], + "char": "๐Ÿšถ" + }, + "dancer": { + "keywords": ["female", "girl", "woman", "fun"], + "char": "๐Ÿ’ƒ" + }, + "rowboat": { + "keywords": ["sports", "hobby", "water", "ship"], + "char": "๐Ÿšฃ" + }, + "swimmer": { + "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"], + "char": "๐ŸŠ" + }, + "surfer": { + "keywords": ["sports", "ocean", "sea", "summer", "beach"], + "char": "๐Ÿ„" + }, + "bath": { + "keywords": ["clean", "shower", "bathroom"], + "char": "๐Ÿ›€" + }, + "snowboarder": { + "keywords": ["sports", "winter"], + "char": "๐Ÿ‚" + }, + "ski": { + "keywords": ["sports", "winter", "cold", "snow"], + "char": "๐ŸŽฟ" + }, + "snowman": { + "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"], + "char": "โ›„" + }, + "bicyclist": { + "keywords": ["sports", "bike", "exercise", "hipster"], + "char": "๐Ÿšด" + }, + "mountain_bicyclist": { + "keywords": ["transportation", "sports", "human", "race", "bike"], + "char": "๐Ÿšต" + }, + "horse_racing": { + "keywords": ["animal", "betting", "competition", "gambling", "luck"], + "char": "๐Ÿ‡" + }, + "tent": { + "keywords": ["photo", "camp", "outdoors"], + "char": "โ›บ" + }, + "fishing_pole_and_fish": { + "keywords": ["food", "hobby", "summer"], + "char": "๐ŸŽฃ" + }, + "soccer": { + "keywords": ["sports", "balls", "football", "fifa"], + "char": "โšฝ" + }, + "basketball": { + "keywords": ["sports", "balls", "NBA"], + "char": "๐Ÿ€" + }, + "football": { + "keywords": ["sports", "balls", "NFL"], + "char": "๐Ÿˆ" + }, + "baseball": { + "keywords": ["sports", "balls", "MLB"], + "char": "โšพ๏ธ" + }, + "tennis": { + "keywords": ["sports", "balls", "green"], + "char": "๐ŸŽพ" + }, + "rugby_football": { + "keywords": ["sports", "team"], + "char": "๐Ÿ‰" + }, + "golf": { + "keywords": ["sports", "business", "flag", "hole", "summer"], + "char": "โ›ณ" + }, + "trophy": { + "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"], + "char": "๐Ÿ†" + }, + "running_shirt_with_sash": { + "keywords": ["play", "pageant"], + "char": "๐ŸŽฝ" + }, + "checkered_flag": { + "keywords": ["contest", "finishline", "rase", "gokart"], + "char": "๐Ÿ" + }, + "musical_keyboard": { + "keywords": ["piano", "instrument"], + "char": "๐ŸŽน" + }, + "guitar": { + "keywords": ["music", "instrument"], + "char": "๐ŸŽธ" + }, + "violin": { + "keywords": ["music", "instrument", "orchestra", "symphony"], + "char": "๐ŸŽป" + }, + "saxophone": { + "keywords": ["music", "instrument", "jazz", "blues"], + "char": "๐ŸŽท" + }, + "trumpet": { + "keywords": ["music", "brass"], + "char": "๐ŸŽบ" + }, + "musical_note": { + "keywords": ["score", "tone", "sound"], + "char": "๐ŸŽต" + }, + "notes": { + "keywords": ["music", "score"], + "char": "๐ŸŽถ" + }, + "musical_score": { + "keywords": ["treble", "clef"], + "char": "๐ŸŽผ" + }, + "headphones": { + "keywords": ["music", "score", "gadgets"], + "char": "๐ŸŽง" + }, + "microphone": { + "keywords": ["sound", "music", "PA"], + "char": "๐ŸŽค" + }, + "performing_arts": { + "keywords": ["acting", "theater", "drama"], + "char": "๐ŸŽญ" + }, + "ticket": { + "keywords": ["event", "concert", "pass"], + "char": "๐ŸŽซ" + }, + "tophat": { + "keywords": ["magic", "gentleman", "classy", "circus"], + "char": "๐ŸŽฉ" + }, + "circus_tent": { + "keywords": ["festival", "carnival", "party"], + "char": "๐ŸŽช" + }, + "clapper": { + "keywords": ["movie", "film", "record"], + "char": "๐ŸŽฌ" + }, + "art": { + "keywords": ["design", "paint", "draw"], + "char": "๐ŸŽจ" + }, + "dart": { + "keywords": ["game", "play", "bar"], + "char": "๐ŸŽฏ" + }, + "8ball": { + "keywords": ["pool", "hobby", "game", "luck", "magic"], + "char": "๐ŸŽฑ" + }, + "bowling": { + "keywords": ["sports", "fun", "play"], + "char": "๐ŸŽณ" + }, + "slot_machine": { + "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"], + "char": "๐ŸŽฐ" + }, + "game_die": { + "keywords": ["dice", "random", "tabbletop", "play", "luck"], + "char": "๐ŸŽฒ" + }, + "video_game": { + "keywords": ["play", "console", "PS4", "controller"], + "char": "๐ŸŽฎ" + }, + "flower_playing_cards": { + "keywords": ["game", "sunset", "red"], + "char": "๐ŸŽด" + }, + "black_joker": { + "keywords": ["poker", "cards", "game", "play", "magic"], + "char": "๐Ÿƒ" + }, + "mahjong": { + "keywords": ["game", "play", "chinese", "kanji"], + "char": "๐Ÿ€„" + }, + "carousel_horse": { + "keywords": ["photo", "carnival"], + "char": "๐ŸŽ " + }, + "ferris_wheel": { + "keywords": ["photo", "carnival", "londoneye"], + "char": "๐ŸŽก" + }, + "roller_coaster": { + "keywords": ["carnival", "playground", "photo", "fun"], + "char": "๐ŸŽข" + }, + "railway_car": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿšƒ" + }, + "mountain_railway": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿšž" + }, + "steam_locomotive": { + "keywords": ["transportation", "vehicle", "train"], + "char": "๐Ÿš‚" + }, + "train": { + "keywords": ["transportation", "vehicle", "carriage", "public", "travel"], + "char": "๐Ÿš‹" + }, + "monorail": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿš" + }, + "bullettrain_side": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿš„" + }, + "bullettrain_front": { + "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"], + "char": "๐Ÿš…" + }, + "train2": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿš†" + }, + "metro": { + "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"], + "char": "๐Ÿš‡" + }, + "light_rail": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿšˆ" + }, + "station": { + "keywords": ["transportation", "vehicle", "public"], + "char": "๐Ÿš‰" + }, + "tram": { + "keywords": ["transportation", "vehicle"], + "char": "๐ŸšŠ" + }, + "bus": { + "keywords": ["car", "vehicle", "transportation"], + "char": "๐ŸšŒ" + }, + "oncoming_bus": { + "keywords": ["vehicle", "transportation"], + "char": "๐Ÿš" + }, + "trolleybus": { + "keywords": ["bart", "transportation", "vehicle"], + "char": "๐ŸšŽ" + }, + "minibus": { + "keywords": ["vehicle", "car", "transportation"], + "char": "๐Ÿš" + }, + "ambulance": { + "keywords": ["health", "911", "hospital"], + "char": "๐Ÿš‘" + }, + "fire_engine": { + "keywords": ["transportation", "cars", "vehicle"], + "char": "๐Ÿš’" + }, + "police_car": { + "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"], + "char": "๐Ÿš“" + }, + "oncoming_police_car": { + "keywords": ["vehicle", "law", "legal", "enforcement", "911"], + "char": "๐Ÿš”" + }, + "rotating_light": { + "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"], + "char": "๐Ÿšจ" + }, + "taxi": { + "keywords": ["uber", "vehicle", "cars", "transportation"], + "char": "๐Ÿš•" + }, + "oncoming_taxi": { + "keywords": ["vehicle", "cars", "uber"], + "char": "๐Ÿš–" + }, + "car": { + "keywords": ["red", "transportation", "vehicle"], + "char": "๐Ÿš—" + }, + "oncoming_automobile": { + "keywords": ["car", "vehicle", "transportation"], + "char": "๐Ÿš˜" + }, + "blue_car": { + "keywords": ["transportation", "vehicle"], + "char": "๐Ÿš™" + }, + "truck": { + "keywords": ["cars", "transportation"], + "char": "๐Ÿšš" + }, + "articulated_lorry": { + "keywords": ["vehicle", "cars", "transportation", "express"], + "char": "๐Ÿš›" + }, + "tractor": { + "keywords": ["vehicle", "car", "farming", "agriculture"], + "char": "๐Ÿšœ" + }, + "bike": { + "keywords": ["sports", "bicycle", "exercise", "hipster"], + "char": "๐Ÿšฒ" + }, + "busstop": { + "keywords": ["transportation", "wait"], + "char": "๐Ÿš" + }, + "fuelpump": { + "keywords": ["gas station", "petroleum"], + "char": "โ›ฝ" + }, + "construction": { + "keywords": ["wip", "progress", "caution", "warning"], + "char": "๐Ÿšง" + }, + "vertical_traffic_light": { + "keywords": ["transportation", "driving"], + "char": "๐Ÿšฆ" + }, + "traffic_light": { + "keywords": ["transportation", "signal"], + "char": "๐Ÿšฅ" + }, + "rocket": { + "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"], + "char": "๐Ÿš€" + }, + "helicopter": { + "keywords": ["transportation", "vehicle", "fly"], + "char": "๐Ÿš" + }, + "airplane": { + "keywords": ["vehicle", "transportation", "flight", "fly"], + "char": "โœˆ๏ธ" + }, + "seat": { + "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"], + "char": "๐Ÿ’บ" + }, + "anchor": { + "keywords": ["ship", "ferry", "sea", "boat"], + "char": "โš“" + }, + "ship": { + "keywords": ["transportation", "titanic", "deploy"], + "char": "๐Ÿšข" + }, + "speedboat": { + "keywords": ["ship", "transportation", "vehicle", "summer"], + "char": "๐Ÿšค" + }, + "boat": { + "keywords": ["ship", "summer", "transportation", "water", "sailing", "sailboat"], + "char": "โ›ต" + }, + "aerial_tramway": { + "keywords": ["transportation", "vehicle", "ski"], + "char": "๐Ÿšก" + }, + "mountain_cableway": { + "keywords": ["transportation", "vehicle", "ski"], + "char": "๐Ÿš " + }, + "suspension_railway": { + "keywords": ["vehicle", "transportation"], + "char": "๐ŸšŸ" + }, + "passport_control": { + "keywords": ["custom", "blue-square"], + "char": "๐Ÿ›‚" + }, + "customs": { + "keywords": ["passport", "border", "blue-square"], + "char": "๐Ÿ›ƒ" + }, + "baggage_claim": { + "keywords": ["blue-square", "airport", "transport"], + "char": "๐Ÿ›„" + }, + "left_luggage": { + "keywords": ["blue-square", "travel"], + "char": "๐Ÿ›…" + }, + "yen": { + "keywords": ["money", "sales", "japanese", "dollar", "currency"], + "char": "๐Ÿ’ด" + }, + "euro": { + "keywords": ["money", "sales", "dollar", "currency"], + "char": "๐Ÿ’ถ" + }, + "pound": { + "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"], + "char": "๐Ÿ’ท" + }, + "dollar": { + "keywords": ["money", "sales", "bill", "currency"], + "char": "๐Ÿ’ต" + }, + "statue_of_liberty": { + "keywords": ["american", "newyork"], + "char": "๐Ÿ—ฝ" + }, + "moyai": { + "keywords": ["stone", "easter island", "beach", "statue"], + "char": "๐Ÿ—ฟ" + }, + "foggy": { + "keywords": ["photo", "mountain"], + "char": "๐ŸŒ" + }, + "tokyo_tower": { + "keywords": ["photo", "japanese"], + "char": "๐Ÿ—ผ" + }, + "fountain": { + "keywords": ["photo", "summer", "water", "fresh"], + "char": "โ›ฒ" + }, + "european_castle": { + "keywords": ["building", "royalty", "history"], + "char": "๐Ÿฐ" + }, + "japanese_castle": { + "keywords": ["photo", "building"], + "char": "๐Ÿฏ" + }, + "city_sunrise": { + "keywords": ["photo", "good morning", "dawn"], + "char": "๐ŸŒ‡" + }, + "city_sunset": { + "keywords": ["photo", "evening", "sky", "buildings"], + "char": "๐ŸŒ†" + }, + "night_with_stars": { + "keywords": ["evening", "city", "downtown"], + "char": "๐ŸŒƒ" + }, + "bridge_at_night": { + "keywords": ["photo", "sanfrancisco"], + "char": "๐ŸŒ‰" + }, + "house": { + "keywords": ["building", "home"], + "char": "๐Ÿ " + }, + "house_with_garden": { + "keywords": ["home", "plant", "nature"], + "char": "๐Ÿก" + }, + "office": { + "keywords": ["building", "bureau", "work"], + "char": "๐Ÿข" + }, + "department_store": { + "keywords": ["building", "shopping", "mall"], + "char": "๐Ÿฌ" + }, + "factory": { + "keywords": ["building", "industry", "pollution", "smoke"], + "char": "๐Ÿญ" + }, + "post_office": { + "keywords": ["building", "email", "communication"], + "char": "๐Ÿฃ" + }, + "european_post_office": { + "keywords": ["building", "email"], + "char": "๐Ÿค" + }, + "hospital": { + "keywords": ["building", "health", "surgery", "doctor"], + "char": "๐Ÿฅ" + }, + "bank": { + "keywords": ["building", "money", "sales", "cash", "business", "enterprise"], + "char": "๐Ÿฆ" + }, + "hotel": { + "keywords": ["building", "whotel", "accomodation", "checkin"], + "char": "๐Ÿจ" + }, + "love_hotel": { + "keywords": ["like", "affection", "dating"], + "char": "๐Ÿฉ" + }, + "wedding": { + "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"], + "char": "๐Ÿ’’" + }, + "church": { + "keywords": ["building", "religion", "christ"], + "char": "โ›ช" + }, + "convenience_store": { + "keywords": ["building", "shopping", "groceries"], + "char": "๐Ÿช" + }, + "school": { + "keywords": ["building", "student", "education", "learn", "teach"], + "char": "๐Ÿซ" + }, + "cn": { + "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"], + "char": "๐Ÿ‡จ๐Ÿ‡ณ" + }, + "fr": { + "keywords": ["banner", "flag", "nation", "france", "french", "country"], + "char": "๐Ÿ‡ซ๐Ÿ‡ท" + }, + "de": { + "keywords": ["german", "nation", "flag", "country", "banner"], + "char": "๐Ÿ‡ฉ๐Ÿ‡ช" + }, + "it": { + "keywords": ["italy", "flag", "nation", "country", "banner"], + "char": "๐Ÿ‡ฎ๐Ÿ‡น" + }, + "jp": { + "keywords": ["japanese", "nation", "flag", "country", "banner"], + "char": "๐Ÿ‡ฏ๐Ÿ‡ต" + }, + "kr": { + "keywords": ["korea", "nation", "flag", "country", "banner"], + "char": "๐Ÿ‡ฐ๐Ÿ‡ท" + }, + "ru": { + "keywords": ["russian", "nation", "flag", "country", "banner"], + "char": "๐Ÿ‡ท๐Ÿ‡บ" + }, + "es": { + "keywords": ["spain", "flag", "nation", "country", "banner"], + "char": "๐Ÿ‡ช๐Ÿ‡ธ" + }, + "gb": { + "keywords": ["banner", "nation", "flag", "british", "UK", "country", "english", "england", "union jack"], + "char": "๐Ÿ‡ฌ๐Ÿ‡ง" + }, + "us": { + "keywords": ["nation", "flag", "american", "country", "banner"], + "char": "๐Ÿ‡บ๐Ÿ‡ธ" + }, + "watch": { + "keywords": ["time", "accessories"], + "char": "โŒš" + }, + "iphone": { + "keywords": ["technology", "apple", "gadgets", "dial"], + "char": "๐Ÿ“ฑ" + }, + "calling": { + "keywords": ["iphone", "incoming"], + "char": "๐Ÿ“ฒ" + }, + "computer": { + "keywords": ["tech", "laptop", "screen", "display", "monitor"], + "char": "๐Ÿ’ป" + }, + "alarm_clock": { + "keywords": ["time", "wake"], + "char": "โฐ" + }, + "hourglass_flowing_sand": { + "keywords": ["oldschool", "time", "countdown"], + "char": "โณ" + }, + "hourglass": { + "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"], + "char": "โŒ›" + }, + "camera": { + "keywords": ["gadgets", "photo"], + "char": "๐Ÿ“ท" + }, + "video_camera": { + "keywords": ["film", "record"], + "char": "๐Ÿ“น" + }, + "movie_camera": { + "keywords": ["film", "record"], + "char": "๐ŸŽฅ" + }, + "tv": { + "keywords": ["technology", "program", "oldschool", "show", "television"], + "char": "๐Ÿ“บ" + }, + "radio": { + "keywords": ["communication", "music", "podcast", "program"], + "char": "๐Ÿ“ป" + }, + "pager": { + "keywords": ["bbcall", "oldschool"], + "char": "๐Ÿ“Ÿ" + }, + "telephone_receiver": { + "keywords": ["technology", "communication", "dial"], + "char": "๐Ÿ“ž" + }, + "phone": { + "keywords": ["technology", "communication", "dial", "telephone"], + "char": "โ˜Ž๏ธ" + }, + "fax": { + "keywords": ["communication", "technology"], + "char": "๐Ÿ“ " + }, + "minidisc": { + "keywords": ["technology", "record", "data", "disk"], + "char": "๐Ÿ’ฝ" + }, + "floppy_disk": { + "keywords": ["oldschool", "technology", "save", "90s"], + "char": "๐Ÿ’พ" + }, + "cd": { + "keywords": ["technology", "dvd", "disk", "disc"], + "char": "๐Ÿ’ฟ" + }, + "dvd": { + "keywords": ["cd", "disk", "disc"], + "char": "๐Ÿ“€" + }, + "vhs": { + "keywords": ["record", "video", "oldschool", "90s", "80s"], + "char": "๐Ÿ“ผ" + }, + "battery": { + "keywords": ["power", "energy", "sustain"], + "char": "๐Ÿ”‹" + }, + "electric_plug": { + "keywords": ["charger", "power"], + "char": "๐Ÿ”Œ" + }, + "bulb": { + "keywords": ["light", "electricity", "idea"], + "char": "๐Ÿ’ก" + }, + "flashlight": { + "keywords": ["dark", "camping", "sight", "night"], + "char": "๐Ÿ”ฆ" + }, + "satellite": { + "keywords": ["communication", "future", "radio", "space"], + "char": "๐Ÿ“ก" + }, + "credit_card": { + "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"], + "char": "๐Ÿ’ณ" + }, + "money_with_wings": { + "keywords": ["dollar", "bills", "payment", "sale"], + "char": "๐Ÿ’ธ" + }, + "moneybag": { + "keywords": ["dollar", "payment", "coins", "sale"], + "char": "๐Ÿ’ฐ" + }, + "gem": { + "keywords": ["blue", "ruby", "diamond", "jewelry"], + "char": "๐Ÿ’Ž" + }, + "closed_umbrella": { + "keywords": ["weather", "rain", "drizzle"], + "char": "๐ŸŒ‚" + }, + "pouch": { + "keywords": ["bag", "accessories", "shopping"], + "char": "๐Ÿ‘" + }, + "purse": { + "keywords": ["fashion", "accessories", "money", "sales", "shopping"], + "char": "๐Ÿ‘›" + }, + "handbag": { + "keywords": ["fashion", "accessory", "accessories", "shopping"], + "char": "๐Ÿ‘œ" + }, + "briefcase": { + "keywords": ["business", "documents", "work", "law", "legal"], + "char": "๐Ÿ’ผ" + }, + "school_satchel": { + "keywords": ["student", "education", "bag"], + "char": "๐ŸŽ’" + }, + "lipstick": { + "keywords": ["female", "girl", "fashion", "woman"], + "char": "๐Ÿ’„" + }, + "eyeglasses": { + "keywords": ["fashion", "accessories", "eyesight", "nerd", "dork"], + "char": "๐Ÿ‘“" + }, + "womans_hat": { + "keywords": ["fashion", "accessories", "female", "lady", "spring"], + "char": "๐Ÿ‘’" + }, + "sandal": { + "keywords": ["shoes", "fashion"], + "char": "๐Ÿ‘ก" + }, + "high_heel": { + "keywords": ["fashion", "shoes", "female", "pumps"], + "char": "๐Ÿ‘ " + }, + "boot": { + "keywords": ["shoes", "fashion"], + "char": "๐Ÿ‘ข" + }, + "mans_shoe": { + "keywords": ["fashion", "male"], + "char": "๐Ÿ‘ž" + }, + "athletic_shoe": { + "keywords": ["shoes", "sports"], + "char": "๐Ÿ‘Ÿ" + }, + "bikini": { + "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"], + "char": "๐Ÿ‘™" + }, + "dress": { + "keywords": ["clothes", "fashion", "shopping"], + "char": "๐Ÿ‘—" + }, + "kimono": { + "keywords": ["dress", "fashion", "women", "female", "japanese"], + "char": "๐Ÿ‘˜" + }, + "womans_clothes": { + "keywords": ["fashion", "shopping", "female"], + "char": "๐Ÿ‘š" + }, + "shirt": { + "keywords": ["fashion", "cloth", "casual", "tshirt"], + "char": "๐Ÿ‘•" + }, + "necktie": { + "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"], + "char": "๐Ÿ‘”" + }, + "jeans": { + "keywords": ["fashion", "shopping"], + "char": "๐Ÿ‘–" + }, + "door": { + "keywords": ["house", "entry", "exit"], + "char": "๐Ÿšช" + }, + "shower": { + "keywords": ["clean", "water", "bathroom"], + "char": "๐Ÿšฟ" + }, + "bathtub": { + "keywords": ["clean", "shower", "bathroom"], + "char": "๐Ÿ›" + }, + "toilet": { + "keywords": ["restroom", "wc", "washroom", "bathroom"], + "char": "๐Ÿšฝ" + }, + "barber": { + "keywords": ["hair", "salon", "style"], + "char": "๐Ÿ’ˆ" + }, + "syringe": { + "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"], + "char": "๐Ÿ’‰" + }, + "pill": { + "keywords": ["health", "medicine", "doctor", "pharmacy"], + "char": "๐Ÿ’Š" + }, + "microscope": { + "keywords": ["laboratory", "experiment", "zoomin", "science", "study"], + "char": "๐Ÿ”ฌ" + }, + "telescope": { + "keywords": ["stars", "space", "zoom"], + "char": "๐Ÿ”ญ" + }, + "crystal_ball": { + "keywords": ["disco", "party", "magic", "circus", "fortune_teller"], + "char": "๐Ÿ”ฎ" + }, + "wrench": { + "keywords": ["tools", "diy", "ikea", "fix", "maintainer"], + "char": "๐Ÿ”ง" + }, + "hocho": { + "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"], + "char": "๐Ÿ”ช" + }, + "nut_and_bolt": { + "keywords": ["handy", "tools", "fix"], + "char": "๐Ÿ”ฉ" + }, + "hammer": { + "keywords": ["tools", "verdict", "judge", "done", "law", "legal", "ruling", "gavel"], + "char": "๐Ÿ”จ" + }, + "bomb": { + "keywords": ["boom", "explode", "explosion"], + "char": "๐Ÿ’ฃ" + }, + "smoking": { + "keywords": ["kills", "tobacco", "cigarette"], + "char": "๐Ÿšฌ" + }, + "gun": { + "keywords": ["violence", "weapon", "pistol", "revolver"], + "char": "๐Ÿ”ซ" + }, + "bookmark": { + "keywords": ["favorite", "label", "save"], + "char": "๐Ÿ”–" + }, + "newspaper": { + "keywords": ["press", "headline"], + "char": "๐Ÿ“ฐ" + }, + "key": { + "keywords": ["lock", "door", "password"], + "char": "๐Ÿ”‘" + }, + "email": { + "keywords": ["envelope", "letter", "postal", "inbox", "communication"], + "char": "โœ‰๏ธ" + }, + "envelope_with_arrow": { + "keywords": ["email", "communication"], + "char": "๐Ÿ“ฉ" + }, + "incoming_envelope": { + "keywords": ["email", "inbox"], + "char": "๐Ÿ“จ" + }, + "e-mail": { + "keywords": ["communication", "inbox", "email"], + "char": "๐Ÿ“ง" + }, + "inbox_tray": { + "keywords": ["email", "documents"], + "char": "๐Ÿ“ฅ" + }, + "outbox_tray": { + "keywords": ["inbox", "email"], + "char": "๐Ÿ“ค" + }, + "package": { + "keywords": ["mail", "gift", "cardboard", "box", "moving"], + "char": "๐Ÿ“ฆ" + }, + "postal_horn": { + "keywords": ["instrument", "music"], + "char": "๐Ÿ“ฏ" + }, + "postbox": { + "keywords": ["email", "letter", "envelope"], + "char": "๐Ÿ“ฎ" + }, + "mailbox_closed": { + "keywords": ["email", "communication", "inbox"], + "char": "๐Ÿ“ช" + }, + "mailbox": { + "keywords": ["email", "inbox", "communication"], + "char": "๐Ÿ“ซ" + }, + "mailbox_with_mail": { + "keywords": ["email", "inbox", "communication"], + "char": "๐Ÿ“ฌ" + }, + "mailbox_with_no_mail": { + "keywords": ["email", "inbox"], + "char": "๐Ÿ“ญ" + }, + "page_facing_up": { + "keywords": ["documents", "office", "paper", "information"], + "char": "๐Ÿ“„" + }, + "page_with_curl": { + "keywords": ["documents", "office", "paper"], + "char": "๐Ÿ“ƒ" + }, + "bookmark_tabs": { + "keywords": ["favorite", "save", "order", "tidy"], + "char": "๐Ÿ“‘" + }, + "chart_with_upwards_trend": { + "keywords": ["graph", "presenetation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"], + "char": "๐Ÿ“ˆ" + }, + "chart_with_downwards_trend": { + "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"], + "char": "๐Ÿ“‰" + }, + "bar_chart": { + "keywords": ["graph", "presentation", "stats"], + "char": "๐Ÿ“Š" + }, + "date": { + "keywords": ["calendar", "schedule"], + "char": "๐Ÿ“…" + }, + "calendar": { + "keywords": ["schedule", "date", "planning"], + "char": "๐Ÿ“†" + }, + "low_brightness": { + "keywords": ["sun", "afternoon", "warm", "summer"], + "char": "๐Ÿ”…" + }, + "high_brightness": { + "keywords": ["sun", "light"], + "char": "๐Ÿ”†" + }, + "scroll": { + "keywords": ["documents", "ancient", "history", "paper"], + "char": "๐Ÿ“œ" + }, + "clipboard": { + "keywords": ["stationery", "documents"], + "char": "๐Ÿ“‹" + }, + "book": { + "keywords": ["open_book", "read", "library", "knowledge", "literature", "learn", "study"], + "char": "๐Ÿ“–" + }, + "notebook": { + "keywords": ["stationery", "record", "notes", "paper", "study"], + "char": "๐Ÿ““" + }, + "notebook_with_decorative_cover": { + "keywords": ["classroom", "notes", "record", "paper", "study"], + "char": "๐Ÿ“”" + }, + "ledger": { + "keywords": ["notes", "paper"], + "char": "๐Ÿ“’" + }, + "closed_book": { + "keywords": ["read", "library", "knowledge", "textbook", "learn"], + "char": "๐Ÿ“•" + }, + "green_book": { + "keywords": ["read", "library", "knowledge", "study"], + "char": "๐Ÿ“—" + }, + "blue_book": { + "keywords": ["read", "library", "knowledge", "learn", "study"], + "char": "๐Ÿ“˜" + }, + "orange_book": { + "keywords": ["read", "library", "knowledge", "textbook", "study"], + "char": "๐Ÿ“™" + }, + "books": { + "keywords": ["literature", "library", "study"], + "char": "๐Ÿ“š" + }, + "card_index": { + "keywords": ["business", "stationery"], + "char": "๐Ÿ“‡" + }, + "link": { + "keywords": ["rings", "url"], + "char": "๐Ÿ”—" + }, + "paperclip": { + "keywords": ["documents", "stationery"], + "char": "๐Ÿ“Ž" + }, + "pushpin": { + "keywords": ["stationery", "mark", "here"], + "char": "๐Ÿ“Œ" + }, + "scissors": { + "keywords": ["stationery", "cut"], + "char": "โœ‚๏ธ" + }, + "triangular_ruler": { + "keywords": ["stationery", "math", "architect", "sketch"], + "char": "๐Ÿ“" + }, + "round_pushpin": { + "keywords": ["stationery", "location", "map", "here"], + "char": "๐Ÿ“" + }, + "straight_ruler": { + "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"], + "char": "๐Ÿ“" + }, + "triangular_flag_on_post": { + "keywords": ["mark", "milestone", "place"], + "char": "๐Ÿšฉ" + }, + "file_folder": { + "keywords": ["documents", "business", "office"], + "char": "๐Ÿ“" + }, + "open_file_folder": { + "keywords": ["documents", "load"], + "char": "๐Ÿ“‚" + }, + "black_nib": { + "keywords": ["pen", "stationery", "writing", "write"], + "char": "โœ’๏ธ" + }, + "pencil2": { + "keywords": ["stationery", "write", "paper", "writing", "school", "study"], + "char": "โœ๏ธ" + }, + "memo": { + "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study"], + "char": "๐Ÿ“" + }, + "lock_with_ink_pen": { + "keywords": ["security", "secret"], + "char": "๐Ÿ”" + }, + "closed_lock_with_key": { + "keywords": ["security", "privacy"], + "char": "๐Ÿ”" + }, + "lock": { + "keywords": ["security", "password", "padlock"], + "char": "๐Ÿ”’" + }, + "unlock": { + "keywords": ["privacy", "security"], + "char": "๐Ÿ”“" + }, + "mega": { + "keywords": ["sound", "speaker", "volume"], + "char": "๐Ÿ“ฃ" + }, + "loudspeaker": { + "keywords": ["volume", "sound"], + "char": "๐Ÿ“ข" + }, + "speaker": { + "keywords": ["sound", "volume", "silence", "broadcast"], + "char": "๐Ÿ”ˆ" + }, + "sound": { + "keywords": ["volume", "speaker", "broadcast"], + "char": "๐Ÿ”‰" + }, + "loud_sound": { + "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"], + "char": "๐Ÿ”Š" + }, + "mute": { + "keywords": ["sound", "volume", "silence", "quiet"], + "char": "๐Ÿ”‡" + }, + "zzz": { + "keywords": ["sleepy", "tired"], + "char": "๐Ÿ’ค" + }, + "bell": { + "keywords": ["sound", "notification", "christmas", "xmas", "chime"], + "char": "๐Ÿ””" + }, + "no_bell": { + "keywords": ["sound", "volume", "mute", "quiet", "silent"], + "char": "๐Ÿ”•" + }, + "thought_balloon": { + "keywords": ["bubble", "cloud", "speech", "thinking"], + "char": "๐Ÿ’ญ" + }, + "speech_balloon": { + "keywords": ["bubble", "words", "message", "talk", "chatting"], + "char": "๐Ÿ’ฌ" + }, + "children_crossing": { + "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"], + "char": "๐Ÿšธ" + }, + "mag": { + "keywords": ["search", "zoom"], + "char": "๐Ÿ”" + }, + "mag_right": { + "keywords": ["search", "zoom"], + "char": "๐Ÿ”Ž" + }, + "no_entry_sign": { + "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"], + "char": "๐Ÿšซ" + }, + "no_entry": { + "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"], + "char": "โ›”" + }, + "name_badge": { + "keywords": ["fire", "forbid"], + "char": "๐Ÿ“›" + }, + "no_pedestrians": { + "keywords": ["rules", "crossing", "walking", "circle"], + "char": "๐Ÿšท" + }, + "do_not_litter": { + "keywords": ["trash", "bin", "garbage", "circle"], + "char": "๐Ÿšฏ" + }, + "no_bicycles": { + "keywords": ["cyclist", "prohibited", "circle"], + "char": "๐Ÿšณ" + }, + "non-potable_water": { + "keywords": ["drink", "faucet", "tap", "circle"], + "char": "๐Ÿšฑ" + }, + "no_mobile_phones": { + "keywords": ["iphone", "mute", "circle"], + "char": "๐Ÿ“ต" + }, + "underage": { + "keywords": ["18", "drink", "pub", "night", "minor", "circle"], + "char": "๐Ÿ”ž" + }, + "accept": { + "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"], + "char": "๐Ÿ‰‘" + }, + "ideograph_advantage": { + "keywords": ["chinese", "kanji", "obtain", "get", "circle"], + "char": "๐Ÿ‰" + }, + "white_flower": { + "keywords": ["japanese", "spring"], + "char": "๐Ÿ’ฎ" + }, + "secret": { + "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"], + "char": "ใŠ™๏ธ" + }, + "congratulations": { + "keywords": ["chinese", "kanji", "japanese", "red-circle"], + "char": "ใŠ—๏ธ" + }, + "u5408": { + "keywords": ["japanese", "chinese", "join", "kanji", "red-square"], + "char": "๐Ÿˆด" + }, + "u6e80": { + "keywords": ["full", "chinese", "japanese", "red-square", "kanji"], + "char": "๐Ÿˆต" + }, + "u7981": { + "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square"], + "char": "๐Ÿˆฒ" + }, + "u6709": { + "keywords": ["orange-square", "chinese", "have", "kanji"], + "char": "๐Ÿˆถ" + }, + "u7121": { + "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square"], + "char": "๐Ÿˆš" + }, + "u7533": { + "keywords": ["chinese", "japanese", "kanji", "orange-square"], + "char": "๐Ÿˆธ" + }, + "u55b6": { + "keywords": ["japanese", "opening hours", "orange-square"], + "char": "๐Ÿˆบ" + }, + "u6708": { + "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji"], + "char": "๐Ÿˆท๏ธ" + }, + "u5272": { + "keywords": ["cut", "divide", "chinese", "kanji", "pink-square"], + "char": "๐Ÿˆน" + }, + "u7a7a": { + "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square"], + "char": "๐Ÿˆณ" + }, + "sa": { + "keywords": ["japanese", "blue-square", "katakana"], + "char": "๐Ÿˆ‚๏ธ" + }, + "koko": { + "keywords": ["blue-square", "here", "katakana", "japanese", "destination"], + "char": "๐Ÿˆ" + }, + "u6307": { + "keywords": ["chinese", "point", "green-square", "kanji"], + "char": "๐Ÿˆฏ" + }, + "chart": { + "keywords": ["green-square", "graph", "presentation", "stats"], + "char": "๐Ÿ’น" + }, + "sparkle": { + "keywords": ["stars", "green-square", "awesome", "good", "fireworks"], + "char": "โ‡๏ธ" + }, + "eight_spoked_asterisk": { + "keywords": ["star", "sparkle", "green-square"], + "char": "โœณ๏ธ" + }, + "negative_squared_cross_mark": { + "keywords": ["x", "green-square", "no", "deny"], + "char": "โŽ" + }, + "white_check_mark": { + "keywords": ["green-square", "ok", "agree", "vote", "election"], + "char": "โœ…" + }, + "eight_pointed_black_star": { + "keywords": ["orange-square", "shape", "polygon"], + "char": "โœด๏ธ" + }, + "vibration_mode": { + "keywords": ["orange-square", "phone"], + "char": "๐Ÿ“ณ" + }, + "mobile_phone_off": { + "keywords": ["mute", "orange-square", "silence", "quiet"], + "char": "๐Ÿ“ด" + }, + "vs": { + "keywords": ["words", "orange-square"], + "char": "๐Ÿ†š" + }, + "a": { + "keywords": ["red-square", "alphabet", "letter"], + "char": "๐Ÿ…ฐ๏ธ" + }, + "b": { + "keywords": ["red-square", "alphabet", "letter"], + "char": "๐Ÿ…ฑ๏ธ" + }, + "ab": { + "keywords": ["red-square", "alphabet"], + "char": "๐Ÿ†Ž" + }, + "cl": { + "keywords": ["alphabet", "words", "red-square"], + "char": "๐Ÿ†‘" + }, + "o2": { + "keywords": ["alphabet", "red-square", "letter"], + "char": "๐Ÿ…พ๏ธ" + }, + "sos": { + "keywords": ["help", "red-square", "words", "emergency", "911"], + "char": "๐Ÿ†˜" + }, + "id": { + "keywords": ["purple-square", "words"], + "char": "๐Ÿ†”" + }, + "parking": { + "keywords": ["cars", "blue-square", "alphabet", "letter"], + "char": "๐Ÿ…ฟ๏ธ" + }, + "wc": { + "keywords": ["toilet", "restroom", "blue-square"], + "char": "๐Ÿšพ" + }, + "cool": { + "keywords": ["words", "blue-square"], + "char": "๐Ÿ†’" + }, + "free": { + "keywords": ["blue-square", "words"], + "char": "๐Ÿ†“" + }, + "new": { + "keywords": ["blue-square", "words", "start"], + "char": "๐Ÿ†•" + }, + "ng": { + "keywords": ["blue-square", "words", "shape", "icon"], + "char": "๐Ÿ†–" + }, + "ok": { + "keywords": ["good", "agree", "yes", "blue-square"], + "char": "๐Ÿ†—" + }, + "up": { + "keywords": ["blue-square", "above", "high"], + "char": "๐Ÿ†™" + }, + "atm": { + "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"], + "char": "๐Ÿง" + }, + "aries": { + "keywords": ["sign", "purple-square", "zodiac", "astrology"], + "char": "โ™ˆ" + }, + "taurus": { + "keywords": ["purple-square", "sign", "zodiac", "astrology"], + "char": "โ™‰" + }, + "gemini": { + "keywords": ["sign", "zodiac", "purple-square", "astrology"], + "char": "โ™Š" + }, + "cancer": { + "keywords": ["sign", "zodiac", "purple-square", "astrology"], + "char": "โ™‹" + }, + "leo": { + "keywords": ["sign", "purple-square", "zodiac", "astrology"], + "char": "โ™Œ" + }, + "virgo": { + "keywords": ["sign", "zodiac", "purple-square", "astrology"], + "char": "โ™" + }, + "libra": { + "keywords": ["sign", "purple-square", "zodiac", "astrology"], + "char": "โ™Ž" + }, + "scorpius": { + "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"], + "char": "โ™" + }, + "sagittarius": { + "keywords": ["sign", "zodiac", "purple-square", "astrology"], + "char": "โ™" + }, + "capricorn": { + "keywords": ["sign", "zodiac", "purple-square", "astrology"], + "char": "โ™‘" + }, + "aquarius": { + "keywords": ["sign", "purple-square", "zodiac", "astrology"], + "char": "โ™’" + }, + "pisces": { + "keywords": ["purple-square", "sign", "zodiac", "astrology"], + "char": "โ™“" + }, + "restroom": { + "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"], + "char": "๐Ÿšป" + }, + "mens": { + "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"], + "char": "๐Ÿšน" + }, + "womens": { + "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"], + "char": "๐Ÿšบ" + }, + "baby_symbol": { + "keywords": ["orange-square", "child"], + "char": "๐Ÿšผ" + }, + "wheelchair": { + "keywords": ["blue-square", "disabled"], + "char": "โ™ฟ" + }, + "potable_water": { + "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"], + "char": "๐Ÿšฐ" + }, + "no_smoking": { + "keywords": ["cigarette", "blue-square", "smell", "smoke"], + "char": "๐Ÿšญ" + }, + "put_litter_in_its_place": { + "keywords": ["blue-square", "sign", "human", "info"], + "char": "๐Ÿšฎ" + }, + "arrow_forward": { + "keywords": ["blue-square", "right", "direction"], + "char": "โ–ถ๏ธ" + }, + "arrow_backward": { + "keywords": ["blue-square", "left", "direction"], + "char": "โ—€๏ธ" + }, + "arrow_up_small": { + "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"], + "char": "๐Ÿ”ผ" + }, + "arrow_down_small": { + "keywords": ["blue-square", "direction", "bottom"], + "char": "๐Ÿ”ฝ" + }, + "fast_forward": { + "keywords": ["blue-square", "play", "speed", "continue"], + "char": "โฉ" + }, + "rewind": { + "keywords": ["play", "blue-square"], + "char": "โช" + }, + "arrow_double_up": { + "keywords": ["blue-square", "direction", "top"], + "char": "โซ" + }, + "arrow_double_down": { + "keywords": ["blue-square", "direction", "bottom"], + "char": "โฌ" + }, + "arrow_right": { + "keywords": ["blue-square", "next"], + "char": "โžก๏ธ" + }, + "arrow_left": { + "keywords": ["blue-square", "previous", "back"], + "char": "โฌ…๏ธ" + }, + "arrow_up": { + "keywords": ["blue-square", "continue", "top", "direction"], + "char": "โฌ†๏ธ" + }, + "arrow_down": { + "keywords": ["blue-square", "direction", "bottom"], + "char": "โฌ‡๏ธ" + }, + "arrow_upper_right": { + "keywords": ["blue-square", "point", "direction"], + "char": "โ†—๏ธ" + }, + "arrow_lower_right": { + "keywords": ["blue-square", "direction"], + "char": "โ†˜๏ธ" + }, + "arrow_lower_left": { + "keywords": ["blue-square", "direction"], + "char": "โ†™๏ธ" + }, + "arrow_upper_left": { + "keywords": ["blue-square", "point", "direction"], + "char": "โ†–๏ธ" + }, + "arrow_up_down": { + "keywords": ["blue-square", "direction", "way"], + "char": "โ†•๏ธ" + }, + "left_right_arrow": { + "keywords": ["shape", "direction"], + "char": "โ†”๏ธ" + }, + "arrows_counterclockwise": { + "keywords": ["blue-square", "sync", "cycle"], + "char": "๐Ÿ”„" + }, + "arrow_right_hook": { + "keywords": ["blue-square", "return", "rotade", "direction"], + "char": "โ†ช๏ธ" + }, + "leftwards_arrow_with_hook": { + "keywords": ["back", "return", "blue-square", "undo"], + "char": "โ†ฉ๏ธ" + }, + "arrow_heading_up": { + "keywords": ["blue-square", "direction", "top"], + "char": "โคด๏ธ" + }, + "arrow_heading_down": { + "keywords": ["blue-square", "direction", "bottom"], + "char": "โคต๏ธ" + }, + "twisted_rightwards_arrows": { + "keywords": ["blue-square", "shuffle", "music", "random"], + "char": "๐Ÿ”€" + }, + "repeat": { + "keywords": ["loop", "record"], + "char": "๐Ÿ”" + }, + "repeat_one": { + "keywords": ["blue-square", "loop"], + "char": "๐Ÿ”‚" + }, + "hash": { + "keywords": ["symbol", "blue-square", "twitter"], + "char": "#๏ธโƒฃ" + }, + "zero": { + "keywords": ["0", "numbers", "blue-square", "null"], + "char": "0๏ธโƒฃ" + }, + "one": { + "keywords": ["blue-square", "numbers", "1"], + "char": "1๏ธโƒฃ" + }, + "two": { + "keywords": ["numbers", "2", "prime", "blue-square"], + "char": "2๏ธโƒฃ" + }, + "three": { + "keywords": ["3", "numbers", "prime", "blue-square"], + "char": "3๏ธโƒฃ" + }, + "four": { + "keywords": ["4", "numbers", "blue-square"], + "char": "4๏ธโƒฃ" + }, + "five": { + "keywords": ["5", "numbers", "blue-square", "prime"], + "char": "5๏ธโƒฃ" + }, + "six": { + "keywords": ["6", "numbers", "blue-square"], + "char": "6๏ธโƒฃ" + }, + "seven": { + "keywords": ["7", "numbers", "blue-square", "prime"], + "char": "7๏ธโƒฃ" + }, + "eight": { + "keywords": ["8", "blue-square", "numbers"], + "char": "8๏ธโƒฃ" + }, + "nine": { + "keywords": ["blue-square", "numbers", "9"], + "char": "9๏ธโƒฃ" + }, + "keycap_ten": { + "keywords": ["numbers", "10", "blue-square"], + "char": "๐Ÿ”Ÿ" + }, + "1234": { + "keywords": ["numbers", "blue-square"], + "char": "๐Ÿ”ข" + }, + "abc": { + "keywords": ["blue-square", "alphabet"], + "char": "๐Ÿ”ค" + }, + "abcd": { + "keywords": ["blue-square", "alphabet"], + "char": "๐Ÿ”ก" + }, + "capital_abcd": { + "keywords": ["alphabet", "words", "blue-square"], + "char": "๐Ÿ” " + }, + "information_source": { + "keywords": ["blue-square", "alphabet", "letter"], + "char": "โ„น๏ธ" + }, + "signal_strength": { + "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth"], + "char": "๐Ÿ“ถ" + }, + "cinema": { + "keywords": ["blue-square", "record", "film", "movie"], + "char": "๐ŸŽฆ" + }, + "symbols": { + "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"], + "char": "๐Ÿ”ฃ" + }, + "heavy_plus_sign": { + "keywords": ["math", "calculation", "addition", "more", "increase"], + "char": "โž•" + }, + "heavy_minus_sign": { + "keywords": ["math", "calculation", "subtract", "less"], + "char": "โž–" + }, + "wavy_dash": { + "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"], + "char": "ใ€ฐ๏ธ" + }, + "heavy_division_sign": { + "keywords": ["divide", "math", "calculation"], + "char": "โž—" + }, + "heavy_multiplication_x": { + "keywords": ["math", "calculation"], + "char": "โœ–๏ธ" + }, + "heavy_check_mark": { + "keywords": ["ok", "nike"], + "char": "โœ”๏ธ" + }, + "arrows_clockwise": { + "keywords": ["sync", "cycle", "round", "repeat"], + "char": "๐Ÿ”ƒ" + }, + "tm": { + "keywords": ["trademark", "brand", "law", "legal"], + "char": "โ„ข๏ธ" + }, + "copyright": { + "keywords": ["ip", "license", "circle", "law", "legal"], + "char": "ยฉ๏ธ" + }, + "registered": { + "keywords": ["alphabet", "circle"], + "char": "ยฎ๏ธ" + }, + "currency_exchange": { + "keywords": ["money", "sales", "dollar", "travel"], + "char": "๐Ÿ’ฑ" + }, + "heavy_dollar_sign": { + "keywords": ["money", "sales", "payment", "currency"], + "char": "๐Ÿ’ฒ" + }, + "curly_loop": { + "keywords": ["scribble", "draw", "shape", "squiggle"], + "char": "โžฐ" + }, + "loop": { + "keywords": ["tape", "cassette"], + "char": "โžฟ" + }, + "part_alternation_mark": { + "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"], + "char": "ใ€ฝ๏ธ" + }, + "exclamation": { + "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"], + "char": "โ—" + }, + "question": { + "keywords": ["doubt", "confused"], + "char": "โ“" + }, + "grey_exclamation": { + "keywords": ["surprise", "punctuation", "gray", "wow", "warning"], + "char": "โ•" + }, + "grey_question": { + "keywords": ["doubts", "gray", "huh"], + "char": "โ”" + }, + "bangbang": { + "keywords": ["exclamation", "surprise"], + "char": "โ€ผ๏ธ" + }, + "interrobang": { + "keywords": ["wat", "punctuation", "surprise"], + "char": "โ‰๏ธ" + }, + "x": { + "keywords": ["no", "delete", "remove"], + "char": "โŒ" + }, + "o": { + "keywords": ["circle", "round"], + "char": "โญ•" + }, + "100": { + "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"], + "char": "๐Ÿ’ฏ" + }, + "end": { + "keywords": ["words", "arrow"], + "char": "๐Ÿ”š" + }, + "back": { + "keywords": ["arrow", "words", "return"], + "char": "๐Ÿ”™" + }, + "on": { + "keywords": ["arrow", "words"], + "char": "๐Ÿ”›" + }, + "top": { + "keywords": ["words", "blue-square"], + "char": "๐Ÿ”" + }, + "soon": { + "keywords": ["arrow", "words"], + "char": "๐Ÿ”œ" + }, + "cyclone": { + "keywords": ["weather", "swirl", "blue", "cloud"], + "char": "๐ŸŒ€" + }, + "m": { + "keywords": ["alphabet", "blue-circle", "letter"], + "char": "โ“‚๏ธ" + }, + "ophiuchus": { + "keywords": ["sign", "purple-square", "constellation", "astrology"], + "char": "โ›Ž" + }, + "six_pointed_star": { + "keywords": ["purple-square", "religion", "jewish", "hexagram"], + "char": "๐Ÿ”ฏ" + }, + "beginner": { + "keywords": ["badge", "shield"], + "char": "๐Ÿ”ฐ" + }, + "trident": { + "keywords": ["weapon", "spear"], + "char": "๐Ÿ”ฑ" + }, + "warning": { + "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"], + "char": "โš ๏ธ" + }, + "hotsprings": { + "keywords": ["bath", "warm", "relax"], + "char": "โ™จ๏ธ" + }, + "recycle": { + "keywords": ["arrow", "environment", "garbage", "trash"], + "char": "โ™ป๏ธ" + }, + "anger": { + "keywords": ["angry", "mad"], + "char": "๐Ÿ’ข" + }, + "diamond_shape_with_a_dot_inside": { + "keywords": ["jewel", "blue", "gem", "crystal", "fancy"], + "char": "๐Ÿ’ " + }, + "spades": { + "keywords": ["poker", "cards", "suits", "magic"], + "char": "โ™ ๏ธ" + }, + "hearts": { + "keywords": ["poker", "cards", "magic", "suits"], + "char": "โ™ฅ๏ธ" + }, + "clubs": { + "keywords": ["poker", "cards", "magic", "suits"], + "char": "โ™ฃ๏ธ" + }, + "diamonds": { + "keywords": ["poker", "cards", "magic", "suits"], + "char": "โ™ฆ๏ธ" + }, + "ballot_box_with_check": { + "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election"], + "char": "โ˜‘๏ธ" + }, + "white_circle": { + "keywords": ["shape", "round"], + "char": "โšช" + }, + "black_circle": { + "keywords": ["shape", "button", "round"], + "char": "โšซ" + }, + "radio_button": { + "keywords": ["input", "old", "music", "circle"], + "char": "๐Ÿ”˜" + }, + "red_circle": { + "keywords": ["shape", "error", "danger"], + "char": "๐Ÿ”ด" + }, + "large_blue_circle": { + "keywords": ["shape", "icon", "button"], + "char": "๐Ÿ”ต" + }, + "small_red_triangle": { + "keywords": ["shape", "direction", "up", "top"], + "char": "๐Ÿ”บ" + }, + "small_red_triangle_down": { + "keywords": ["shape", "direction", "bottom"], + "char": "๐Ÿ”ป" + }, + "small_orange_diamond": { + "keywords": ["shape", "jewel", "gem"], + "char": "๐Ÿ”ธ" + }, + "small_blue_diamond": { + "keywords": ["shape", "jewel", "gem"], + "char": "๐Ÿ”น" + }, + "large_orange_diamond": { + "keywords": ["shape", "jewel", "gem"], + "char": "๐Ÿ”ถ" + }, + "large_blue_diamond": { + "keywords": ["shape", "jewel", "gem"], + "char": "๐Ÿ”ท" + }, + "black_small_square": { + "keywords": ["shape", "icon"], + "char": "โ–ช๏ธ" + }, + "white_small_square": { + "keywords": ["shape", "icon"], + "char": "โ–ซ๏ธ" + }, + "black_large_square": { + "keywords": ["shape", "icon", "button"], + "char": "โฌ›" + }, + "white_large_square": { + "keywords": ["shape", "icon", "stone", "button"], + "char": "โฌœ" + }, + "black_medium_square": { + "keywords": ["shape", "button", "icon"], + "char": "โ—ผ๏ธ" + }, + "white_medium_square": { + "keywords": ["shape", "stone", "icon"], + "char": "โ—ป๏ธ" + }, + "black_medium_small_square": { + "keywords": ["icon", "shape", "button"], + "char": "โ—พ" + }, + "white_medium_small_square": { + "keywords": ["shape", "stone", "icon", "button"], + "char": "โ—ฝ" + }, + "black_square_button": { + "keywords": ["shape", "input", "frame"], + "char": "๐Ÿ”ฒ" + }, + "white_square_button": { + "keywords": ["shape", "input"], + "char": "๐Ÿ”ณ" + }, + "clock1": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•" + }, + "clock130": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•œ" + }, + "clock2": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•‘" + }, + "clock230": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•" + }, + "clock3": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•’" + }, + "clock330": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ž" + }, + "clock4": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•“" + }, + "clock430": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•Ÿ" + }, + "clock5": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•”" + }, + "clock530": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ• " + }, + "clock6": { + "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"], + "char": "๐Ÿ••" + }, + "clock630": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ก" + }, + "clock7": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•–" + }, + "clock730": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ข" + }, + "clock8": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•—" + }, + "clock830": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ฃ" + }, + "clock9": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•˜" + }, + "clock930": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ค" + }, + "clock10": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•™" + }, + "clock1030": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ฅ" + }, + "clock11": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•š" + }, + "clock1130": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ฆ" + }, + "clock12": { + "keywords": ["time", "noon", "midnight", "late", "early", "schedule"], + "char": "๐Ÿ•›" + }, + "clock1230": { + "keywords": ["time", "late", "early", "schedule"], + "char": "๐Ÿ•ง" + }, + "octocat": { + "keywords": ["animal", "octopus", "github", "custom_"], + "char": null + }, + "shipit": { + "keywords": ["squirrel", "detective", "animal", "sherlock", "inspector", "custom_"], + "char": null + }, + "bowtie": { + "keywords": ["face", "formal", "fashion", "suit", "classy", "magic", "circus"], + "char": null + }, + "neckbeard": { + "keywords": ["nerdy", "face", "custom_"], + "char": null + }, + "metal": { + "keywords": ["fingers", "rocknroll", "concert", "band", "custom_"], + "char": null + }, + "fu": { + "keywords": ["fuck", "finger", "dislike", "thumbsdown", "disapprove", "no", "custom_"], + "char": null + }, + "trollface": { + "keywords": ["internet", "meme", "custom_"], + "char": null + }, + "godmode": { + "keywords": ["doom", "oldschool"], + "char": null + }, + "goberserk": { + "keywords": ["doom", "rage", "bloody", "hurt"], + "char": null + }, + "finnadie": { + "keywords": ["doom", "oldschool"], + "char": null + }, + "feelsgood": { + "keywords": ["doom", "oldschool"], + "char": null + }, + "rage1": { + "keywords": ["angry", "mad", "hate", "despise"], + "char": null + }, + "rage2": { + "keywords": ["angry", "mad", "hate", "despise"], + "char": null + }, + "rage3": { + "keywords": ["angry", "mad", "hate", "despise"], + "char": null + }, + "rage4": { + "keywords": ["angry", "mad", "hate", "despise"], + "char": null + }, + "suspect": { + "keywords": ["mad", "custom_"], + "char": null + }, + "hurtrealbad": { + "keywords": ["mad", "injured", "doom", "oldschool", "custom_"], + "char": null + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Sideload.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Sideload.swift new file mode 100644 index 0000000..5cc2293 --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Sideload.swift @@ -0,0 +1,79 @@ +import Foundation + +public class Sideload { + public static let sharedInstance = Sideload() + + public var containerUrl: URL? { + guard let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "X93LWC49WV.com.ameba.esse") else { + print("Failed to get shared container URL") + return nil + } + + let appSupportDirectory = sharedContainerURL.appendingPathComponent("Library/Application Support/Scripts") + + if !FileManager.default.fileExists(atPath: appSupportDirectory.path) { + do { + try FileManager.default.createDirectory(at: appSupportDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Failed to create Application Support directory: \(error.localizedDescription)") + return nil + } + } + + return appSupportDirectory + } + + func getDocumentsDirectory() -> URL? { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory + } + + func loadFunctions() -> [TextFunction] { + guard let url = containerUrl else { return [] } + + if !FileManager.default.fileExists(atPath: url.path) { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Sideload \(error)") + } + } + var out: [TextFunction] = [] + + let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil) + while let element = enumerator?.nextObject() as? URL { + guard element.absoluteString.hasSuffix("js") else { continue } + if let function = loadScript(url: element) { + guard !out.contains(where: { $0.id == function.id }) else { continue } // filter duplicate IDs + out.append(function) + } + } + return out + } + + private func loadScript(url: URL) -> TextFunction? { + do { + let script = try String(contentsOf: url) + guard + let openComment = script.range(of: "/**"), + let closeComment = script.range(of: "**/") + else { + throw NSError() + } + + let meta = script[openComment.upperBound ..< closeComment.lowerBound] + let json = try JSONSerialization.jsonObject(with: meta.data(using: .utf8)!, options: .allowFragments) as! [String: Any] + let function = TextFunction(id: json["id"] as? String ?? UUID().uuidString, + title: json["name"] as? String ?? "Unknown External Function", + description: json["description"] as? String ?? "", + category: json["category"] as? String ?? "Custom", + author: json["author"] as? String ?? "Unknown", + function: script, fileURL: url) + return function + } catch { + print("Unable to load ", url.absoluteString) + return nil + } + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Storage.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Storage.swift new file mode 100644 index 0000000..f4843ec --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Storage.swift @@ -0,0 +1,326 @@ +import Foundation +import Observation +#if !os(macOS) + import UIKit +#endif + +public class Storage { + public static let sharedInstance = Storage() + + private var sideload = Sideload.sharedInstance + public let userDefaults = UserDefaults(suiteName: "group.ameba.co.essy")! + private let savedFunctions = "savedFunctions" + private let savedCustomFunctions = "savedCustomFunctions" + private let savedScratchpadFunctions = "savedScratchpadFunctions" + private let actionExtensionFunctions = "actionExtensionFunctions" + private let todayWidgetFunctions = "todayWidgetFunctions" + private var savedCustomFunctionsFile: URL? { + sideload.containerUrl?.appendingPathComponent(".customFunctions.esse") + } + + private var functionIDs: [String] { + didSet { + userDefaults.set(functionIDs, forKey: savedFunctions) + userDefaults.synchronize() + } + } + + private var customFunctionIDS: [String] = [] { + didSet { + userDefaults.setValue(customFunctionIDS, forKey: savedScratchpadFunctions) + userDefaults.synchronize() + } + } + + private var customFunctions: [TextFunction] = [] + private var customFunctionsStorable: [TextFunctionStorable] = [] { + didSet { + if let data = try? JSONEncoder().encode(customFunctionsStorable) { + userDefaults.setValue(data, forKey: savedCustomFunctions) + userDefaults.synchronize() + if let file = savedCustomFunctionsFile { + try? data.write(to: file) + } + } + } + } + + private var externalFunctions: [TextFunction] { + sideload.loadFunctions() + } + + private var actionExtensionFunctionsIDS: [String] = [] { + didSet { + userDefaults.setValue(actionExtensionFunctionsIDS, forKey: actionExtensionFunctions) + userDefaults.synchronize() + } + } + + private var todayWidgetFunctionsIDS: [String] = [] { + didSet { + userDefaults.setValue(todayWidgetFunctionsIDS, forKey: todayWidgetFunctions) + userDefaults.synchronize() + } + } + + #if !os(macOS) + public var fontDescriptor: UIFontDescriptor { + didSet { + let archive = try? NSKeyedArchiver.archivedData(withRootObject: fontDescriptor, requiringSecureCoding: true) + userDefaults.setValue(archive, forKey: "fontDescriptor") + userDefaults.synchronize() + } + } + #endif + public var fontSize: Double { + didSet { + userDefaults.setValue(fontSize, forKey: "fontSize") + userDefaults.synchronize() + } + } + + public var pFunctionIDs: [String] { + functionIDs + } + + public var pCustomFunctionIDs: [String] { + customFunctionIDS + } + + public var pActionFunctionIDs: [String] { + actionExtensionFunctionsIDS + } + + public var pTodayWidgetFunctionsIDS: [String] { + todayWidgetFunctionsIDS + } + + public var pExternalFunctions: [TextFunction] { + externalFunctions + } + + public var pAllFunctions: [TextFunction] { + AllFunctions + customFunctions + externalFunctions + } + + public init() { + if let savedFunctions = userDefaults.array(forKey: savedFunctions) as? [String] { + functionIDs = savedFunctions + } else { + functionIDs = [ + "co.ameba.Esse.CaseFunctions.lowerCase", + "co.ameba.Esse.CaseFunctions.capitaliseWords", + "co.ameba.Esse.CleaningFunctions.removeEmptyLines", + "co.ameba.Esse.CleaningFunctions.removeDuplicateLines", + ] + } + + if let customFunctionsIDs = userDefaults.array(forKey: savedScratchpadFunctions) as? [String] { + customFunctionIDS = customFunctionsIDs + } else { + customFunctionIDS = [ + "co.ameba.Esse.CaseFunctions.upperCase", + "co.ameba.Esse.CaseFunctions.kebabCase", + ] + } + + #if !os(macOS) + if let archive = userDefaults.object(forKey: "fontDescriptor") as? Data, let fontDescr = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIFontDescriptor.self, from: archive) { + fontDescriptor = fontDescr + } else { + fontDescriptor = UIFont.systemFont(ofSize: UIFont.systemFontSize).fontDescriptor + } + #endif + + fontSize = userDefaults.double(forKey: "fontSize") + if fontSize == 0 { + fontSize = 14 + } + + if let file = savedCustomFunctionsFile, FileManager.default.fileExists(atPath: file.path), let data = try? Data(contentsOf: file) { + let decoder = JSONDecoder() + if let savedFunctions = try? decoder.decode([TextFunctionStorable].self, from: data), !savedFunctions.isEmpty { + customFunctionsStorable = savedFunctions + } + } else if let data = userDefaults.object(forKey: savedCustomFunctions) as? Data { + let decoder = JSONDecoder() + if let savedFunctions = try? decoder.decode([TextFunctionStorable].self, from: data) { + customFunctionsStorable = savedFunctions + } + } + + for f in customFunctionsStorable { + let functions = f.functionIDs.compactMap { id in + pAllFunctions.first { $0.id == id }?.actions + }.flatMap { $0 } + let tf = TextFunction(id: f.id, title: f.title, description: f.description, actions: functions) + customFunctions.append(tf) + } + + if let savedFunctions = userDefaults.array(forKey: actionExtensionFunctions) as? [String] { + actionExtensionFunctionsIDS = savedFunctions + } else { + actionExtensionFunctionsIDS = [ + "co.ameba.Esse.OtherFunctions.upsideDown", + "co.ameba.Esse.OtherFunctions.circleLetters", + ] + } + + if let savedFunctions = userDefaults.array(forKey: todayWidgetFunctions) as? [String] { + todayWidgetFunctionsIDS = savedFunctions + } else { + todayWidgetFunctionsIDS = [ + "co.ameba.Esse.CaseFunctions.upperCase", + "co.ameba.Esse.OtherFunctions.rot13", + ] + } + } +} + +// MARK: Functions + +public extension Storage { + func add(_ functionId: String) { + guard !functionIDs.contains(functionId) else { return } + functionIDs.append(functionId) + } + + func insert(_ functionId: String, at index: Int) -> Int? { + let idx = remove(functionId) + functionIDs.insert(functionId, at: index) + return idx + } + + func remove(_ functionId: String) -> Int? { + if let idx = functionIDs.firstIndex(of: functionId) { + functionIDs.remove(at: idx) + return idx + } + return nil + } + + func replace(_ functionsIDs: [String]) { + functionIDs = functionsIDs + } +} + +// MARK: Custom Functions + +public extension Storage { + func CFadd(_ functionId: String) { + customFunctionIDS.append(functionId) + } + + func CFSet(_ functionsIDs: [String]) { + customFunctionIDS = functionsIDs + } + + func CFInsert(_ functionId: String, at index: Int) { + customFunctionIDS.insert(functionId, at: index) + } + + func CFReset() { + customFunctionIDS = [] + } + + func CFRemove(_ functionId: String) { + if let idx = customFunctionIDS.firstIndex(of: functionId) { + customFunctionIDS.remove(at: idx) + } + } + + func CFRemove(index: Int) { + if customFunctionIDS.count >= (index + 1) { + customFunctionIDS.remove(at: index) + } + } + + func CFSaveNewFuntion(title: String) { + guard customFunctionIDS.count > 0 else { return } + + let id = UUID().uuidString + let description = customFunctionIDS.compactMap { id in + pAllFunctions.first { $0.id == id } + }.compactMap(\.title).joined(separator: " โž” ") + + let functionIDs = customFunctionIDS.compactMap { id in + pAllFunctions.first { $0.id == id } + }.compactMap(\.id) + let storableFunction = TextFunctionStorable(id: id, title: title, description: description, functionIDs: functionIDs) + customFunctionsStorable.append(storableFunction) + + let textFunctionActions = customFunctionIDS.compactMap { id in + pAllFunctions.first { $0.id == id }?.actions + }.flatMap { $0 } + let tf = TextFunction(id: id, title: title, description: description, actions: textFunctionActions) + customFunctions.append(tf) + } + + func CFDeleteSavedFunction(id: String) { + if let idx = customFunctions.firstIndex(where: { $0.id == id }) { + customFunctions.remove(at: idx) + } + + if let idx = customFunctionsStorable.firstIndex(where: { $0.id == id }) { + customFunctionsStorable.remove(at: idx) + } + + if let idx = functionIDs.firstIndex(where: { $0 == id }) { + functionIDs.remove(at: idx) + } + + if let idx = actionExtensionFunctionsIDS.firstIndex(where: { $0 == id }) { + actionExtensionFunctionsIDS.remove(at: idx) + } + + if let idx = todayWidgetFunctionsIDS.firstIndex(where: { $0 == id }) { + todayWidgetFunctionsIDS.remove(at: idx) + } + } +} + +// MARK: Share Sheet Functions + +public extension Storage { + func AEAdd(_ functionId: String) { + guard !actionExtensionFunctionsIDS.contains(functionId) else { return } + actionExtensionFunctionsIDS.append(functionId) + } + + func AERemove(_ functionId: String) { + if let idx = actionExtensionFunctionsIDS.firstIndex(of: functionId) { + actionExtensionFunctionsIDS.remove(at: idx) + } + } + + func AEInsert(_ functionId: String, at index: Int) { + actionExtensionFunctionsIDS.insert(functionId, at: index) + } + + func AEReplace(_ functionsIDs: [String]) { + actionExtensionFunctionsIDS = functionsIDs + } +} + +// MARK: Today Widget Functions + +public extension Storage { + func TWAdd(_ functionId: String) { + guard !todayWidgetFunctionsIDS.contains(functionId) else { return } + todayWidgetFunctionsIDS.append(functionId) + } + + func TWRemove(_ functionId: String) { + if let idx = todayWidgetFunctionsIDS.firstIndex(of: functionId) { + todayWidgetFunctionsIDS.remove(at: idx) + } + } + + func TWInsert(_ functionId: String, at index: Int) { + todayWidgetFunctionsIDS.insert(functionId, at: index) + } + + func TWReplace(_ functionsIDs: [String]) { + todayWidgetFunctionsIDS = functionsIDs + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Utils/Array+Utils.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/Array+Utils.swift new file mode 100644 index 0000000..1e72742 --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/Array+Utils.swift @@ -0,0 +1,13 @@ +extension Array where Element: Hashable { + func removingDuplicates() -> [Element] { + var addedDict = [Element: Bool]() + + return filter { + addedDict.updateValue(true, forKey: $0) == nil + } + } + + mutating func removeDuplicates() { + self = removingDuplicates() + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Fuzzy.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Fuzzy.swift new file mode 100644 index 0000000..3de7eeb --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Fuzzy.swift @@ -0,0 +1,133 @@ +// +// Based on string_score 0.1.21 by Joshaven Potter. +// https://github.com/joshaven/string_score/ +// +// Copyright (c) 2016-present YICHI ZHANG +// https://github.com/yichizhang +// zhang-yi-chi@hotmail.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +import Foundation + +private extension String { + func charAt(_ i: Int) -> Character { + let index = index(startIndex, offsetBy: i) + return self[index] + } + + func charStrAt(_ i: Int) -> String { + String(charAt(i)) + } +} + +public extension String { + func score(word: String, fuzziness: Double? = nil) -> Double { + // If the string is equal to the word, perfect match. + if self == word { + return 1 + } + + // if it's not a perfect match and is empty return 0 + if word.isEmpty || isEmpty { + return 0 + } + + var runningScore = 0.0 + var charScore = 0.0 + var finalScore = 0.0 + + let string = self + let lString = string.lowercased() + let strLength = string.count + let lWord = word.lowercased() + let wordLength = word.count + + var idxOf: String.Index! + var startAt = lString.startIndex + var fuzzies = 1.0 + var fuzzyFactor = 0.0 + var fuzzinessIsNil = true + + // Cache fuzzyFactor for speed increase + if let fuzziness { + fuzzyFactor = 1 - fuzziness + fuzzinessIsNil = false + } + + for i in 0 ..< wordLength { + // Find next first case-insensitive match of word's i-th character. + // The search in "string" begins at "startAt". + + if let range = lString.range( + of: lWord.charStrAt(i), + options: [.caseInsensitive, .diacriticInsensitive], + range: startAt ..< lString.endIndex, + locale: nil + ) { + // start index of word's i-th character in string. + idxOf = range.lowerBound + + if startAt == idxOf { + // Consecutive letter & start-of-string Bonus + charScore = 0.7 + } else { + charScore = 0.1 + + // Acronym Bonus + // Weighing Logic: Typing the first character of an acronym is as if you + // preceded it with two perfect character matches. + if string[string.index(before: idxOf)] == " " { + charScore += 0.8 + } + } + } else { + // Character not found. + if fuzzinessIsNil { + // Fuzziness is nil. Return 0. + return 0 + } else { + fuzzies += fuzzyFactor + continue + } + } + + // Same case bonus. + if string[idxOf] == word[word.index(word.startIndex, offsetBy: i)] { + charScore += 0.1 + } + + // Update scores and startAt position for next round of indexOf + runningScore += charScore + startAt = string.index(idxOf, offsetBy: 1) + } + + // Reduce penalty for longer strings. + finalScore = 0.5 * (runningScore / Double(strLength) + runningScore / Double(wordLength)) / fuzzies + + if finalScore < 0.85, + lWord.charStrAt(0).compare(lString.charStrAt(0), options: .diacriticInsensitive) == .orderedSame + { + finalScore += 0.15 + } + + return finalScore + } +} diff --git a/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Utils.swift b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Utils.swift new file mode 100644 index 0000000..eff653d --- /dev/null +++ b/Esse/Packages/EsseCore/Sources/EsseCore/Utils/String+Utils.swift @@ -0,0 +1,101 @@ +import Foundation +import NaturalLanguage + +public extension String { + func lowercaseFirst() -> String { + guard count > 0 else { return self } + + let indexOfSecondChar = index(startIndex, offsetBy: 1) + return String(self[startIndex]).lowercased() + String(self[indexOfSecondChar...]) + } + + func capitaliseFirst() -> String { + guard count > 0 else { return self } + + let indexOfSecondChar = index(startIndex, offsetBy: 1) + return String(self[startIndex]).capitalized + String(self[indexOfSecondChar...]) + } + + func toArray() -> [String] { + components(separatedBy: "\n") + } + + func getUnits(of unitType: NSLinguisticTaggerUnit) -> [String] { + guard count > 0 else { return [] } + + var units = [String]() + let tagger = NSLinguisticTagger(tagSchemes: [.tokenType, .language, .lexicalClass, .nameType, .lemma], options: 0) + let options: NSLinguisticTagger.Options = [.omitWhitespace, .joinNames] + tagger.string = self + let range = NSRange(location: 0, length: utf16.count) + tagger.enumerateTags(in: range, unit: unitType, scheme: .tokenType, options: options) { _, tokenRange, _ in + let unit = (self as NSString).substring(with: tokenRange) + units.append(unit) + } + return units + } + + func words() -> [String] { + let range = Range(uncheckedBounds: (lower: startIndex, endIndex)) + var words = [String]() + + enumerateSubstrings(in: range, options: NSString.EnumerationOptions.byWords) { substring, _, _, _ in + if let substring { + words.append(substring) + } + } + + return words + } + + func lines() -> [String] { + guard !isEmpty else { return [] } + return components(separatedBy: .newlines) + } + + func inserting(separator: String, every n: Int) -> String { + var result = "" + let characters = Array(self) + for item in stride(from: 0, to: characters.count, by: n) { + result += String(characters[item ..< min(item + n, characters.count)]) + if item + n < characters.count { + result += separator + } + } + return result + } + + /// word - lemma + func lemmas() -> [String: String] { + let tagger = NSLinguisticTagger(tagSchemes: [.lemma], options: 0) + let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace] + + var result: [String: String] = [:] + + tagger.string = self + let range = NSRange(location: 0, length: utf16.count) + + tagger.enumerateTags(in: range, unit: .word, scheme: .lemma, options: options) { tag, tokenRange, _ in + let word = (self as NSString).substring(with: tokenRange) + if let lemma = tag?.rawValue { + print("Lemma: \(lemma)") + result[word] = lemma + } + } + + return result + } + + func isValidEmail() -> Bool { + // here, `try!` will always succeed because the pattern is valid + let regex = try! NSRegularExpression(pattern: "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", options: .caseInsensitive) + return regex.firstMatch(in: self, options: [], range: NSRange(location: 0, length: count)) != nil + } + + subscript(_ range: NSRange) -> String { + let start = index(startIndex, offsetBy: range.lowerBound) + let end = index(startIndex, offsetBy: range.upperBound) + let subString = self[start ..< end] + return String(subString) + } +} diff --git a/Esse/Packages/EsseCore/Tests/EsseCoreTests/EsseCoreTests.swift b/Esse/Packages/EsseCore/Tests/EsseCoreTests/EsseCoreTests.swift new file mode 100644 index 0000000..6e37cae --- /dev/null +++ b/Esse/Packages/EsseCore/Tests/EsseCoreTests/EsseCoreTests.swift @@ -0,0 +1,12 @@ +@testable import EsseCore +import XCTest + +final class EsseCoreTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +}