From a9fa0a638fc0438c5a674be5850b32c665c1204b Mon Sep 17 00:00:00 2001 From: Gnohz Gniy <0x00eeee@gmail.com> Date: Tue, 7 Feb 2023 18:39:32 +0800 Subject: [PATCH] Initial source code release --- .github/workflows/build-and-test.yml | 47 + .gitignore | 6 + .periphery.yml | 16 + .swiftlint.yml | 560 +++ Build.xcconfig | 16 + CoreEditor/.eslintrc.js | 104 + CoreEditor/.gitignore | 2 + CoreEditor/index.css | 39 + CoreEditor/index.html | 15 + CoreEditor/index.ts | 73 + CoreEditor/jest.config.js | 7 + CoreEditor/package.json | 45 + CoreEditor/src/@codegen/config.json | 46 + CoreEditor/src/@codegen/swift-config.mustache | 29 + .../src/@codegen/swift-named-type.mustache | 38 + .../src/@codegen/swift-native-module.mustache | 102 + .../src/@codegen/swift-shared-types.mustache | 14 + .../src/@codegen/swift-web-module.mustache | 56 + CoreEditor/src/@light/index.html | 13 + CoreEditor/src/@light/index.ts | 48 + CoreEditor/src/@light/vite.config.ts | 10 + CoreEditor/src/@test/editor.ts | 41 + CoreEditor/src/@test/mock.ts | 76 + CoreEditor/src/@types/JSRect.ts | 9 + CoreEditor/src/@types/global.d.ts | 44 + .../src/@vendor/joplin/markdownMathParser.ts | 131 + .../src/@vendor/lang-markdown/README.md | 40 + .../src/@vendor/lang-markdown/commands.ts | 224 + CoreEditor/src/@vendor/lang-markdown/index.ts | 58 + .../src/@vendor/lang-markdown/markdown.ts | 84 + .../src/@vendor/language-data/README.md | 22 + CoreEditor/src/@vendor/language-data/index.ts | 406 ++ CoreEditor/src/bridge/native/core.ts | 13 + CoreEditor/src/bridge/native/preview.ts | 12 + CoreEditor/src/bridge/nativeModule.ts | 79 + CoreEditor/src/bridge/web/config.ts | 93 + CoreEditor/src/bridge/web/core.ts | 32 + CoreEditor/src/bridge/web/format.ts | 110 + CoreEditor/src/bridge/web/grammarly.ts | 27 + CoreEditor/src/bridge/web/history.ts | 32 + CoreEditor/src/bridge/web/lineEndings.ts | 22 + CoreEditor/src/bridge/web/search.ts | 62 + CoreEditor/src/bridge/web/selection.ts | 27 + CoreEditor/src/bridge/web/textChecker.ts | 17 + CoreEditor/src/bridge/web/toc.ts | 22 + CoreEditor/src/bridge/webModule.ts | 8 + CoreEditor/src/common/store.ts | 8 + CoreEditor/src/common/utils.ts | 2 + CoreEditor/src/config.ts | 53 + CoreEditor/src/core.ts | 67 + CoreEditor/src/dom/events/index.ts | 32 + CoreEditor/src/dom/events/isMetaKey.ts | 3 + CoreEditor/src/dom/views/index.ts | 40 + CoreEditor/src/dom/views/types.ts | 8 + CoreEditor/src/extensions.ts | 110 + CoreEditor/src/modules/commands/index.ts | 101 + .../modules/commands/insertBlockWithMarks.ts | 9 + .../src/modules/commands/removeListMarkers.ts | 6 + .../src/modules/commands/replaceSelections.ts | 15 + .../modules/commands/toggleBlockWithMarks.ts | 51 + .../modules/commands/toggleLineLeadingMark.ts | 63 + .../src/modules/commands/toggleListStyle.ts | 102 + CoreEditor/src/modules/commands/types.ts | 9 + CoreEditor/src/modules/config/index.ts | 79 + CoreEditor/src/modules/grammarly/index.css | 5 + CoreEditor/src/modules/grammarly/index.ts | 92 + CoreEditor/src/modules/history/index.ts | 43 + CoreEditor/src/modules/indentation/index.ts | 26 + CoreEditor/src/modules/indentation/types.ts | 5 + CoreEditor/src/modules/input/index.ts | 69 + CoreEditor/src/modules/input/wrapBlock.ts | 25 + CoreEditor/src/modules/lineEndings/index.ts | 70 + CoreEditor/src/modules/lineEndings/types.ts | 18 + CoreEditor/src/modules/localization/index.ts | 16 + CoreEditor/src/modules/preview/index.css | 4 + CoreEditor/src/modules/preview/index.ts | 43 + CoreEditor/src/modules/search/index.ts | 87 + CoreEditor/src/modules/search/options.ts | 8 + .../src/modules/search/rangesFromQuery.ts | 13 + .../src/modules/search/searchOccurrences.ts | 15 + CoreEditor/src/modules/selection/index.ts | 118 + .../modules/selection/searchMatchPosition.ts | 11 + .../modules/selection/selectWholeLineAt.ts | 19 + .../src/modules/selection/selectWithRanges.ts | 8 + .../modules/selection/selectedLineColumn.ts | 18 + .../src/modules/selection/selectedRanges.ts | 3 + CoreEditor/src/modules/selection/types.ts | 5 + CoreEditor/src/modules/snippets/index.ts | 19 + .../src/modules/snippets/insertSnippet.ts | 14 + CoreEditor/src/modules/textChecker/index.ts | 19 + CoreEditor/src/modules/textChecker/options.ts | 6 + CoreEditor/src/modules/toc/index.ts | 52 + CoreEditor/src/modules/toc/types.ts | 6 + CoreEditor/src/styling/builder.ts | 210 + CoreEditor/src/styling/config.ts | 153 + CoreEditor/src/styling/helper.ts | 25 + CoreEditor/src/styling/markdown.ts | 41 + CoreEditor/src/styling/matchers/lexer.ts | 63 + CoreEditor/src/styling/matchers/regex.ts | 33 + CoreEditor/src/styling/matchers/stateful.ts | 32 + CoreEditor/src/styling/nodes/code.ts | 63 + CoreEditor/src/styling/nodes/gutter.ts | 8 + CoreEditor/src/styling/nodes/heading.ts | 11 + CoreEditor/src/styling/nodes/invisible.ts | 65 + CoreEditor/src/styling/nodes/link.ts | 42 + CoreEditor/src/styling/nodes/table.ts | 27 + CoreEditor/src/styling/themes/cobalt.ts | 54 + CoreEditor/src/styling/themes/colors.ts | 23 + CoreEditor/src/styling/themes/dracula.ts | 52 + CoreEditor/src/styling/themes/github-dark.ts | 57 + CoreEditor/src/styling/themes/github-light.ts | 57 + CoreEditor/src/styling/themes/index.ts | 31 + CoreEditor/src/styling/themes/minimal-dark.ts | 48 + .../src/styling/themes/minimal-light.ts | 48 + .../styling/themes/winter-is-coming-dark.ts | 58 + .../styling/themes/winter-is-coming-light.ts | 59 + CoreEditor/src/styling/themes/xcode-dark.ts | 56 + CoreEditor/src/styling/themes/xcode-light.ts | 56 + CoreEditor/src/styling/types.ts | 30 + CoreEditor/test/commands.test.ts | 108 + CoreEditor/test/history.test.ts | 24 + CoreEditor/test/input.test.ts | 16 + CoreEditor/test/lezer.test.ts | 66 + CoreEditor/test/lineEndings.test.ts | 36 + CoreEditor/test/search.test.ts | 30 + CoreEditor/test/styling.test.ts | 37 + CoreEditor/test/toc.test.ts | 15 + CoreEditor/tsconfig.json | 18 + CoreEditor/vite.config.ts | 6 + CoreEditor/yarn.lock | 3810 +++++++++++++++++ MarkEdit.xcodeproj/project.pbxproj | 1061 +++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/MarkEditMac (zh-Hans).xcscheme | 90 + .../xcschemes/MarkEditMac (zh-Hant).xcscheme | 90 + .../xcschemes/MarkEditMac.xcscheme | 89 + .../xcschemes/PreviewExtension.xcscheme | 108 + MarkEditCore/.gitignore | 9 + .../xcschemes/MarkEditCore.xcscheme | 67 + MarkEditCore/Package.swift | 42 + MarkEditCore/README.md | 7 + MarkEditCore/Sources/EditorConfig.swift | 62 + MarkEditCore/Sources/EditorLocalizable.swift | 41 + .../Sources/Extensions/Data+Extension.swift | 41 + .../Extensions/EditorConfig+Extension.swift | 24 + .../Extensions/Encodable+Extension.swift | 13 + .../Sources/Extensions/String+Extension.swift | 30 + MarkEditCore/Tests/EncodingTests.swift | 41 + MarkEditCore/Tests/Files/sample-gb18030.md | 1 + .../Tests/Files/sample-japanese-euc.md | 1 + MarkEditCore/Tests/Files/sample-korean-euc.md | 1 + MarkEditCore/Tests/Files/sample-utf8.md | 1 + MarkEditKit/.gitignore | 9 + .../xcschemes/MarkEditKit.xcscheme | 67 + MarkEditKit/Package.swift | 32 + MarkEditKit/README.md | 7 + .../Native/Generated/NativeModuleCore.swift | 81 + .../Generated/NativeModulePreview.swift | 74 + .../Native/Modules/EditorModuleCore.swift | 33 + .../Native/Modules/EditorModulePreview.swift | 23 + .../Sources/Bridge/Native/NativeModules.swift | 49 + .../Web/Generated/WebBridgeConfig.swift | 180 + .../Bridge/Web/Generated/WebBridgeCore.swift | 54 + .../Web/Generated/WebBridgeFormat.swift | 130 + .../Web/Generated/WebBridgeGrammarly.swift | 48 + .../Web/Generated/WebBridgeHistory.swift | 42 + .../Web/Generated/WebBridgeLineEndings.swift | 49 + .../Web/Generated/WebBridgeSearch.swift | 92 + .../Web/Generated/WebBridgeSelection.swift | 42 + .../Generated/WebBridgeTableOfContents.swift | 52 + .../Web/Generated/WebBridgeTextChecker.swift | 44 + .../Sources/Bridge/Web/WebModuleBridge.swift | 36 + MarkEditKit/Sources/EditorLogger.swift | 29 + .../Sources/EditorMessageHandler.swift | 115 + MarkEditKit/Sources/EditorTextEncoding.swift | 95 + .../Extensions/DispatchQueue+Extension.swift | 21 + .../Sources/Extensions/JSRect+Extension.swift | 13 + .../Extensions/LineEndings+Extension.swift | 20 + .../Sources/Extensions/URL+Extension.swift | 17 + .../Extensions/UserDefaults+Extension.swift | 38 + .../Extensions/WKWebView+Extension.swift | 75 + .../WKWebViewConfiguration+Extension.swift | 31 + MarkEditMac/Base.lproj/Main.storyboard | 808 ++++ MarkEditMac/Info.entitlements | 14 + MarkEditMac/Info.plist | 94 + MarkEditMac/Modules/.gitignore | 9 + .../xcschemes/AppKitControls.xcscheme | 67 + .../xcschemes/AppKitExtensions.xcscheme | 67 + .../xcschemes/FontPicker.xcscheme | 67 + MarkEditMac/Modules/Package.swift | 104 + MarkEditMac/Modules/README.md | 7 + .../AppKitControls/BackgroundTheming.swift | 22 + .../Buttons/IconOnlyButton.swift | 28 + .../Buttons/NonBezelButton.swift | 35 + .../Buttons/TitleOnlyButton.swift | 28 + .../Sources/AppKitControls/DividerView.swift | 34 + .../AppKitControls/FocusTrackingView.swift | 20 + .../GotoLine/GotoLineView.swift | 104 + .../GotoLine/GotoLineWindow.swift | 60 + .../Sources/AppKitControls/LabelView.swift | 21 + .../AppKitControls/LabeledSearchField.swift | 54 + .../AppKitControls/RoundedButtonGroup.swift | 80 + .../Foundation/NSApplication+Extension.swift | 35 + .../Foundation/NSDataDetector+Extension.swift | 15 + .../Foundation/NSDocument+Extension.swift | 13 + .../Foundation/NSPasteboard+Extension.swift | 29 + .../Foundation/NSWorkspace+Extension.swift | 29 + .../UI/NSAnimationContext+Extension.swift | 16 + .../UI/NSAppearance+Extension.swift | 37 + .../UI/NSColor+Extension.swift | 50 + .../UI/NSControl+Extension.swift | 43 + .../UI/NSFont+Extension.swift | 33 + .../UI/NSImage+Extension.swift | 39 + .../UI/NSMenu+Extension.swift | 36 + .../UI/NSMenuItem+Extension.swift | 27 + .../UI/NSSearchField+Extension.swift | 23 + .../UI/NSTextField+Extension.swift | 17 + .../UI/NSView+Extension.swift | 92 + .../UI/NSViewController+Extension.swift | 13 + .../UI/NSWindow+Extension.swift | 59 + .../Extensions/NSNotification+Extension.swift | 17 + .../Sources/FontPicker/FontPicker.swift | 107 + .../FontPicker/FontPickerConfiguration.swift | 50 + .../FontPicker/FontPickerHandlers.swift | 17 + .../Sources/FontPicker/FontStyle.swift | 49 + .../Internal/FontManagerDelegate.swift | 27 + .../Modules/Sources/Previewer/Previewer.swift | 131 + .../Sources/Previewer/Resources/katex.html | 28 + .../Sources/Previewer/Resources/mermaid.html | 32 + .../Sources/Previewer/Resources/table.html | 69 + .../Modules/Sources/Proofing/Grammarly.swift | 61 + .../Extensions/View+Extension.swift | 45 + .../Sources/SettingsUI/SettingsForm.swift | 41 + .../SettingsRootViewController.swift | 77 + .../SettingsTabViewController.swift | 45 + .../Modules/Tests/DataDetectorTests.swift | 15 + .../Modules/Tests/PasteboardTests.swift | 15 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 68 + .../AppIcon.appiconset/icon_128x128.png | Bin 0 -> 19581 bytes .../AppIcon.appiconset/icon_128x128@2x.png | Bin 0 -> 61190 bytes .../AppIcon.appiconset/icon_16x16.png | Bin 0 -> 899 bytes .../AppIcon.appiconset/icon_16x16@2x.png | Bin 0 -> 2282 bytes .../AppIcon.appiconset/icon_256x256.png | Bin 0 -> 10444 bytes .../AppIcon.appiconset/icon_256x256@2x.png | Bin 0 -> 26253 bytes .../AppIcon.appiconset/icon_32x32.png | Bin 0 -> 2334 bytes .../AppIcon.appiconset/icon_32x32@2x.png | Bin 0 -> 6202 bytes .../AppIcon.appiconset/icon_512x512.png | Bin 0 -> 25301 bytes .../AppIcon.appiconset/icon_512x512@2x.png | Bin 0 -> 81043 bytes .../Resources/Assets.xcassets/Contents.json | 6 + .../Resources/en.lproj/Localizable.strings | Bin 0 -> 4 bytes .../zh-Hans.lproj/Localizable.strings | 291 ++ .../zh-Hant.lproj/Localizable.strings | 291 ++ .../EditorViewController+Config.swift | 72 + .../EditorViewController+Delegate.swift | 121 + .../EditorViewController+Encoding.swift | 24 + .../EditorViewController+GotoLine.swift | 30 + .../EditorViewController+HyperLink.swift | 30 + .../EditorViewController+LineEndings.swift | 24 + .../EditorViewController+Menu.swift | 284 ++ .../EditorViewController+Pandoc.swift | 33 + .../EditorViewController+Preview.swift | 19 + .../EditorViewController+TextFinder.swift | 145 + .../EditorViewController+Toolbar.swift | 227 + .../Controllers/EditorViewController+UI.swift | 122 + .../Controllers/EditorViewController.swift | 162 + MarkEditMac/Sources/Editor/EditorWindow.swift | 57 + .../Editor/EditorWindowController.swift | 28 + .../Editor/Models/EditorDocument.swift | 186 + .../Editor/Models/EditorReusePool.swift | 50 + .../Editor/Models/EditorToolbarItems.swift | 155 + .../Editor/Views/EditorPanelView.swift | 24 + .../Editor/Views/EditorStatusView.swift | 77 + .../Sources/Editor/Views/EditorWebView.swift | 63 + .../Extensions/NSApplication+Extension.swift | 20 + MarkEditMac/Sources/Main/AppPreferences.swift | 301 ++ MarkEditMac/Sources/Main/AppResources.swift | 178 + MarkEditMac/Sources/Main/AppTheme.swift | 168 + .../Application/AppDelegate+Document.swift | 38 + .../Main/Application/AppDelegate+Menu.swift | 109 + .../Main/Application/AppDelegate.swift | 79 + .../Panels/Find/EditorFindButtons.swift | 22 + .../Find/EditorFindPanel+Delegate.swift | 31 + .../Panels/Find/EditorFindPanel+Menu.swift | 84 + .../Panels/Find/EditorFindPanel+UI.swift | 62 + .../Sources/Panels/Find/EditorFindPanel.swift | 67 + .../Panels/Replace/EditorReplaceButtons.swift | 21 + .../Panels/Replace/EditorReplacePanel.swift | 99 + .../Sources/Settings/EditorSettingsView.swift | 181 + .../Settings/GeneralSettingsView.swift | 69 + .../Sources/Settings/SettingTabs.swift | 22 + .../Sources/Settings/WindowSettingsView.swift | 49 + MarkEditMac/zh-Hans.lproj/Main.strings | 429 ++ MarkEditMac/zh-Hant.lproj/Main.strings | 429 ++ MarkEditMacTests/MarkEditMacTests.swift | 12 + .../contents.xcworkspacedata | 7 + MarkEditTools/Package.swift | 27 + MarkEditTools/Plugins/SwiftLint/main.swift | 36 + MarkEditTools/README.md | 5 + PreviewExtension/Base.lproj/Main.xib | 22 + PreviewExtension/Info.entitlements | 12 + PreviewExtension/Info.plist | 24 + PreviewExtension/PreviewViewController.swift | 89 + README.md | 23 +- 304 files changed, 22040 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .gitignore create mode 100644 .periphery.yml create mode 100644 .swiftlint.yml create mode 100644 Build.xcconfig create mode 100644 CoreEditor/.eslintrc.js create mode 100644 CoreEditor/.gitignore create mode 100644 CoreEditor/index.css create mode 100644 CoreEditor/index.html create mode 100644 CoreEditor/index.ts create mode 100644 CoreEditor/jest.config.js create mode 100644 CoreEditor/package.json create mode 100644 CoreEditor/src/@codegen/config.json create mode 100644 CoreEditor/src/@codegen/swift-config.mustache create mode 100644 CoreEditor/src/@codegen/swift-named-type.mustache create mode 100644 CoreEditor/src/@codegen/swift-native-module.mustache create mode 100644 CoreEditor/src/@codegen/swift-shared-types.mustache create mode 100644 CoreEditor/src/@codegen/swift-web-module.mustache create mode 100644 CoreEditor/src/@light/index.html create mode 100644 CoreEditor/src/@light/index.ts create mode 100644 CoreEditor/src/@light/vite.config.ts create mode 100644 CoreEditor/src/@test/editor.ts create mode 100644 CoreEditor/src/@test/mock.ts create mode 100644 CoreEditor/src/@types/JSRect.ts create mode 100644 CoreEditor/src/@types/global.d.ts create mode 100644 CoreEditor/src/@vendor/joplin/markdownMathParser.ts create mode 100644 CoreEditor/src/@vendor/lang-markdown/README.md create mode 100644 CoreEditor/src/@vendor/lang-markdown/commands.ts create mode 100644 CoreEditor/src/@vendor/lang-markdown/index.ts create mode 100644 CoreEditor/src/@vendor/lang-markdown/markdown.ts create mode 100644 CoreEditor/src/@vendor/language-data/README.md create mode 100644 CoreEditor/src/@vendor/language-data/index.ts create mode 100644 CoreEditor/src/bridge/native/core.ts create mode 100644 CoreEditor/src/bridge/native/preview.ts create mode 100644 CoreEditor/src/bridge/nativeModule.ts create mode 100644 CoreEditor/src/bridge/web/config.ts create mode 100644 CoreEditor/src/bridge/web/core.ts create mode 100644 CoreEditor/src/bridge/web/format.ts create mode 100644 CoreEditor/src/bridge/web/grammarly.ts create mode 100644 CoreEditor/src/bridge/web/history.ts create mode 100644 CoreEditor/src/bridge/web/lineEndings.ts create mode 100644 CoreEditor/src/bridge/web/search.ts create mode 100644 CoreEditor/src/bridge/web/selection.ts create mode 100644 CoreEditor/src/bridge/web/textChecker.ts create mode 100644 CoreEditor/src/bridge/web/toc.ts create mode 100644 CoreEditor/src/bridge/webModule.ts create mode 100644 CoreEditor/src/common/store.ts create mode 100644 CoreEditor/src/common/utils.ts create mode 100644 CoreEditor/src/config.ts create mode 100644 CoreEditor/src/core.ts create mode 100644 CoreEditor/src/dom/events/index.ts create mode 100644 CoreEditor/src/dom/events/isMetaKey.ts create mode 100644 CoreEditor/src/dom/views/index.ts create mode 100644 CoreEditor/src/dom/views/types.ts create mode 100644 CoreEditor/src/extensions.ts create mode 100644 CoreEditor/src/modules/commands/index.ts create mode 100644 CoreEditor/src/modules/commands/insertBlockWithMarks.ts create mode 100644 CoreEditor/src/modules/commands/removeListMarkers.ts create mode 100644 CoreEditor/src/modules/commands/replaceSelections.ts create mode 100644 CoreEditor/src/modules/commands/toggleBlockWithMarks.ts create mode 100644 CoreEditor/src/modules/commands/toggleLineLeadingMark.ts create mode 100644 CoreEditor/src/modules/commands/toggleListStyle.ts create mode 100644 CoreEditor/src/modules/commands/types.ts create mode 100644 CoreEditor/src/modules/config/index.ts create mode 100644 CoreEditor/src/modules/grammarly/index.css create mode 100644 CoreEditor/src/modules/grammarly/index.ts create mode 100644 CoreEditor/src/modules/history/index.ts create mode 100644 CoreEditor/src/modules/indentation/index.ts create mode 100644 CoreEditor/src/modules/indentation/types.ts create mode 100644 CoreEditor/src/modules/input/index.ts create mode 100644 CoreEditor/src/modules/input/wrapBlock.ts create mode 100644 CoreEditor/src/modules/lineEndings/index.ts create mode 100644 CoreEditor/src/modules/lineEndings/types.ts create mode 100644 CoreEditor/src/modules/localization/index.ts create mode 100644 CoreEditor/src/modules/preview/index.css create mode 100644 CoreEditor/src/modules/preview/index.ts create mode 100644 CoreEditor/src/modules/search/index.ts create mode 100644 CoreEditor/src/modules/search/options.ts create mode 100644 CoreEditor/src/modules/search/rangesFromQuery.ts create mode 100644 CoreEditor/src/modules/search/searchOccurrences.ts create mode 100644 CoreEditor/src/modules/selection/index.ts create mode 100644 CoreEditor/src/modules/selection/searchMatchPosition.ts create mode 100644 CoreEditor/src/modules/selection/selectWholeLineAt.ts create mode 100644 CoreEditor/src/modules/selection/selectWithRanges.ts create mode 100644 CoreEditor/src/modules/selection/selectedLineColumn.ts create mode 100644 CoreEditor/src/modules/selection/selectedRanges.ts create mode 100644 CoreEditor/src/modules/selection/types.ts create mode 100644 CoreEditor/src/modules/snippets/index.ts create mode 100644 CoreEditor/src/modules/snippets/insertSnippet.ts create mode 100644 CoreEditor/src/modules/textChecker/index.ts create mode 100644 CoreEditor/src/modules/textChecker/options.ts create mode 100644 CoreEditor/src/modules/toc/index.ts create mode 100644 CoreEditor/src/modules/toc/types.ts create mode 100644 CoreEditor/src/styling/builder.ts create mode 100644 CoreEditor/src/styling/config.ts create mode 100644 CoreEditor/src/styling/helper.ts create mode 100644 CoreEditor/src/styling/markdown.ts create mode 100644 CoreEditor/src/styling/matchers/lexer.ts create mode 100644 CoreEditor/src/styling/matchers/regex.ts create mode 100644 CoreEditor/src/styling/matchers/stateful.ts create mode 100644 CoreEditor/src/styling/nodes/code.ts create mode 100644 CoreEditor/src/styling/nodes/gutter.ts create mode 100644 CoreEditor/src/styling/nodes/heading.ts create mode 100644 CoreEditor/src/styling/nodes/invisible.ts create mode 100644 CoreEditor/src/styling/nodes/link.ts create mode 100644 CoreEditor/src/styling/nodes/table.ts create mode 100644 CoreEditor/src/styling/themes/cobalt.ts create mode 100644 CoreEditor/src/styling/themes/colors.ts create mode 100644 CoreEditor/src/styling/themes/dracula.ts create mode 100644 CoreEditor/src/styling/themes/github-dark.ts create mode 100644 CoreEditor/src/styling/themes/github-light.ts create mode 100644 CoreEditor/src/styling/themes/index.ts create mode 100644 CoreEditor/src/styling/themes/minimal-dark.ts create mode 100644 CoreEditor/src/styling/themes/minimal-light.ts create mode 100644 CoreEditor/src/styling/themes/winter-is-coming-dark.ts create mode 100644 CoreEditor/src/styling/themes/winter-is-coming-light.ts create mode 100644 CoreEditor/src/styling/themes/xcode-dark.ts create mode 100644 CoreEditor/src/styling/themes/xcode-light.ts create mode 100644 CoreEditor/src/styling/types.ts create mode 100644 CoreEditor/test/commands.test.ts create mode 100644 CoreEditor/test/history.test.ts create mode 100644 CoreEditor/test/input.test.ts create mode 100644 CoreEditor/test/lezer.test.ts create mode 100644 CoreEditor/test/lineEndings.test.ts create mode 100644 CoreEditor/test/search.test.ts create mode 100644 CoreEditor/test/styling.test.ts create mode 100644 CoreEditor/test/toc.test.ts create mode 100644 CoreEditor/tsconfig.json create mode 100644 CoreEditor/vite.config.ts create mode 100644 CoreEditor/yarn.lock create mode 100644 MarkEdit.xcodeproj/project.pbxproj create mode 100644 MarkEdit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 MarkEdit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hans).xcscheme create mode 100644 MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hant).xcscheme create mode 100644 MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac.xcscheme create mode 100644 MarkEdit.xcodeproj/xcshareddata/xcschemes/PreviewExtension.xcscheme create mode 100644 MarkEditCore/.gitignore create mode 100644 MarkEditCore/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditCore.xcscheme create mode 100644 MarkEditCore/Package.swift create mode 100644 MarkEditCore/README.md create mode 100644 MarkEditCore/Sources/EditorConfig.swift create mode 100644 MarkEditCore/Sources/EditorLocalizable.swift create mode 100644 MarkEditCore/Sources/Extensions/Data+Extension.swift create mode 100644 MarkEditCore/Sources/Extensions/EditorConfig+Extension.swift create mode 100644 MarkEditCore/Sources/Extensions/Encodable+Extension.swift create mode 100644 MarkEditCore/Sources/Extensions/String+Extension.swift create mode 100644 MarkEditCore/Tests/EncodingTests.swift create mode 100644 MarkEditCore/Tests/Files/sample-gb18030.md create mode 100644 MarkEditCore/Tests/Files/sample-japanese-euc.md create mode 100644 MarkEditCore/Tests/Files/sample-korean-euc.md create mode 100644 MarkEditCore/Tests/Files/sample-utf8.md create mode 100644 MarkEditKit/.gitignore create mode 100644 MarkEditKit/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditKit.xcscheme create mode 100644 MarkEditKit/Package.swift create mode 100644 MarkEditKit/README.md create mode 100644 MarkEditKit/Sources/Bridge/Native/Generated/NativeModuleCore.swift create mode 100644 MarkEditKit/Sources/Bridge/Native/Generated/NativeModulePreview.swift create mode 100644 MarkEditKit/Sources/Bridge/Native/Modules/EditorModuleCore.swift create mode 100644 MarkEditKit/Sources/Bridge/Native/Modules/EditorModulePreview.swift create mode 100644 MarkEditKit/Sources/Bridge/Native/NativeModules.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeConfig.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeCore.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeFormat.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeGrammarly.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeHistory.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeLineEndings.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSearch.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSelection.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTableOfContents.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTextChecker.swift create mode 100644 MarkEditKit/Sources/Bridge/Web/WebModuleBridge.swift create mode 100644 MarkEditKit/Sources/EditorLogger.swift create mode 100644 MarkEditKit/Sources/EditorMessageHandler.swift create mode 100644 MarkEditKit/Sources/EditorTextEncoding.swift create mode 100644 MarkEditKit/Sources/Extensions/DispatchQueue+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/JSRect+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/LineEndings+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/URL+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/UserDefaults+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/WKWebView+Extension.swift create mode 100644 MarkEditKit/Sources/Extensions/WKWebViewConfiguration+Extension.swift create mode 100755 MarkEditMac/Base.lproj/Main.storyboard create mode 100644 MarkEditMac/Info.entitlements create mode 100644 MarkEditMac/Info.plist create mode 100644 MarkEditMac/Modules/.gitignore create mode 100644 MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitControls.xcscheme create mode 100644 MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitExtensions.xcscheme create mode 100644 MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/FontPicker.xcscheme create mode 100644 MarkEditMac/Modules/Package.swift create mode 100644 MarkEditMac/Modules/README.md create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/BackgroundTheming.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/Buttons/IconOnlyButton.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/Buttons/NonBezelButton.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/Buttons/TitleOnlyButton.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/DividerView.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/FocusTrackingView.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineView.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineWindow.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/LabelView.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/LabeledSearchField.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitControls/RoundedButtonGroup.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSApplication+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDataDetector+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDocument+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSPasteboard+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSWorkspace+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAnimationContext+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAppearance+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSColor+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSControl+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSFont+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSImage+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenu+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenuItem+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSSearchField+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSTextField+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSView+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSViewController+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSWindow+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/Extensions/NSNotification+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/FontPicker.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/FontPickerConfiguration.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/FontPickerHandlers.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/FontStyle.swift create mode 100644 MarkEditMac/Modules/Sources/FontPicker/Internal/FontManagerDelegate.swift create mode 100644 MarkEditMac/Modules/Sources/Previewer/Previewer.swift create mode 100644 MarkEditMac/Modules/Sources/Previewer/Resources/katex.html create mode 100644 MarkEditMac/Modules/Sources/Previewer/Resources/mermaid.html create mode 100644 MarkEditMac/Modules/Sources/Previewer/Resources/table.html create mode 100644 MarkEditMac/Modules/Sources/Proofing/Grammarly.swift create mode 100644 MarkEditMac/Modules/Sources/SettingsUI/Extensions/View+Extension.swift create mode 100644 MarkEditMac/Modules/Sources/SettingsUI/SettingsForm.swift create mode 100644 MarkEditMac/Modules/Sources/SettingsUI/SettingsRootViewController.swift create mode 100644 MarkEditMac/Modules/Sources/SettingsUI/SettingsTabViewController.swift create mode 100644 MarkEditMac/Modules/Tests/DataDetectorTests.swift create mode 100644 MarkEditMac/Modules/Tests/PasteboardTests.swift create mode 100644 MarkEditMac/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png create mode 100644 MarkEditMac/Resources/Assets.xcassets/Contents.json create mode 100644 MarkEditMac/Resources/en.lproj/Localizable.strings create mode 100644 MarkEditMac/Resources/zh-Hans.lproj/Localizable.strings create mode 100644 MarkEditMac/Resources/zh-Hant.lproj/Localizable.strings create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Config.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Delegate.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Encoding.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+GotoLine.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+HyperLink.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+LineEndings.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Menu.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Pandoc.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Preview.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+TextFinder.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+Toolbar.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController+UI.swift create mode 100644 MarkEditMac/Sources/Editor/Controllers/EditorViewController.swift create mode 100644 MarkEditMac/Sources/Editor/EditorWindow.swift create mode 100644 MarkEditMac/Sources/Editor/EditorWindowController.swift create mode 100644 MarkEditMac/Sources/Editor/Models/EditorDocument.swift create mode 100644 MarkEditMac/Sources/Editor/Models/EditorReusePool.swift create mode 100644 MarkEditMac/Sources/Editor/Models/EditorToolbarItems.swift create mode 100644 MarkEditMac/Sources/Editor/Views/EditorPanelView.swift create mode 100644 MarkEditMac/Sources/Editor/Views/EditorStatusView.swift create mode 100644 MarkEditMac/Sources/Editor/Views/EditorWebView.swift create mode 100644 MarkEditMac/Sources/Extensions/NSApplication+Extension.swift create mode 100644 MarkEditMac/Sources/Main/AppPreferences.swift create mode 100644 MarkEditMac/Sources/Main/AppResources.swift create mode 100644 MarkEditMac/Sources/Main/AppTheme.swift create mode 100644 MarkEditMac/Sources/Main/Application/AppDelegate+Document.swift create mode 100644 MarkEditMac/Sources/Main/Application/AppDelegate+Menu.swift create mode 100644 MarkEditMac/Sources/Main/Application/AppDelegate.swift create mode 100644 MarkEditMac/Sources/Panels/Find/EditorFindButtons.swift create mode 100644 MarkEditMac/Sources/Panels/Find/EditorFindPanel+Delegate.swift create mode 100644 MarkEditMac/Sources/Panels/Find/EditorFindPanel+Menu.swift create mode 100644 MarkEditMac/Sources/Panels/Find/EditorFindPanel+UI.swift create mode 100644 MarkEditMac/Sources/Panels/Find/EditorFindPanel.swift create mode 100644 MarkEditMac/Sources/Panels/Replace/EditorReplaceButtons.swift create mode 100644 MarkEditMac/Sources/Panels/Replace/EditorReplacePanel.swift create mode 100644 MarkEditMac/Sources/Settings/EditorSettingsView.swift create mode 100644 MarkEditMac/Sources/Settings/GeneralSettingsView.swift create mode 100644 MarkEditMac/Sources/Settings/SettingTabs.swift create mode 100644 MarkEditMac/Sources/Settings/WindowSettingsView.swift create mode 100644 MarkEditMac/zh-Hans.lproj/Main.strings create mode 100644 MarkEditMac/zh-Hant.lproj/Main.strings create mode 100644 MarkEditMacTests/MarkEditMacTests.swift create mode 100644 MarkEditTools/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 MarkEditTools/Package.swift create mode 100644 MarkEditTools/Plugins/SwiftLint/main.swift create mode 100644 MarkEditTools/README.md create mode 100644 PreviewExtension/Base.lproj/Main.xib create mode 100644 PreviewExtension/Info.entitlements create mode 100644 PreviewExtension/Info.plist create mode 100644 PreviewExtension/PreviewViewController.swift diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..caac6ad2 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,47 @@ +name: Build and test all targets + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + build: + + runs-on: macos-latest + + strategy: + matrix: + node-version: ['18.x'] + + defaults: + run: + working-directory: 'CoreEditor' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: '**/yarn.lock' + + - name: Build and test CoreEditor + run: | + yarn install + yarn build + yarn test + + - name: Build MarkEditMac + run: | + xcodebuild build -project ../MarkEdit.xcodeproj -scheme MarkEditMac -destination 'platform=macOS' + + - name: Test MarkEditCoreTests + run: | + xcodebuild test -project ../MarkEdit.xcodeproj -scheme MarkEditCoreTests -destination 'platform=macOS' + + - name: Test ModulesTests + run: | + xcodebuild test -project ../MarkEdit.xcodeproj -scheme ModulesTests -destination 'platform=macOS' diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a825620f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +## System +*.DS_Store + +## User settings +xcuserdata/ +Local.xcconfig diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 00000000..6c085a2c --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,16 @@ +project: MarkEdit.xcodeproj +retain_public: true +schemes: +- MarkEditMac +- PreviewExtension +targets: +- MarkEditCore.MarkEditCore +- MarkEditKit.MarkEditKit +- MarkEditMac +- Modules.AppKitControls +- Modules.AppKitExtensions +- Modules.FontPicker +- Modules.Previewer +- Modules.Proofing +- Modules.SettingsUI +- PreviewExtension diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..996d6dc2 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,560 @@ +disabled_rules: + + # Rationale: Arbitrary restriction + # https://github.com/realm/SwiftLint/blob/master/Rules.md#cyclomatic-complexity + - cyclomatic_complexity + + # Rationale: There are cases where you may want to declare the string enum value explicitly + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-string-enum-value + - redundant_string_enum_value + +opt_in_rules: + + # Rationale: When using map, we think of it being used to transform a current array into something else + # https://github.com/realm/SwiftLint/blob/master/Rules.md#array-init + - array_init + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#attributes + - attributes + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#block-based-kvo + - block_based_kvo + + # Rationale: Prevents retain cycles + # https://github.com/realm/SwiftLint/blob/master/Rules.md#class-delegate-protocol + - class_delegate_protocol + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#closing-brace-spacing + - closing_brace + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#closure-parameter-position + - closure_parameter_position + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#closure-spacing + - closure_spacing + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#colon + - colon + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#comma-spacing + - comma + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#compiler-protocol-init + - compiler_protocol_init + + # Rationale: A more clear and consise way to check if something exists + # https://github.com/realm/SwiftLint/blob/master/Rules.md#contains_over_filter_count + - contains_over_filter_count + + # Rationale: A more clear and consise way to check if something exists + # https://github.com/realm/SwiftLint/blob/master/Rules.md#contains_over_filter_is_empty + - contains_over_filter_is_empty + + # Rationale: A more clear and consise way to check if something exists + # https://github.com/realm/SwiftLint/blob/master/Rules.md#contains-over-first-not-nil + - contains_over_first_not_nil + + # Rationale: A more clear and consise way to check if a range exists + # https://github.com/realm/SwiftLint/blob/master/Rules.md#contains_over_range_nil_comparison + - contains_over_range_nil_comparison + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#control-statement + - control_statement + + # Rationale: Encourages proper memory practices + # https://github.com/realm/SwiftLint/blob/master/Rules.md#discarded-notification-center-observer + - discarded_notification_center_observer + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#discouraged-direct-initialization + - discouraged_direct_init + + # Rationale: A nil bool is a tri-state variable which can be modeled more clearly + # https://github.com/realm/SwiftLint/blob/master/Rules.md#discouraged-optional-boolean + - discouraged_optional_boolean + + # Rationale: Imports are not required more than once. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#duplicate-imports + - duplicate_imports + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#dynamic-inline + - dynamic_inline + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty_collection_literal + - empty_collection_literal + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-count + - empty_count + + # Rationale: Provides consistency in coding style and brevity. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-enum-arguments + - empty_enum_arguments + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-parameters + - empty_parameters + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-parentheses-with-trailing-closure + - empty_parentheses_with_trailing_closure + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-string + - empty_string + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#explicit-init + - explicit_init + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#fallthrough + - fallthrough + + # Rationale: Encourages better documentation + # https://github.com/realm/SwiftLint/blob/master/Rules.md#fatal-error-message + - fatal_error_message + + # Rationale: Encourages using the right API to solve a problem + # https://github.com/realm/SwiftLint/blob/master/Rules.md#first-where + - first_where + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#flatmap_over_map_reduce + - flatmap_over_map_reduce + + # Rationale: Encourages using the right API to solve a problem + # https://github.com/realm/SwiftLint/blob/master/Rules.md#for-where + - for_where + + # Rationale: Prevents coder error, doesn't crash, makes coder be explicit about their assumptions + # https://github.com/realm/SwiftLint/blob/master/Rules.md#force-cast + - force_cast + + # Rationale: Prevents coder error, doesn't crash, makes coder be explicit about their assumptions + # https://github.com/realm/SwiftLint/blob/master/Rules.md#force-try + - force_try + + # Rationale: Prevents coder error, doesn't crash, makes coder be explicit about their assumptions + # https://github.com/realm/SwiftLint/blob/master/Rules.md#force-unwrapping + - force_unwrapping + + # Rationale: Provides consistency in coding style and brevity. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#implicit-getter + - implicit_getter + + # Rationale: Prevents coder error, doesn't crash, makes coder be explicit about their assumptions + # https://github.com/realm/SwiftLint/blob/master/Rules.md#implicitly-unwrapped-optional + - implicitly_unwrapped_optional + + # Rationale: Encourages using the right API to solve a problem + # https://github.com/realm/SwiftLint/blob/master/Rules.md#is-disjoint + - is_disjoint + + # Rationale: Provides clarity and consistency by using the default parameter + # https://github.com/realm/SwiftLint/blob/master/Rules.md#joined-default-parameter + - joined_default_parameter + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#last-where + - last_where + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#leading-whitespace + - leading_whitespace + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-cggeometry-functions + - legacy_cggeometry_functions + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-constant + - legacy_constant + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-constructor + - legacy_constructor + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-hashing + - legacy_hashing + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-nsgeometry-functions + - legacy_nsgeometry_functions + + # Rationale: Usage of proper access level + # https://github.com/realm/SwiftLint/blob/master/Rules.md#lower-acl-than-parent + - lower_acl_than_parent + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#mark + - mark + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#multiline-parameters + - multiline_parameters + + # Rationale: Clarity of code + # https://github.com/realm/SwiftLint/blob/master/Rules.md#multiple-closures-with-trailing-closure + - multiple_closures_with_trailing_closure + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#no_space_in_method_call + - no_space_in_method_call + + # Rationale: Encourages coder best practices though language feature likely makes this obsolete + # https://github.com/realm/SwiftLint/blob/master/Rules.md#multiple-closures-with-trailing-closure + - notification_center_detachment + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#opening-brace-spacing + - opening_brace + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#operator-usage-whitespace + - operator_usage_whitespace + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#operator-function-whitespace + - operator_whitespace + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#overridden-methods-call-super + - overridden_super_call + + # Rationale: Prevents unpredictable behavior + # https://github.com/realm/SwiftLint/blob/master/Rules.md#override-in-extension + - override_in_extension + + # Rationale: Promotes consistency and reduces duplication. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#pattern-matching-keywords + - pattern_matching_keywords + + # Rationale: Keep internal details from being overexposed + # https://github.com/realm/SwiftLint/blob/master/Rules.md#private-over-fileprivate + - private_over_fileprivate + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#private-unit-test + - private_unit_test + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#prohibited-calls-to-super + - prohibited_super_call + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#protocol-property-accessors-order + - protocol_property_accessors_order + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-discardable-let + - redundant_discardable_let + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-nil-coalescing + - redundant_nil_coalescing + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-objc-attribute + - redundant_objc_attribute + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-optional-initialization + - redundant_optional_initialization + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-void-return + - redundant_void_return + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#required-enum-case + - required_enum_case + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#returning-whitespace + - return_arrow_whitespace + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#shorthand-operator + - shorthand_operator + + # Rationale: There should be only XCTestCase per file + # https://github.com/realm/SwiftLint/blob/master/Rules.md#single-test-class + - single_test_class + + # Rationale: Provides consistency and clarity in coding style and is less code + # https://github.com/realm/SwiftLint/blob/master/Rules.md#min-or-max-over-sorted-first-or-last + - sorted_first_last + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#statement-position + - statement_position + + # Rationale: Provides cleaniness of code + # https://github.com/realm/SwiftLint/blob/master/Rules.md#superfluous-disable-command + - superfluous_disable_command + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#switch-and-case-statement-alignment + - switch_case_alignment + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#syntactic-sugar + - syntactic_sugar + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#trailing-newline + - trailing_newline + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#trailing-semicolon + - trailing_semicolon + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unneeded-break-in-switch + - unneeded_break_in_switch + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-control-flow-label + - unused_control_flow_label + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-closure-parameter + - unused_closure_parameter + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-enumerated + - unused_enumerated + + # Rationale: Provides consistency in coding style and brevity + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-optional-binding + - unused_optional_binding + + # Rationale: Avoids issues where the setter is not using the value passed in. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-setter-value + - unused_setter_value + + # Rationale: Prevents coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#valid-ibinspectable + - valid_ibinspectable + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#vertical-parameter-alignment + - vertical_parameter_alignment + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#vertical-parameter-alignment-on-call + - vertical_parameter_alignment_on_call + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#vertical-whitespace + - vertical_whitespace + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#vertical-whitespace-before-closing-braces + - vertical_whitespace_closing_braces + + # Rationale: Provides consistency in coding style and follows modern practices of the language + # https://github.com/realm/SwiftLint/blob/master/Rules.md#void-return + - void_return + + # Rationale: Prevents retain cycles and coder error + # https://github.com/realm/SwiftLint/blob/master/Rules.md#weak-delegate + - weak_delegate + + # Rationale: Encourages better documentation + # https://github.com/realm/SwiftLint/blob/master/Rules.md#xctfail-message + - xctfail_message + + # Rationale: Provides consistency in coding style + # https://github.com/realm/SwiftLint/blob/master/Rules.md#yoda-condition-rule + - yoda_condition + + # Rationale: Provides consistency in coding style. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#reduce-boolean + - reduce_boolean + + # Rationale: == is not used for NSObject comparison, and could lead to confusion. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#nsobject-prefer-isequal + - nsobject_prefer_isequal + + # Rationale: Provides consistency in coding style. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-capture-list + - unused_capture_list + + # Rationale: Prevents issues with using unowned. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#unowned-variable-capture + - unowned_variable_capture + + # Rationale: Ensures all enums can be switched upon. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#duplicate-enum-cases + - duplicate_enum_cases + + # Rationale: Provides consistency in coding style. + # https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-multiple + - legacy_multiple + + # Prefer using `AnyObject` over `class` for class-only protocols. + - anyobject_protocol + + # Closure end should have the same indentation as the line that started it. + - closure_end_indentation + + # All elements in a collection literal should be vertically aligned + - collection_alignment + + # Prefer // comment over //comment + - comment_spacing + + # Refer assertionFailure() over assert(false) + - discouraged_assert + + # Discourage enum cases that interfere with Optional.none and type checking. + - discouraged_none_name + + # Encourages initializers over object literals + - discouraged_object_literal + + # Empty XCTest methods should be avoided. + - empty_xctest_method + + # Swift files must have our copyright header info. + - file_header + + # Comparing two identical operands is likely a mistake. + - identical_operands + + # Customized identifier name rules + - identifier_name + + # Let and var should be separated from other statements by a blank line. + - let_var_whitespace + + # Array and dictionary literal end should have the same indentation as the line that started it. + - literal_expression_end_indentation + + # Modifier order should be consistent + - modifier_order + + # Static strings should be used as a key/comment in NSLocalizedString in order for genstrings to work. + - nslocalizedstring_key + + # Matching an enum case against an optional enum without '?' is supported on Swift 5.1 and above. + - optional_enum_case_matching + + # Using Self to reference a Type when possible is more stable + - prefer_self_in_static_references + + # Prefer `Self` over `type(of: self)` when accessing properties or calling methods. + - prefer_self_type_over_type_of_self + + # Prefer `.zero` over explicit init with zero parameters (e.g., `CGPoint(x: 0, y: 0)`) + - prefer_zero_over_explicit_init + + # Mutable reference can be faster than repeated copying + - reduce_into + + # Catch uses of self inside an inline closure used for initializing a variable + - self_in_property_initialization + + # Operators should be declared as static functions, not free functions. + - static_operator + + # Test case API should be private for that test case + - test_case_accessibility + + # Prefer `someBool.toggle()` over `someBool = !someBool` + - toggle_bool + + # Parentheses are not needed when declaring closure arguments. + - unneeded_parentheses_in_closure_argument + + # Test classes must implement balanced setUp and tearDown methods + - balanced_xctest_lifecycle + + # Types used for hosting only static members should be implemeted as a caseless enum to avoid instantiation. + - convenience_type + + # Number of associated values in an enum should be low + - enum_case_associated_values_count + + # Arguments should be either on the same line, or one per line + - multiline_arguments + + # Multiline arguments should have their surrounding brackets in a new line. + - multiline_arguments_brackets + + # Chained function calls should be either on the same line, or one per line. + - multiline_function_chains + + # Multiline literals should have their surrounding brackets in a new line. + - multiline_literal_brackets + + # Multiline parameters should have their surrounding brackets in a new line. + - multiline_parameters_brackets + + # Trailing closure syntax should be used whenever possible. + - trailing_closure + + # Uncallable or unreachabable implimentations should be marked unavailable + - unavailable_function + + # Catch statements should not declare error variables without type casting. + - untyped_error_in_catch + + # Prefer specific XCTest matchers over `XCTAssertEqual` and `XCTAssertNotEqual` + - xct_specific_matcher + +attributes: + always_on_same_line: ["@IBAction", "@IBSegueAction", "@NSManaged", "@escaping", "@objc", "@frozen"] + +file_length: + warning: 1000 + error: 2000 + +function_body_length: + warning: 200 + error: 300 + +identifier_name: + min_length: 1 + max_length: + warning: 60 + error: 80 + allowed_symbols: ["_"] + validates_start_with_lowercase: true + +inclusive_language: + override_allowed_terms: ["master", "blacklist", "whitelist"] + +large_tuple: + warning: 3 + error: 3 + +line_length: + warning: 400 + error: 400 + +nesting: + type_level: 2 + function_level: 5 + +trailing_comma: + mandatory_comma: true + +type_name: + max_length: + warning: 75 + error: 75 diff --git a/Build.xcconfig b/Build.xcconfig new file mode 100644 index 00000000..5ac6f8bc --- /dev/null +++ b/Build.xcconfig @@ -0,0 +1,16 @@ +// +// Build.xcconfig +// MarkEditMac +// +// Created by cyan on 12/26/22. +// +// https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project + +CODE_SIGN_IDENTITY = - +DEVELOPMENT_TEAM = +PRODUCT_BUNDLE_IDENTIFIER = app.cyan.markedit-dev + +MARKETING_VERSION = 1.0.0 +CURRENT_PROJECT_VERSION = 3 + +#include? "Local.xcconfig" diff --git a/CoreEditor/.eslintrc.js b/CoreEditor/.eslintrc.js new file mode 100644 index 00000000..c0ed53a4 --- /dev/null +++ b/CoreEditor/.eslintrc.js @@ -0,0 +1,104 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: [ + 'tsconfig.json', + ], + }, + plugins: [ + '@typescript-eslint', + 'promise', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:compat/recommended', + 'plugin:promise/recommended', + ], + env: { + browser: true, + }, + ignorePatterns: [ + '.eslintrc.js', + 'src/@vendor/*', + ], + rules: { + 'no-case-declarations': 'error', + 'no-prototype-builtins': 'error', + 'array-bracket-spacing': ['error', 'never'], + 'eol-last': 'error', + 'no-new-wrappers': 'error', + 'no-array-constructor': 'error', + 'no-throw-literal': 'error', + + 'no-restricted-syntax': [ + 'error', + { + 'selector': 'TSEnumDeclaration[const=true]', + 'message': 'Always use enum and not const enum. TypeScript enums already cannot be mutated; const enum is a separate language feature related to optimization that makes the enum invisible to JavaScript users of the module.', + }, + { + 'selector': 'ArrowFunctionExpression[parent.type=\'PropertyDefinition\'][parent.parent.type=\'ClassBody\']', + 'message': 'Arrow functions are not allowed in class properties. Define as a method instead.', + }, + ], + + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/member-delimiter-style': 'error', + '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-readonly': 'error', + '@typescript-eslint/strict-boolean-expressions': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/type-annotation-spacing': 'error', + '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], + '@typescript-eslint/consistent-type-exports': ['error', { fixMixedExportsWithInlineTypeSpecifier: true }], + '@typescript-eslint/class-literal-property-style': 'error', + + 'brace-style': 'off', + '@typescript-eslint/brace-style': ['error', '1tbs', { 'allowSingleLine': true }], + 'comma-dangle': 'off', + '@typescript-eslint/comma-dangle': ['error', 'always-multiline'], + 'comma-spacing': 'off', + '@typescript-eslint/comma-spacing': 'error', + 'dot-notation': 'off', + '@typescript-eslint/dot-notation': 'error', + 'func-call-spacing': 'off', + '@typescript-eslint/func-call-spacing': 'error', + 'indent': 'off', + '@typescript-eslint/indent': ['error', 2], + 'keyword-spacing': 'off', + '@typescript-eslint/keyword-spacing': 'error', + 'no-dupe-class-members': 'off', + '@typescript-eslint/no-dupe-class-members': 'error', + 'no-duplicate-imports': 'off', + '@typescript-eslint/no-duplicate-imports': 'error', + 'no-extra-parens': 'off', + '@typescript-eslint/no-extra-parens': ['error', 'functions'], + 'no-invalid-this': 'off', + '@typescript-eslint/no-invalid-this': 'error', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'error', + 'no-throw-literal': 'off', + '@typescript-eslint/no-throw-literal': 'error', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { 'ignoreRestSiblings': true, 'argsIgnorePattern': '^_' }], + 'object-curly-spacing': 'off', + '@typescript-eslint/object-curly-spacing': ['error', 'always'], + 'quotes': 'off', + '@typescript-eslint/quotes': [2, 'single', { 'avoidEscape': true }], + 'no-return-await': 'off', + '@typescript-eslint/return-await': 'error', + 'semi': 'off', + '@typescript-eslint/semi': 'error', + + 'promise/prefer-await-to-then': 'error', + }, +}; diff --git a/CoreEditor/.gitignore b/CoreEditor/.gitignore new file mode 100644 index 00000000..763301fc --- /dev/null +++ b/CoreEditor/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/CoreEditor/index.css b/CoreEditor/index.css new file mode 100644 index 00000000..a3213cb6 --- /dev/null +++ b/CoreEditor/index.css @@ -0,0 +1,39 @@ +/* General */ + +:root { + color-scheme: light dark; +} + +html, body { + margin: 0; + padding: 0; + overflow: hidden; +} + +/* CodeMirror */ + +.cm-editor { + height: 100vh; +} + +.cm-gutters { + cursor: default; + user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + + /* To work around a Safari bug where :hover is not cleared when mouse is outside the window */ + margin-top: 1px; + margin-left: 1px; +} + +/* Markdown */ + +.cm-md-link { + cursor: pointer; + text-decoration: underline; +} + +.cm-md-header { + font-weight: bold; +} diff --git a/CoreEditor/index.html b/CoreEditor/index.html new file mode 100644 index 00000000..a5acb60d --- /dev/null +++ b/CoreEditor/index.html @@ -0,0 +1,15 @@ + + + + + + MarkEdit + + + + + +
+ + + diff --git a/CoreEditor/index.ts b/CoreEditor/index.ts new file mode 100644 index 00000000..60c75ec8 --- /dev/null +++ b/CoreEditor/index.ts @@ -0,0 +1,73 @@ +import { Config } from './src/config'; +import { isProd } from './src/common/utils'; + +import { WebModuleConfigImpl } from './src/bridge/web/config'; +import { WebModuleCoreImpl } from './src/bridge/web/core'; +import { WebModuleHistoryImpl } from './src/bridge/web/history'; +import { WebModuleLineEndingsImpl } from './src/bridge/web/lineEndings'; +import { WebModuleTextCheckerImpl } from './src/bridge/web/textChecker'; +import { WebModuleSelectionImpl } from './src/bridge/web/selection'; +import { WebModuleFormatImpl } from './src/bridge/web/format'; +import { WebModuleSearchImpl } from './src/bridge/web/search'; +import { WebModuleTableOfContentsImpl } from './src/bridge/web/toc'; +import { WebModuleGrammarlyImpl } from './src/bridge/web/grammarly'; + +import { pseudoDocument } from './src/@test/mock'; +import { createNativeModule, handleNativeReply } from './src/bridge/nativeModule'; +import { NativeModuleCore } from './src/bridge/native/core'; +import { NativeModulePreview } from './src/bridge/native/preview'; + +import * as core from './src/core'; +import * as styling from './src/styling/config'; +import * as events from './src/dom/events'; + +// "{{EDITOR_CONFIG}}" will be replaced with a JSON literal in production +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const config: Config = isProd ? ('{{EDITOR_CONFIG}}' as any) : { + text: pseudoDocument, + theme: 'github-light', + fontFamily: 'monospace', + fontSize: 17, + showLineNumbers: true, + showActiveLineIndicator: true, + showInvisibles: true, + typewriterMode: false, + focusMode: false, + lineWrapping: true, + lineHeight: 1.5, + localizable: { + previewButtonTitle: 'preview', + }, +}; + +window.webModules = { + config: new WebModuleConfigImpl(), + core: new WebModuleCoreImpl(), + history: new WebModuleHistoryImpl(), + lineEndings: new WebModuleLineEndingsImpl(), + textChecker: new WebModuleTextCheckerImpl(), + selection: new WebModuleSelectionImpl(), + format: new WebModuleFormatImpl(), + search: new WebModuleSearchImpl(), + toc: new WebModuleTableOfContentsImpl(), + grammarly: new WebModuleGrammarlyImpl(), +}; + +window.nativeModules = { + core: createNativeModule('core'), + preview: createNativeModule('preview'), +}; + +window.onload = () => { + window.config = config; + window.handleNativeReply = handleNativeReply; + window.nativeModules.core.notifyWindowDidLoad(); + + // On Prod, text is reset by the native code + if (!isProd) { + core.resetEditor(config.text); + } +}; + +styling.setUp(config); +events.startObserving(); diff --git a/CoreEditor/jest.config.js b/CoreEditor/jest.config.js new file mode 100644 index 00000000..d3a5c360 --- /dev/null +++ b/CoreEditor/jest.config.js @@ -0,0 +1,7 @@ +/** @type { import('ts-jest').JestConfigWithTsJest } */ + +// eslint-disable-next-line no-undef +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', +}; diff --git a/CoreEditor/package.json b/CoreEditor/package.json new file mode 100644 index 00000000..5d1b3963 --- /dev/null +++ b/CoreEditor/package.json @@ -0,0 +1,45 @@ +{ + "name": "mark-edit", + "version": "1.0.0", + "description": "Just like TextEdit on Mac but dedicated to Markdown.", + "scripts": { + "dev": "vite", + "lint": "eslint .", + "codegen": "ts-gyb --config ./src/@codegen/config.json", + "test": "jest", + "build": "yarn lint && yarn codegen && vite build && vite build -c src/@light/vite.config.ts" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/language-data": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@grammarly/editor-sdk": "^2.0.0", + "@lezer/common": "^1.0.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/eslint": "^8.4.10", + "@types/jest": "^29.4.0", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^5.50.0", + "@typescript-eslint/parser": "^5.50.0", + "eslint": "^8.33.0", + "eslint-plugin-compat": "^4.0.2", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.4.1", + "jest-environment-jsdom": "^29.4.1", + "rollup": "^3.0.0", + "ts-gyb": "^0.8.0", + "ts-jest": "^29.0.5", + "typescript": "^4.0.0", + "vite": "^4.0.0", + "vite-plugin-singlefile": "^0.13.2" + } +} diff --git a/CoreEditor/src/@codegen/config.json b/CoreEditor/src/@codegen/config.json new file mode 100644 index 00000000..59dcf592 --- /dev/null +++ b/CoreEditor/src/@codegen/config.json @@ -0,0 +1,46 @@ +{ + "parsing": { + "targets": { + "config": { + "source": ["../config.ts"] + }, + "native": { + "source": ["../bridge/native/*.ts"] + }, + "web": { + "source": ["../bridge/web/*.ts"] + } + }, + "predefinedTypes": [ + "CodeGen_Int" + ], + "defaultCustomTags": {}, + "dropInterfaceIPrefix": true + }, + "rendering": { + "swift": { + "renders": [ + { + "target": "config", + "template": "swift-config.mustache", + "outputPath": "../../../MarkEditCore/Sources" + }, + { + "target": "native", + "template": "swift-native-module.mustache", + "outputPath": "../../../MarkEditKit/Sources/Bridge/Native/Generated" + }, + { + "target": "web", + "template": "swift-web-module.mustache", + "outputPath": "../../../MarkEditKit/Sources/Bridge/Web/Generated" + } + ], + "namedTypesTemplatePath": "swift-shared-types.mustache", + "namedTypesOutputPath": "../../../MarkEditKit/Sources/Generated/SharedTypes.swift", + "typeNameMap": { + "CodeGen_Int": "Int" + } + } + } +} diff --git a/CoreEditor/src/@codegen/swift-config.mustache b/CoreEditor/src/@codegen/swift-config.mustache new file mode 100644 index 00000000..6752255f --- /dev/null +++ b/CoreEditor/src/@codegen/swift-config.mustache @@ -0,0 +1,29 @@ +// +// {{moduleName}}.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public struct {{moduleName}}: Encodable { + {{#members}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + let {{name}}: {{type}} + {{/members}} + + public init( + {{#members}} + {{name}}: {{type}}{{^last}},{{/last}} + {{/members}} + ) { + {{#members}} + self.{{name}} = {{name}} + {{/members}} + } +} diff --git a/CoreEditor/src/@codegen/swift-named-type.mustache b/CoreEditor/src/@codegen/swift-named-type.mustache new file mode 100644 index 00000000..b4857832 --- /dev/null +++ b/CoreEditor/src/@codegen/swift-named-type.mustache @@ -0,0 +1,38 @@ +{{#custom}} +{{#documentationLines}} +///{{{.}}} +{{/documentationLines}} +public struct {{typeName}}: Codable { + {{#members}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + public var {{name}}: {{type}} + {{/members}} + {{#staticMembers}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + private var {{name}}: {{type}} = {{{value}}} + {{/staticMembers}} + + public init({{#members}}{{name}}: {{type}}{{^last}}, {{/last}}{{/members}}) { + {{#members}} + self.{{name}} = {{name}} + {{/members}} + } +} +{{/custom}} +{{#enum}} +{{#documentationLines}} +///{{{.}}} +{{/documentationLines}} +public enum {{typeName}}: {{valueType}}, Codable { + {{#members}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + case {{key}} = {{{value}}} + {{/members}} +} +{{/enum}} diff --git a/CoreEditor/src/@codegen/swift-native-module.mustache b/CoreEditor/src/@codegen/swift-native-module.mustache new file mode 100644 index 00000000..d472329f --- /dev/null +++ b/CoreEditor/src/@codegen/swift-native-module.mustache @@ -0,0 +1,102 @@ +// +// {{moduleName}}.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public protocol {{moduleName}}: NativeModule { + {{#methods}} + func {{methodName}}({{parametersDeclaration}}){{#returnType}} async -> {{returnType}}{{/returnType}} + {{/methods}} +} + +public extension {{moduleName}} { + var bridge: NativeBridge { {{customTags.bridgeName}}(self) } +} + +final class {{customTags.bridgeName}}: NativeBridge { + static let name = "{{customTags.invokePath}}" + lazy var methods: [String: NativeMethod] = [ + {{#methods}} + "{{methodName}}": { [weak self] in + await self?.{{methodName}}(parameters: $0) + }, + {{/methods}} + ] + + private let module: {{moduleName}} + private lazy var decoder = JSONDecoder() + + init(_ module: {{moduleName}}) { + self.module = module + } + {{#methods}} + + @MainActor private func {{methodName}}(parameters: Data) async -> Result? { + {{#parameters.length}} + struct Message: Decodable { + {{#parameters}} + var {{name}}: {{type}} + {{/parameters}} + } + + let message: Message + do { + message = try decoder.decode(Message.self, from: parameters) + } catch { + Logger.assertFail("Failed to decode parameters: \(parameters)") + return .failure(error) + } + + {{/parameters.length}} + {{#returnType}}let result = await {{/returnType}}module.{{methodName}}({{#parameters}}{{name}}: message.{{name}}{{^last}}, {{/last}}{{/parameters}}) + return .success({{#returnType}}result{{/returnType}}{{^returnType}}nil{{/returnType}}) + } + {{/methods}} +} +{{#associatedTypes}} + +{{#custom}} +{{#documentationLines}} +///{{{.}}} +{{/documentationLines}} +public struct {{typeName}}: {{#isFromParameter}}{{#isFromReturn}}Codable{{/isFromReturn}}{{^isFromReturn}}Decodable{{/isFromReturn}}{{/isFromParameter}}{{^isFromParameter}}{{#isFromReturn}}Encodable{{/isFromReturn}}{{/isFromParameter}}, Equatable { + {{#members}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + public var {{name}}: {{type}} + {{/members}} + {{#staticMembers}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + private var {{name}}: {{type}} = {{{value}}} + {{/staticMembers}} + + public init({{#members}}{{name}}: {{type}}{{^last}}, {{/last}}{{/members}}) { + {{#members}} + self.{{name}} = {{name}} + {{/members}} + } +} +{{/custom}} +{{#enum}} +{{#documentationLines}} +///{{{.}}} +{{/documentationLines}} +public enum {{typeName}}: {{valueType}}, Codable { + {{#members}} + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + case {{key}} = {{{value}}} + {{/members}} +} +{{/enum}} +{{/associatedTypes}} diff --git a/CoreEditor/src/@codegen/swift-shared-types.mustache b/CoreEditor/src/@codegen/swift-shared-types.mustache new file mode 100644 index 00000000..47e38633 --- /dev/null +++ b/CoreEditor/src/@codegen/swift-shared-types.mustache @@ -0,0 +1,14 @@ +// +// SharedTypes.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation +{{#.}} + +{{> swift-named-type}} +{{/.}} diff --git a/CoreEditor/src/@codegen/swift-web-module.mustache b/CoreEditor/src/@codegen/swift-web-module.mustache new file mode 100644 index 00000000..0b693259 --- /dev/null +++ b/CoreEditor/src/@codegen/swift-web-module.mustache @@ -0,0 +1,56 @@ +// +// {{moduleName}}.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class {{moduleName}} { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + {{#methods}} + + {{#documentationLines}} + ///{{{.}}} + {{/documentationLines}} + {{#returnType}}@MainActor {{/returnType}}public func {{methodName}}({{parametersDeclaration}}{{#parametersDeclaration.length}}{{^returnType}}, {{/returnType}}{{/parametersDeclaration.length}}{{^returnType}}completion: ((Result) -> Void)? = nil{{/returnType}}) {{#returnType}}async throws -> {{returnType}} {{/returnType}}{ + {{#parameters.length}} + struct Message: Encodable { + {{#parameters}} + let {{name}}: {{type}} + {{/parameters}} + } + + {{/parameters.length}} + {{#parameters.length}} + let message = Message( + {{#parameters}} + {{name}}: {{name}}{{^last}},{{/last}} + {{/parameters}} + ) + + {{/parameters.length}} + {{#returnType}} + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.{{customTags.invokePath}}.{{methodName}}"{{#parameters.length}}, message: message{{/parameters.length}}) { + continuation.resume(with: $0) + } + } + {{/returnType}} + {{^returnType}} + webView?.invoke(path: "webModules.{{customTags.invokePath}}.{{methodName}}", {{#parameters.length}}message: message, {{/parameters.length}}completion: completion) + {{/returnType}} + } + {{/methods}} +} +{{#associatedTypes}} + +{{> swift-named-type}} +{{/associatedTypes}} diff --git a/CoreEditor/src/@light/index.html b/CoreEditor/src/@light/index.html new file mode 100644 index 00000000..b2bddb7b --- /dev/null +++ b/CoreEditor/src/@light/index.html @@ -0,0 +1,13 @@ + + + + + + MarkEdit + + + +
+ + + diff --git a/CoreEditor/src/@light/index.ts b/CoreEditor/src/@light/index.ts new file mode 100644 index 00000000..696e9265 --- /dev/null +++ b/CoreEditor/src/@light/index.ts @@ -0,0 +1,48 @@ +import { EditorView, highlightSpecialChars } from '@codemirror/view'; +import { Compartment, EditorState } from '@codemirror/state'; +import { markdown, markdownLanguage } from '../@vendor/lang-markdown'; + +import { Config } from '../config'; +import { markdownExtensions, renderExtensions } from '../styling/markdown'; + +import { loadTheme } from '../styling/themes'; +import * as styling from '../styling/config'; + +// "{{EDITOR_CONFIG}}" will be replaced with a JSON literal +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const config: Config = '{{EDITOR_CONFIG}}' as any; +window.config = config; + +const theme = new Compartment; +window.dynamics = { theme }; + +const extensions = [ + // Basic + highlightSpecialChars(), + EditorView.editable.of(false), + EditorState.readOnly.of(true), + EditorView.lineWrapping, + + // Markdown + markdown({ + base: markdownLanguage, + extensions: markdownExtensions, + }), + + // Styling + theme.of(loadTheme(config.theme)), + renderExtensions, +]; + +const doc = config.text; +const parent = document.querySelector('#editor') ?? document.body; + +window.editor = new EditorView({ doc, parent, extensions }); +styling.setUp(config); + +// To keep the app size smaller, we don't have bridge here, +// inject function to window directly. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).setTheme = (name: string) => { + styling.setTheme(loadTheme(name)); +}; diff --git a/CoreEditor/src/@light/vite.config.ts b/CoreEditor/src/@light/vite.config.ts new file mode 100644 index 00000000..bf99498a --- /dev/null +++ b/CoreEditor/src/@light/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; + +export default defineConfig({ + root: './src/@light', + build: { + outDir: './dist', + }, + plugins: [viteSingleFile()], +}); diff --git a/CoreEditor/src/@test/editor.ts b/CoreEditor/src/@test/editor.ts new file mode 100644 index 00000000..776686be --- /dev/null +++ b/CoreEditor/src/@test/editor.ts @@ -0,0 +1,41 @@ +import { EditorView } from '@codemirror/view'; +import { Extension, EditorSelection } from '@codemirror/state'; +import { markdown, markdownLanguage } from '../@vendor/lang-markdown'; + +export function setUp(doc: string, extensions: Extension = []) { + const editor = new EditorView({ + doc, + parent: document.body, + extensions: [ + ...[extensions], + markdown({ base: markdownLanguage }), + ], + }); + + editor.focus(); + window.editor = editor; +} + +export function setText(doc: string) { + window.editor.dispatch({ + changes: { + insert: doc, + from: 0, to: window.editor.state.doc.length, + }, + selection: EditorSelection.cursor(0), + }); +} + +export function getText() { + return window.editor.state.doc.toString(); +} + +export function selectAll() { + selectRange(0); +} + +export function selectRange(from: number, to?: number) { + window.editor.dispatch({ + selection: EditorSelection.range(from, to === undefined ? window.editor.state.doc.length - from : to), + }); +} diff --git a/CoreEditor/src/@test/mock.ts b/CoreEditor/src/@test/mock.ts new file mode 100644 index 00000000..0b0322dd --- /dev/null +++ b/CoreEditor/src/@test/mock.ts @@ -0,0 +1,76 @@ +export const pseudoDocument = `# Heading 1 + +Hello, World! Open source: [MarkEdit](https://github.com/MarkEdit-app/MarkEdit) + +Image link: ![Tux, the Linux mascot](/assets/tux.png), autolink https://markedit.app, or [link references][1]. + +[1]: http://example.com + +Heading level 2 +--------------- + +I just love **bold text**. Italicized text is the *cat's meow*. This text is ***really important***. + +> Dorothy followed her through many of the beautiful rooms in her castle. + +*** + +1. First item +2. Second item + +- First item +- Second item + +\`Inline code\`, and code block: + +\`\`\`ts +import fs = require("fs"); + +class MyClass { + public static myValue: string; + constructor(init: string) { + this.myValue = init; + } +} + +module MyModule { + export interface MyInterface extends Other { + myProperty: any; + } +} + +declare magicNumber number; +myArray.forEach(() => { }); // Fat arrow syntax + +function $initHighlight(block, cls) { + try { + if (cls.search(/\bno-highlight\b/) != -1) + return process(block, true, 0x0F) + + \` class="\${cls}"\`; + } catch (e) { + /* handle exception */ + } + for (var i = 0 / 2; i < classes.length; i++) { + if (checkCondition(classes[i]) === undefined) + console.log('undefined'); + } + + return ( +
+ {block} +
+ ) +} + +export $initHighlight; +\`\`\` + + This is a code segment with four leading spaces + +Embedded HTML + +\`\`\`diff +- color: "#24292e", ++ color: "#24292f", +\`\`\` +`; diff --git a/CoreEditor/src/@types/JSRect.ts b/CoreEditor/src/@types/JSRect.ts new file mode 100644 index 00000000..9c6f8d34 --- /dev/null +++ b/CoreEditor/src/@types/JSRect.ts @@ -0,0 +1,9 @@ +/** + * "CGRect-fashion" rect. + */ +export interface JSRect { + x: number; + y: number; + width: number; + height: number; +} diff --git a/CoreEditor/src/@types/global.d.ts b/CoreEditor/src/@types/global.d.ts new file mode 100644 index 00000000..701f57eb --- /dev/null +++ b/CoreEditor/src/@types/global.d.ts @@ -0,0 +1,44 @@ +import { EditorView } from '@codemirror/view'; +import { Config, Dynamics } from '../config'; +import { WebModule } from '../bridge/webModule'; +import { NativeReply } from '../bridge/nativeModule'; +import { NativeModuleCore } from '../bridge/native/core'; +import { NativeModulePreview } from '../bridge/native/preview'; + +declare global { + type CodeGen_Int = number & { _brand: never }; +} + +interface WebKit { + messageHandlers?: { + bridge?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + postMessage: (object: any) => void; + }; + }; +} + +declare global { + interface Window { + webkit?: WebKit; + editor: EditorView; + config: Config; + dynamics: Dynamics; + webModules: Record; + nativeModules: { + core: NativeModuleCore; + preview: NativeModulePreview; + }; + handleNativeReply: (reply: NativeReply) => void; + } + + interface ImportMetaEnv { + readonly PROD: boolean; + } + + interface ImportMeta { + readonly env: ImportMetaEnv; + } +} + +export {}; diff --git a/CoreEditor/src/@vendor/joplin/markdownMathParser.ts b/CoreEditor/src/@vendor/joplin/markdownMathParser.ts new file mode 100644 index 00000000..86fdaab6 --- /dev/null +++ b/CoreEditor/src/@vendor/joplin/markdownMathParser.ts @@ -0,0 +1,131 @@ +// Thanks to https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.ts + +import { tags, Tag } from "@lezer/highlight"; +import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from "@lezer/common"; + +// Extend the existing markdown parser +import { MarkdownConfig, BlockContext, Line, LeafBlock } from "@lezer/markdown"; + +// The existing stexMath parser is used to parse the text between the $s +import { stexMath } from "@codemirror/legacy-modes/mode/stex"; +import { StreamLanguage } from "@codemirror/language"; + +// (?:[>]\s*)?: Optionally allow block math lines to start with '> ' +const mathBlockStartRegex = /^(?:\s*[>]\s*)?\$\$/; +const mathBlockEndRegex = /\$\$\s*$/; + +const texLanguage = StreamLanguage.define(stexMath); +export const blockMathTagName = "BlockMath"; +export const blockMathContentTagName = "BlockMathContent"; +export const inlineMathTagName = "InlineMath"; +export const inlineMathContentTagName = "InlineMathContent"; + +export const mathTag = Tag.define(tags.monospace); +export const inlineMathTag = Tag.define(mathTag); + +/** + * Wraps a TeX math-mode parser. This removes [nodeTag] from the syntax tree + * and replaces it with a region handled by the sTeXMath parser. + * + * @param nodeTag Name of the nodes to replace with regions parsed by the sTeX parser. + * @returns a wrapped sTeX parser. + */ +const wrappedTeXParser = (nodeTag: string): ParseWrapper => { + return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse | null => { + if (node.name !== nodeTag) { + return null; + } + + return { + parser: texLanguage.parser, + }; + }); +}; + +// Extension for recognising block code +const BlockMathConfig: MarkdownConfig = { + defineNodes: [ + { + name: blockMathTagName, + style: mathTag, + }, + { + name: blockMathContentTagName, + }, + ], + parseBlock: [ + { + name: blockMathTagName, + before: "Blockquote", + parse(cx: BlockContext, line: Line): boolean { + const delimLen = 2; + + // $$ delimiter? Start math! + const mathStartMatch = mathBlockStartRegex.exec(line.text); + if (mathStartMatch) { + const start = cx.lineStart + mathStartMatch[0].length; + let stop; + + let endMatch = mathBlockEndRegex.exec(line.text.substring(mathStartMatch[0].length)); + + // If the math region ends immediately (on the same line), + if (endMatch) { + const lineLength = line.text.length; + stop = cx.lineStart + lineLength - endMatch[0].length; + } else { + let hadNextLine = false; + + // Otherwise, it's a multi-line block display. + // Consume lines until we reach the end. + do { + hadNextLine = cx.nextLine(); + endMatch = hadNextLine ? mathBlockEndRegex.exec(line.text) : null; + } while (hadNextLine && endMatch === null); + + if (hadNextLine && endMatch) { + const lineLength = line.text.length; + + // Remove the ending delimiter + stop = cx.lineStart + lineLength - endMatch[0].length; + } else { + stop = cx.lineStart; + } + } + const lineEnd = cx.lineStart + line.text.length; + + // Label the region. Add two labels so that one can be removed. + const contentElem = cx.elt(blockMathContentTagName, start, stop); + const containerElement = cx.elt( + blockMathTagName, + start - delimLen, + + // Math blocks don't need ending delimiters, so ensure we don't + // include text that doesn't exist. + Math.min(lineEnd, stop + delimLen), + + // The child of the container element should be the content element + [contentElem] + ); + cx.addElement(containerElement); + + // Don't re-process the ending delimiter (it may look the same + // as the starting delimiter). + cx.nextLine(); + + return true; + } + + return false; + }, + // End paragraph-like blocks + endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean { + // Leaf blocks (e.g. block quotes) end early if math starts. + return mathBlockStartRegex.exec(line.text) !== null; + }, + }, + ], + wrap: wrappedTeXParser(blockMathContentTagName), +}; + +/** Markdown configuration for block math support. */ +export const markdownMathExtension = BlockMathConfig; diff --git a/CoreEditor/src/@vendor/lang-markdown/README.md b/CoreEditor/src/@vendor/lang-markdown/README.md new file mode 100644 index 00000000..7032caf5 --- /dev/null +++ b/CoreEditor/src/@vendor/lang-markdown/README.md @@ -0,0 +1,40 @@ +# MarkEdit-app/lang-markdown + +For now, lang-markdown is mostly copied from the [@codemirror/lang-markdown](https://github.com/codemirror/lang-markdown) package, with minimal changes to the `insertNewlineContinueMarkup` command and `findSectionEnd` function. + +Check "[MarkEdit]" to see the actual modified behavior. + + + +# @codemirror/lang-markdown [![NPM version](https://img.shields.io/npm/v/@codemirror/lang-markdown.svg)](https://www.npmjs.org/package/@codemirror/lang-markdown) + +[ [**WEBSITE**](https://codemirror.net/6/) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-markdown/blob/main/CHANGELOG.md) ] + +This package implements Markdown language support for the +[CodeMirror](https://codemirror.net/6/) code editor. + +The [project page](https://codemirror.net/6/) has more information, a +number of [examples](https://codemirror.net/6/examples/) and the +[documentation](https://codemirror.net/6/docs/). + +This code is released under an +[MIT license](https://github.com/codemirror/lang-markdown/tree/main/LICENSE). + +We aim to be an inclusive, welcoming community. To make that explicit, +we have a [code of +conduct](http://contributor-covenant.org/version/1/1/0/) that applies +to communication around the project. + +## API Reference + +@markdown + +@markdownLanguage + +@commonmarkLanguage + +@insertNewlineContinueMarkup + +@deleteMarkupBackward + +@markdownKeymap diff --git a/CoreEditor/src/@vendor/lang-markdown/commands.ts b/CoreEditor/src/@vendor/lang-markdown/commands.ts new file mode 100644 index 00000000..eb31946d --- /dev/null +++ b/CoreEditor/src/@vendor/lang-markdown/commands.ts @@ -0,0 +1,224 @@ +import {StateCommand, Text, EditorSelection, ChangeSpec} from "@codemirror/state" +import {syntaxTree} from "@codemirror/language" +import {SyntaxNode, Tree} from "@lezer/common" +import {markdownLanguage} from "./markdown" + +class Context { + constructor( + readonly node: SyntaxNode, + readonly from: number, + readonly to: number, + readonly spaceBefore: string, + readonly spaceAfter: string, + readonly type: string, + readonly item: SyntaxNode | null + ) {} + + blank(maxWidth: number | null, trailing = true) { + let result = this.spaceBefore + (this.node.name == "Blockquote" ? ">" : "") + if (maxWidth != null) { + while (result.length < maxWidth) result += " " + return result + } else { + for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--) result += " " + return result + (trailing ? this.spaceAfter : "") + } + } + + marker(doc: Text, add: number) { + let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item!, doc)[2] + add)) : "" + return this.spaceBefore + number + this.type + this.spaceAfter + } +} + +function getContext(node: SyntaxNode, doc: Text) { + let nodes = [] + for (let cur: SyntaxNode | null = node; cur && cur.name != "Document"; cur = cur.parent) { + if (cur.name == "ListItem" || cur.name == "Blockquote" || cur.name == "FencedCode") + nodes.push(cur) + } + let context = [] + for (let i = nodes.length - 1; i >= 0; i--) { + let node = nodes[i], match + let line = doc.lineAt(node.from), startPos = node.from - line.from + if (node.name == "FencedCode") { + context.push(new Context(node, startPos, startPos, "", "", "", null)) + } else if (node.name == "Blockquote" && (match = /^[ \t]*>( ?)/.exec(line.text.slice(startPos)))) { + context.push(new Context(node, startPos, startPos + match[0].length, "", match[1], ">", null)) + } else if (node.name == "ListItem" && node.parent!.name == "OrderedList" && + (match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(line.text.slice(startPos)))) { + let after = match[3], len = match[0].length + if (after.length >= 4) { after = after.slice(0, after.length - 4); len -= 4 } + context.push(new Context(node.parent!, startPos, startPos + len, match[1], after, match[2], node)) + } else if (node.name == "ListItem" && node.parent!.name == "BulletList" && + (match = /^([ \t]*)([-+*])([ \t]{1,4}\[[ xX]\])?([ \t]+)/.exec(line.text.slice(startPos)))) { + let after = match[4], len = match[0].length + if (after.length > 4) { after = after.slice(0, after.length - 4); len -= 4 } + let type = match[2] + if (match[3]) type += match[3].replace(/[xX]/, ' ') + context.push(new Context(node.parent!, startPos, startPos + len, match[1], after, type, node)) + } + } + return context +} + +function itemNumber(item: SyntaxNode, doc: Text) { + return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10))! +} + +function renumberList(after: SyntaxNode, doc: Text, changes: ChangeSpec[], offset = 0) { + for (let prev = -1, node = after;;) { + if (node.name == "ListItem") { + let m = itemNumber(node, doc) + let number = +m[2] + if (prev >= 0) { + if (number != prev + 1) return + changes.push({from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset)}) + } + prev = number + } + let next = node.nextSibling + if (!next) break + node = next + } +} + +/// This command, when invoked in Markdown context with cursor +/// selection(s), will create a new line with the markup for +/// blockquotes and lists that were active on the old line. If the +/// cursor was directly after the end of the markup for the old line, +/// trailing whitespace and list markers are removed from that line. +/// +/// The command does nothing in non-Markdown context, so it should +/// not be used as the only binding for Enter (even in a Markdown +/// document, HTML and code regions might use a different language). +export const insertNewlineContinueMarkup: StateCommand = ({state, dispatch}) => { + let tree = syntaxTree(state), {doc} = state + let dont = null, changes = state.changeByRange(range => { + if (!range.empty || !markdownLanguage.isActiveAt(state, range.from)) return dont = {range} + let pos = range.from, line = doc.lineAt(pos) + let context = getContext(tree.resolveInner(pos, -1), doc) + while (context.length && context[context.length - 1].from > pos - line.from) context.pop() + if (!context.length) return dont = {range} + let inner = context[context.length - 1] + if (inner.to - inner.spaceAfter.length > pos - line.from) return dont = {range} + + let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to)) + // Empty line in list + if (inner.item && emptyLine) { + // First list item or blank line before: delete a level of markup + if (inner.node.firstChild!.to >= pos || + line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text)) { + let next = context.length > 1 ? context[context.length - 2] : null + let delTo, insert = "" + if (next && next.item) { // Re-add marker for the list at the next level + delTo = line.from + next.from + insert = next.marker(doc, 1) + } else { + delTo = line.from + (next ? next.to : 0) + } + let changes: ChangeSpec[] = [{from: delTo, to: pos, insert}] + if (inner.node.name == "OrderedList") renumberList(inner.item!, doc, changes, -2) + if (next && next.node.name == "OrderedList") renumberList(next.item!, doc, changes) + return {range: EditorSelection.cursor(delTo + insert.length), changes} + } else { // [MarkEdit] Delete the prefix and insert necessary spaces (original: https://github.com/codemirror/lang-markdown/blob/main/src/commands.ts#L124) + let insert = state.lineBreak + (line.text.match(/^\s*/) ?? [""])[0] + return {range: EditorSelection.cursor(pos + insert.length - (line.to - line.from)), changes: {from: line.from, to: line.from + line.text.length, insert}} + } + } + + if (inner.node.name == "Blockquote" && emptyLine && line.from) { + let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text) + // Two aligned empty quoted lines in a row + if (quoted && quoted.index == inner.from) { + let changes = state.changes([{from: prevLine.from + quoted.index, to: prevLine.to}, + {from: line.from + inner.from, to: line.to}]) + return {range: range.map(changes), changes} + } + } + + let changes: ChangeSpec[] = [] + if (inner.node.name == "OrderedList") renumberList(inner.item!, doc, changes) + let continued = inner.item && inner.item.from < line.from + let insert = "" + // If not dedented + if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)![0].length >= inner.to) { + for (let i = 0, e = context.length - 1; i <= e; i++) { + insert += i == e && !continued ? context[i].marker(doc, 1) + : context[i].blank(i < e ? context[i + 1].from - insert.length : null) + } + } + let from = pos + while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1))) from-- + insert = state.lineBreak + insert + changes.push({from, to: pos, insert}) + return {range: EditorSelection.cursor(from + insert.length), changes} + }) + if (dont) return false + dispatch(state.update(changes, {scrollIntoView: true, userEvent: "input"})) + return true +} + +function isMark(node: SyntaxNode) { + return node.name == "QuoteMark" || node.name == "ListMark" +} + +function contextNodeForDelete(tree: Tree, pos: number) { + let node = tree.resolveInner(pos, -1), scan = pos + if (isMark(node)) { + scan = node.from + node = node.parent! + } + for (let prev; prev = node.childBefore(scan);) { + if (isMark(prev)) { + scan = prev.from + } else if (prev.name == "OrderedList" || prev.name == "BulletList") { + node = prev.lastChild! + scan = node.to + } else { + break + } + } + return node +} + +/// This command will, when invoked in a Markdown context with the +/// cursor directly after list or blockquote markup, delete one level +/// of markup. When the markup is for a list, it will be replaced by +/// spaces on the first invocation (a further invocation will delete +/// the spaces), to make it easy to continue a list. +/// +/// When not after Markdown block markup, this command will return +/// false, so it is intended to be bound alongside other deletion +/// commands, with a higher precedence than the more generic commands. +export const deleteMarkupBackward: StateCommand = ({state, dispatch}) => { + let tree = syntaxTree(state) + let dont = null, changes = state.changeByRange(range => { + let pos = range.from, {doc} = state + if (range.empty && markdownLanguage.isActiveAt(state, range.from)) { + let line = doc.lineAt(pos) + let context = getContext(contextNodeForDelete(tree, pos), doc) + if (context.length) { + let inner = context[context.length - 1] + let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0) + // Delete extra trailing space after markup + if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from))) + return {range: EditorSelection.cursor(line.from + spaceEnd), + changes: {from: line.from + spaceEnd, to: pos}} + if (pos - line.from == spaceEnd) { + let start = line.from + inner.from + // Replace a list item marker with blank space + if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to))) + return {range, changes: {from: start, to: line.from + inner.to, insert: inner.blank(inner.to - inner.from)}} + // Delete one level of indentation + if (start < pos) + return {range: EditorSelection.cursor(start), changes: {from: start, to: pos}} + } + } + } + return dont = {range} + }) + if (dont) return false + dispatch(state.update(changes, {scrollIntoView: true, userEvent: "delete"})) + return true +} diff --git a/CoreEditor/src/@vendor/lang-markdown/index.ts b/CoreEditor/src/@vendor/lang-markdown/index.ts new file mode 100644 index 00000000..e2a36ada --- /dev/null +++ b/CoreEditor/src/@vendor/lang-markdown/index.ts @@ -0,0 +1,58 @@ +import {Prec} from "@codemirror/state" +import {KeyBinding, keymap} from "@codemirror/view" +import {Language, LanguageSupport, LanguageDescription} from "@codemirror/language" +import {MarkdownExtension, MarkdownParser, parseCode} from "@lezer/markdown" +import {html} from "@codemirror/lang-html" +import {commonmarkLanguage, markdownLanguage, mkLang, getCodeParser} from "./markdown" +import {insertNewlineContinueMarkup, deleteMarkupBackward} from "./commands" +export {commonmarkLanguage, markdownLanguage, insertNewlineContinueMarkup, deleteMarkupBackward} + +/// A small keymap with Markdown-specific bindings. Binds Enter to +/// [`insertNewlineContinueMarkup`](#lang-markdown.insertNewlineContinueMarkup) +/// and Backspace to +/// [`deleteMarkupBackward`](#lang-markdown.deleteMarkupBackward). +export const markdownKeymap: readonly KeyBinding[] = [ + {key: "Enter", run: insertNewlineContinueMarkup}, + {key: "Backspace", run: deleteMarkupBackward} +] + +const htmlNoMatch = html({matchClosingTags: false}) + +/// Markdown language support. +export function markdown(config: { + /// When given, this language will be used by default to parse code + /// blocks. + defaultCodeLanguage?: Language | LanguageSupport, + /// A source of language support for highlighting fenced code + /// blocks. When it is an array, the parser will use + /// [`LanguageDescription.matchLanguageName`](#language.LanguageDescription^matchLanguageName) + /// with the fenced code info to find a matching language. When it + /// is a function, will be called with the info string and may + /// return a language or `LanguageDescription` object. + codeLanguages?: readonly LanguageDescription[] | ((info: string) => Language | LanguageDescription | null), + /// Set this to false to disable installation of the Markdown + /// [keymap](#lang-markdown.markdownKeymap). + addKeymap?: boolean, + /// Markdown parser + /// [extensions](https://github.com/lezer-parser/markdown#user-content-markdownextension) + /// to add to the parser. + extensions?: MarkdownExtension, + /// The base language to use. Defaults to + /// [`commonmarkLanguage`](#lang-markdown.commonmarkLanguage). + base?: Language +} = {}) { + let {codeLanguages, defaultCodeLanguage, addKeymap = true, base: {parser} = commonmarkLanguage} = config + if (!(parser instanceof MarkdownParser)) throw new RangeError("Base parser provided to `markdown` should be a Markdown parser") + let extensions = config.extensions ? [config.extensions] : [] + let support = [htmlNoMatch.support], defaultCode + if (defaultCodeLanguage instanceof LanguageSupport) { + support.push(defaultCodeLanguage.support) + defaultCode = defaultCodeLanguage.language + } else if (defaultCodeLanguage) { + defaultCode = defaultCodeLanguage + } + let codeParser = codeLanguages || defaultCode ? getCodeParser(codeLanguages, defaultCode) : undefined + extensions.push(parseCode({codeParser, htmlParser: htmlNoMatch.language.parser})) + if (addKeymap) support.push(Prec.high(keymap.of(markdownKeymap))) + return new LanguageSupport(mkLang(parser.configure(extensions)), support) +} diff --git a/CoreEditor/src/@vendor/lang-markdown/markdown.ts b/CoreEditor/src/@vendor/lang-markdown/markdown.ts new file mode 100644 index 00000000..86a146b6 --- /dev/null +++ b/CoreEditor/src/@vendor/lang-markdown/markdown.ts @@ -0,0 +1,84 @@ +import {Language, defineLanguageFacet, languageDataProp, foldNodeProp, indentNodeProp, foldService, + syntaxTree, LanguageDescription, ParseContext} from "@codemirror/language" +import {parser as baseParser, MarkdownParser, GFM, Subscript, Superscript, Emoji} from "@lezer/markdown" +import {SyntaxNode, NodeType, NodeProp} from "@lezer/common" + +const data = defineLanguageFacet({block: {open: ""}}) + +const headingProp = new NodeProp() + +const commonmark = baseParser.configure({ + props: [ + foldNodeProp.add(type => { + return !type.is("Block") || type.is("Document") || isHeading(type) != null ? undefined + : (tree, state) => ({from: state.doc.lineAt(tree.from).to, to: tree.to}) + }), + headingProp.add(isHeading), + indentNodeProp.add({ + Document: () => null + }), + languageDataProp.add({ + Document: data + }) + ] +}) + +function isHeading(type: NodeType) { + let match = /^(?:ATX|Setext)Heading(\d)$/.exec(type.name) + return match ? +match[1] : undefined +} + +// [MarkEdit] We prefer empty sections to be foldable (original: https://github.com/codemirror/lang-markdown/blob/main/src/markdown.ts#L31) +function findSectionEnd(headerNode: SyntaxNode, level: number) { + let last = headerNode + for (;;) { + let next = last.nextSibling, heading + if (!next) return last.parent?.to ?? last.to + if ((heading = isHeading(next.type)) != null && heading <= level) return next.to + last = next + } +} + +const headerIndent = foldService.of((state, start, end) => { + for (let node: SyntaxNode | null = syntaxTree(state).resolveInner(end, -1); node; node = node.parent) { + if (node.from < start) break + let heading = node.type.prop(headingProp) + if (heading == null) continue + let upto = findSectionEnd(node, heading) + if (upto > end) return {from: end, to: upto} + } + return null +}) + +export function mkLang(parser: MarkdownParser) { + return new Language(data, parser, [headerIndent], "markdown") +} + +/// Language support for strict CommonMark. +export const commonmarkLanguage = mkLang(commonmark) + +const extended = commonmark.configure([GFM, Subscript, Superscript, Emoji]) + +/// Language support for [GFM](https://github.github.com/gfm/) plus +/// subscript, superscript, and emoji syntax. +export const markdownLanguage = mkLang(extended) + +export function getCodeParser( + languages: readonly LanguageDescription[] | ((info: string) => Language | LanguageDescription | null) | undefined, + defaultLanguage?: Language +) { + return (info: string) => { + if (info && languages) { + let found = null + // Strip anything after whitespace + info = /\S*/.exec(info)![0] + if (typeof languages == "function") found = languages(info) + else found = LanguageDescription.matchLanguageName(languages, info, true) + if (found instanceof LanguageDescription) + return found.support ? found.support.language.parser : ParseContext.getSkippingParser(found.load()) + else if (found) + return found.parser + } + return defaultLanguage ? defaultLanguage.parser : null + } +} diff --git a/CoreEditor/src/@vendor/language-data/README.md b/CoreEditor/src/@vendor/language-data/README.md new file mode 100644 index 00000000..126ef692 --- /dev/null +++ b/CoreEditor/src/@vendor/language-data/README.md @@ -0,0 +1,22 @@ +# MarkEdit-app/language-data + +For now, language-data is mostly copied from the [@codemirror/language-data](https://github.com/codemirror/language-data) package, only kept commonly used ones to make the bundle smaller. + +# @codemirror/language-data [![NPM version](https://img.shields.io/npm/v/@codemirror/language-data.svg)](https://www.npmjs.org/package/@codemirror/language-data) + +[ [**WEBSITE**](https://codemirror.net/) | [**DOCS**](https://codemirror.net/docs/ref/#language-data) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/language-data/blob/main/CHANGELOG.md) ] + +This package implements language metadata and dynamic loading for the +[CodeMirror](https://codemirror.net/) code editor. + +The [project page](https://codemirror.net/) has more information, a +number of [examples](https://codemirror.net/examples/) and the +[documentation](https://codemirror.net/docs/). + +This code is released under an +[MIT license](https://github.com/codemirror/language-data/tree/main/LICENSE). + +We aim to be an inclusive, welcoming community. To make that explicit, +we have a [code of +conduct](http://contributor-covenant.org/version/1/1/0/) that applies +to communication around the project. diff --git a/CoreEditor/src/@vendor/language-data/index.ts b/CoreEditor/src/@vendor/language-data/index.ts new file mode 100644 index 00000000..98a52757 --- /dev/null +++ b/CoreEditor/src/@vendor/language-data/index.ts @@ -0,0 +1,406 @@ +import {LanguageSupport, LanguageDescription, StreamParser, StreamLanguage} from "@codemirror/language" + +function legacy(parser: StreamParser): LanguageSupport { + return new LanguageSupport(StreamLanguage.define(parser)) +} + +function sql(dialectName: keyof typeof import("@codemirror/lang-sql")) { + return import("@codemirror/lang-sql").then(m => m.sql({dialect: (m as any)[dialectName]})) +} + +/// An array of language descriptions for known language packages. +export const languages = [ + // New-style language modes + LanguageDescription.of({ + name: "C", + extensions: ["c","h","ino"], + load() { + return import("@codemirror/lang-cpp").then(m => m.cpp()) + } + }), + LanguageDescription.of({ + name: "C++", + alias: ["cpp"], + extensions: ["cpp","c++","cc","cxx","hpp","h++","hh","hxx"], + load() { + return import("@codemirror/lang-cpp").then(m => m.cpp()) + } + }), + LanguageDescription.of({ + name: "CQL", + alias: ["cassandra"], + extensions: ["cql"], + load() { return sql("Cassandra") } + }), + LanguageDescription.of({ + name: "CSS", + extensions: ["css"], + load() { + return import("@codemirror/lang-css").then(m => m.css()) + } + }), + LanguageDescription.of({ + name: "HTML", + alias: ["xhtml"], + extensions: ["html", "htm", "handlebars", "hbs"], + load() { + return import("@codemirror/lang-html").then(m => m.html()) + } + }), + LanguageDescription.of({ + name: "Java", + extensions: ["java"], + load() { + return import("@codemirror/lang-java").then(m => m.java()) + } + }), + LanguageDescription.of({ + name: "JavaScript", + alias: ["ecmascript","js","node"], + extensions: ["js", "mjs", "cjs"], + load() { + return import("@codemirror/lang-javascript").then(m => m.javascript()) + } + }), + LanguageDescription.of({ + name: "JSON", + alias: ["json5"], + extensions: ["json","map"], + load() { + return import("@codemirror/lang-json").then(m => m.json()) + } + }), + LanguageDescription.of({ + name: "JSX", + extensions: ["jsx"], + load() { + return import("@codemirror/lang-javascript").then(m => m.javascript({jsx: true})) + } + }), + LanguageDescription.of({ + name: "MariaDB SQL", + load() { return sql("MariaSQL") } + }), + LanguageDescription.of({ + name: "Markdown", + extensions: ["md", "markdown", "mkd"], + load() { + return import("@codemirror/lang-markdown").then(m => m.markdown()) + } + }), + LanguageDescription.of({ + name: "MS SQL", + load() { return sql("MSSQL") } + }), + LanguageDescription.of({ + name: "MySQL", + load() { return sql("MySQL") } + }), + LanguageDescription.of({ + name: "PHP", + extensions: ["php", "php3", "php4", "php5", "php7", "phtml"], + load() { + return import("@codemirror/lang-php").then(m => m.php()) + } + }), + LanguageDescription.of({ + name: "PLSQL", + extensions: ["pls"], + load() { return sql("PLSQL") } + }), + LanguageDescription.of({ + name: "PostgreSQL", + load() { return sql("PostgreSQL") } + }), + LanguageDescription.of({ + name: "Python", + extensions: ["BUILD","bzl","py","pyw"], + filename: /^(BUCK|BUILD)$/, + load() { + return import("@codemirror/lang-python").then(m => m.python()) + } + }), + LanguageDescription.of({ + name: "Rust", + extensions: ["rs"], + load() { + return import("@codemirror/lang-rust").then(m => m.rust()) + } + }), + LanguageDescription.of({ + name: "SQL", + extensions: ["sql"], + load() { return sql("StandardSQL") } + }), + LanguageDescription.of({ + name: "SQLite", + load() { return sql("SQLite") } + }), + LanguageDescription.of({ + name: "TSX", + extensions: ["tsx"], + load() { + return import("@codemirror/lang-javascript").then(m => m.javascript({jsx: true, typescript: true})) + } + }), + LanguageDescription.of({ + name: "TypeScript", + alias: ["ts"], + extensions: ["ts"], + load() { + return import("@codemirror/lang-javascript").then(m => m.javascript({typescript: true})) + } + }), + LanguageDescription.of({ + name: "WebAssembly", + extensions: ["wat","wast"], + load() { + return import("@codemirror/lang-wast").then(m => m.wast()) + } + }), + LanguageDescription.of({ + name: "XML", + alias: ["rss","wsdl","xsd"], + extensions: ["xml","xsl","xsd","svg"], + load() { + return import("@codemirror/lang-xml").then(m => m.xml()) + } + }), + + // Legacy modes ported from CodeMirror 5 + + LanguageDescription.of({ + name: "C#", + alias: ["csharp","cs"], + extensions: ["cs"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.csharp)) + } + }), + LanguageDescription.of({ + name: "CMake", + extensions: ["cmake","cmake.in"], + filename: /^CMakeLists\.txt$/, + load() { + return import("@codemirror/legacy-modes/mode/cmake").then(m => legacy(m.cmake)) + } + }), + LanguageDescription.of({ + name: "Common Lisp", + alias: ["lisp"], + extensions: ["cl","lisp","el"], + load() { + return import("@codemirror/legacy-modes/mode/commonlisp").then(m => legacy(m.commonLisp)) + } + }), + LanguageDescription.of({ + name: "Dart", + extensions: ["dart"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.dart)) + } + }), + LanguageDescription.of({ + name: "diff", + extensions: ["diff","patch"], + load() { + return import("@codemirror/legacy-modes/mode/diff").then(m => legacy(m.diff)) + } + }), + LanguageDescription.of({ + name: "Dockerfile", + filename: /^Dockerfile$/, + load() { + return import("@codemirror/legacy-modes/mode/dockerfile").then(m => legacy(m.dockerFile)) + } + }), + LanguageDescription.of({ + name: "Go", + extensions: ["go"], + load() { + return import("@codemirror/legacy-modes/mode/go").then(m => legacy(m.go)) + } + }), + LanguageDescription.of({ + name: "Groovy", + extensions: ["groovy","gradle"], + filename: /^Jenkinsfile$/, + load() { + return import("@codemirror/legacy-modes/mode/groovy").then(m => legacy(m.groovy)) + } + }), + LanguageDescription.of({ + name: "Haskell", + extensions: ["hs"], + load() { + return import("@codemirror/legacy-modes/mode/haskell").then(m => legacy(m.haskell)) + } + }), + LanguageDescription.of({ + name: "HTTP", + load() { + return import("@codemirror/legacy-modes/mode/http").then(m => legacy(m.http)) + } + }), + LanguageDescription.of({ + name: "Julia", + extensions: ["jl"], + load() { + return import("@codemirror/legacy-modes/mode/julia").then(m => legacy(m.julia)) + } + }), + LanguageDescription.of({ + name: "Kotlin", + extensions: ["kt"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.kotlin)) + } + }), + LanguageDescription.of({ + name: "LESS", + extensions: ["less"], + load() { + return import("@codemirror/legacy-modes/mode/css").then(m => legacy(m.less)) + } + }), + LanguageDescription.of({ + name: "Lua", + extensions: ["lua"], + load() { + return import("@codemirror/legacy-modes/mode/lua").then(m => legacy(m.lua)) + } + }), + LanguageDescription.of({ + name: "Mathematica", + extensions: ["m","nb","wl","wls"], + load() { + return import("@codemirror/legacy-modes/mode/mathematica").then(m => legacy(m.mathematica)) + } + }), + LanguageDescription.of({ + name: "Nginx", + filename: /nginx.*\.conf$/i, + load() { + return import("@codemirror/legacy-modes/mode/nginx").then(m => legacy(m.nginx)) + } + }), + LanguageDescription.of({ + name: "Objective-C", + alias: ["objective-c","objc"], + extensions: ["m"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.objectiveC)) + } + }), + LanguageDescription.of({ + name: "Objective-C++", + alias: ["objective-c++","objc++"], + extensions: ["mm"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.objectiveCpp)) + } + }), + LanguageDescription.of({ + name: "Pascal", + extensions: ["p","pas"], + load() { + return import("@codemirror/legacy-modes/mode/pascal").then(m => legacy(m.pascal)) + } + }), + LanguageDescription.of({ + name: "Perl", + extensions: ["pl","pm"], + load() { + return import("@codemirror/legacy-modes/mode/perl").then(m => legacy(m.perl)) + } + }), + LanguageDescription.of({ + name: "PowerShell", + extensions: ["ps1","psd1","psm1"], + load() { + return import("@codemirror/legacy-modes/mode/powershell").then(m => legacy(m.powerShell)) + } + }), + LanguageDescription.of({ + name: "R", + alias: ["rscript"], + extensions: ["r","R"], + load() { + return import("@codemirror/legacy-modes/mode/r").then(m => legacy(m.r)) + } + }), + LanguageDescription.of({ + name: "Ruby", + alias: ["jruby","macruby","rake","rb","rbx"], + extensions: ["rb"], + load() { + return import("@codemirror/legacy-modes/mode/ruby").then(m => legacy(m.ruby)) + } + }), + LanguageDescription.of({ + name: "Scala", + extensions: ["scala"], + load() { + return import("@codemirror/legacy-modes/mode/clike").then(m => legacy(m.scala)) + } + }), + LanguageDescription.of({ + name: "Scheme", + extensions: ["scm","ss"], + load() { + return import("@codemirror/legacy-modes/mode/scheme").then(m => legacy(m.scheme)) + } + }), + LanguageDescription.of({ + name: "SCSS", + extensions: ["scss"], + load() { + return import("@codemirror/legacy-modes/mode/css").then(m => legacy(m.sCSS)) + } + }), + LanguageDescription.of({ + name: "Shell", + alias: ["bash","sh","zsh"], + extensions: ["sh","ksh","bash"], + filename: /^PKGBUILD$/, + load() { + return import("@codemirror/legacy-modes/mode/shell").then(m => legacy(m.shell)) + } + }), + LanguageDescription.of({ + name: "Swift", + extensions: ["swift"], + load() { + return import("@codemirror/legacy-modes/mode/swift").then(m => legacy(m.swift)) + } + }), + LanguageDescription.of({ + name: "sTeX", + load() { + return import("@codemirror/legacy-modes/mode/stex").then(m => legacy(m.stex)) + } + }), + LanguageDescription.of({ + name: "LaTeX", + alias: ["tex"], + extensions: ["text","ltx","tex"], + load() { + return import("@codemirror/legacy-modes/mode/stex").then(m => legacy(m.stex)) + } + }), + LanguageDescription.of({ + name: "TOML", + extensions: ["toml"], + load() { + return import("@codemirror/legacy-modes/mode/toml").then(m => legacy(m.toml)) + } + }), + LanguageDescription.of({ + name: "YAML", + alias: ["yml"], + extensions: ["yaml","yml"], + load() { + return import("@codemirror/legacy-modes/mode/yaml").then(m => legacy(m.yaml)) + } + }), +] diff --git a/CoreEditor/src/bridge/native/core.ts b/CoreEditor/src/bridge/native/core.ts new file mode 100644 index 00000000..f9cf866b --- /dev/null +++ b/CoreEditor/src/bridge/native/core.ts @@ -0,0 +1,13 @@ +import { NativeModule } from '../nativeModule'; +import { LineColumnInfo } from '../../modules/selection/types'; + +/** + * @shouldExport true + * @invokePath core + * @bridgeName NativeBridgeCore + */ +export interface NativeModuleCore extends NativeModule { + notifyWindowDidLoad(): void; + notifyTextDidChange(): void; + notifySelectionDidChange({ lineColumn }: { lineColumn: LineColumnInfo }): void; +} diff --git a/CoreEditor/src/bridge/native/preview.ts b/CoreEditor/src/bridge/native/preview.ts new file mode 100644 index 00000000..85bbb237 --- /dev/null +++ b/CoreEditor/src/bridge/native/preview.ts @@ -0,0 +1,12 @@ +import { NativeModule } from '../nativeModule'; +import { PreviewType } from '../../modules/preview'; +import { JSRect } from '../../@types/JSRect'; + +/** + * @shouldExport true + * @invokePath preview + * @bridgeName NativeBridgePreview + */ +export interface NativeModulePreview extends NativeModule { + show({ code, type, rect }: { code: string; type: PreviewType; rect: JSRect }): void; +} diff --git a/CoreEditor/src/bridge/nativeModule.ts b/CoreEditor/src/bridge/nativeModule.ts new file mode 100644 index 00000000..570dbab9 --- /dev/null +++ b/CoreEditor/src/bridge/nativeModule.ts @@ -0,0 +1,79 @@ +import { v4 as UUID } from 'uuid'; +import { isProd, isWebKit } from '../common/utils'; + +/** + * Module used to send message to native. + */ +export interface NativeModule { + name: string; +} + +/** + * Reply sent back from native. + */ +export interface NativeReply { + id: string; + result?: unknown; + error?: string; +} + +const callbacks: Record void> = {}; + +/** + * Create a Proxy for redirecting messages to native by relying on messageHandlers. + * + * @param moduleName name of the native module + * @returns The created proxy + */ +export function createNativeModule(moduleName: string): T { + return new Proxy({} as T, { + get(_target, p): ((args?: Record) => Promise) | undefined { + if (typeof p !== 'string') { + return undefined; + } + + // eslint-disable-next-line compat/compat + return args => new Promise((resolve, reject) => { + // Context is saved to callbacks, + // we will retrieve it in handleNativeReply later + const id = UUID(); + callbacks[id] = (reply: NativeReply) => { + if (reply.error === undefined) { + resolve(reply.result); + } else { + reject(new Error(reply.error)); + } + }; + + const message = { + id, + moduleName, + methodName: p, + parameters: JSON.stringify(args ?? {}), + }; + + // Message is serialized and sent to native here + if (isWebKit) { + window.webkit?.messageHandlers?.bridge?.postMessage(message); + } + + if (!isProd) { + console.log(message); + } + }); + }, + }); +} + +/** + * Native invokes this to reply to a message sent by web. + */ +export function handleNativeReply(reply: NativeReply) { + const callback = callbacks[reply.id] as ((reply: NativeReply) => void) | undefined; + if (callback == undefined) { + return; + } + + callback(reply); + delete callbacks[reply.id]; +} diff --git a/CoreEditor/src/bridge/web/config.ts b/CoreEditor/src/bridge/web/config.ts new file mode 100644 index 00000000..88f30d0a --- /dev/null +++ b/CoreEditor/src/bridge/web/config.ts @@ -0,0 +1,93 @@ +import { WebModule } from '../webModule'; +import { TabKeyBehavior } from '../../modules/indentation'; + +import { + setTheme, + setFontFamily, + setFontSize, + setShowLineNumbers, + setShowActiveLineIndicator, + setShowInvisibles, + setTypewriterMode, + setFocusMode, + setLineWrapping, + setLineHeight, + setDefaultLineBreak, + setTabKeyBehavior, + setIndentUnit, +} from '../../modules/config'; + +/** + * @shouldExport true + * @invokePath config + * @overrideModuleName WebBridgeConfig + */ +export interface WebModuleConfig extends WebModule { + setTheme({ name }: { name: string }): void; + setFontFamily({ fontFamily }: { fontFamily: string }): void; + setFontSize({ fontSize }: { fontSize: number }): void; + setShowLineNumbers({ enabled }: { enabled: boolean } ): void; + setShowActiveLineIndicator({ enabled }: { enabled: boolean }): void; + setShowInvisibles({ enabled }: { enabled: boolean }): void; + setTypewriterMode({ enabled }: { enabled: boolean }): void; + setFocusMode({ enabled }: { enabled: boolean }): void; + setLineWrapping({ enabled }: { enabled: boolean }): void; + setLineHeight({ lineHeight }: { lineHeight: number }): void; + setDefaultLineBreak({ lineBreak }: { lineBreak?: string }): void; + setTabKeyBehavior({ behavior }: { behavior: TabKeyBehavior }): void; + setIndentUnit({ unit }: { unit: string }): void; +} + +export class WebModuleConfigImpl implements WebModuleConfig { + setTheme({ name }: { name: string }): void { + setTheme(name); + } + + setFontFamily({ fontFamily }: { fontFamily: string }): void { + setFontFamily(fontFamily); + } + + setFontSize({ fontSize }: { fontSize: number }): void { + setFontSize(fontSize); + } + + setShowLineNumbers({ enabled }: { enabled: boolean }): void { + setShowLineNumbers(enabled); + } + + setShowActiveLineIndicator({ enabled }: { enabled: boolean }): void { + setShowActiveLineIndicator(enabled); + } + + setShowInvisibles({ enabled }: { enabled: boolean }): void { + setShowInvisibles(enabled); + } + + setTypewriterMode({ enabled }: { enabled: boolean }): void { + setTypewriterMode(enabled); + } + + setFocusMode({ enabled }: { enabled: boolean }): void { + setFocusMode(enabled); + } + + setLineWrapping({ enabled }: { enabled: boolean }): void { + setLineWrapping(enabled); + } + + setLineHeight({ lineHeight }: { lineHeight: number }): void { + setLineHeight(lineHeight); + } + + setDefaultLineBreak({ lineBreak }: { lineBreak?: string }): void { + setDefaultLineBreak(lineBreak); + } + + setTabKeyBehavior({ behavior }: { behavior: TabKeyBehavior }): void { + setTabKeyBehavior(behavior); + } + + setIndentUnit({ unit }: { unit: string }): void { + setIndentUnit(unit); + } +} diff --git a/CoreEditor/src/bridge/web/core.ts b/CoreEditor/src/bridge/web/core.ts new file mode 100644 index 00000000..419139bc --- /dev/null +++ b/CoreEditor/src/bridge/web/core.ts @@ -0,0 +1,32 @@ +import { WebModule } from '../webModule'; +import { resetEditor, clearEditor, getEditorText, markEditorDirty } from '../../core'; + +/** + * @shouldExport true + * @invokePath core + * @overrideModuleName WebBridgeCore + */ +export interface WebModuleCore extends WebModule { + resetEditor({ text }: { text: string }): void; + clearEditor(): void; + getEditorText(): string; + markEditorDirty({ isDirty }: { isDirty: boolean }): void; +} + +export class WebModuleCoreImpl implements WebModuleCore { + resetEditor({ text }: { text: string }): void { + resetEditor(text); + } + + clearEditor(): void { + clearEditor(); + } + + getEditorText(): string { + return getEditorText(); + } + + markEditorDirty({ isDirty }: { isDirty: boolean }): void { + markEditorDirty(isDirty); + } +} diff --git a/CoreEditor/src/bridge/web/format.ts b/CoreEditor/src/bridge/web/format.ts new file mode 100644 index 00000000..67557d01 --- /dev/null +++ b/CoreEditor/src/bridge/web/format.ts @@ -0,0 +1,110 @@ +import { WebModule } from '../webModule'; +import { + EditCommand, + toggleBold, + toggleItalic, + toggleStrikethrough, + toggleHeading, + toggleBullet, + toggleNumbering, + toggleTodo, + toggleBlockquote, + toggleInlineCode, + toggleInlineMath, + insertCodeBlock, + insertMathBlock, + insertHorizontalRule, + performEditCommand, +} from '../../modules/commands'; + +import { insertHyperLink, insertTable } from '../../modules/snippets'; + +/** + * @shouldExport true + * @invokePath format + * @overrideModuleName WebBridgeFormat + */ +export interface WebModuleFormat extends WebModule { + toggleBold(): void; + toggleItalic(): void; + toggleStrikethrough(): void; + toggleHeading({ level }: { level: CodeGen_Int }): void; + toggleBullet(): void; + toggleNumbering(): void; + toggleTodo(): void; + toggleBlockquote(): void; + toggleInlineCode(): void; + toggleInlineMath(): void; + insertCodeBlock(): void; + insertMathBlock(): void; + insertHorizontalRule(): void; + insertHyperLink({ title, url, prefix }: { title: string; url: string; prefix?: string }): void; + insertTable({ columnName, itemName }: { columnName: string; itemName: string }): void; + performEditCommand({ command }: { command: EditCommand }): void; +} + +export class WebModuleFormatImpl implements WebModuleFormat { + toggleBold(): void { + toggleBold(); + } + + toggleItalic(): void { + toggleItalic(); + } + + toggleStrikethrough(): void { + toggleStrikethrough(); + } + + toggleHeading({ level }: { level: CodeGen_Int }): void { + toggleHeading(level); + } + + toggleBullet(): void { + toggleBullet(); + } + + toggleNumbering(): void { + toggleNumbering(); + } + + toggleTodo(): void { + toggleTodo(); + } + + toggleBlockquote(): void { + toggleBlockquote(); + } + + toggleInlineCode(): void { + toggleInlineCode(); + } + + toggleInlineMath(): void { + toggleInlineMath(); + } + + insertCodeBlock(): void { + insertCodeBlock(); + } + + insertMathBlock(): void { + insertMathBlock(); + } + + insertHorizontalRule(): void { + insertHorizontalRule(); + } + + insertHyperLink({ title, url, prefix }: { title: string; url: string; prefix?: string }): void { + insertHyperLink(title, url, prefix); + } + + insertTable({ columnName, itemName }: { columnName: string; itemName: string }): void { + insertTable(columnName, itemName); + } + + performEditCommand({ command }: { command: EditCommand }): void { + performEditCommand(command); + } +} diff --git a/CoreEditor/src/bridge/web/grammarly.ts b/CoreEditor/src/bridge/web/grammarly.ts new file mode 100644 index 00000000..bcabd0b4 --- /dev/null +++ b/CoreEditor/src/bridge/web/grammarly.ts @@ -0,0 +1,27 @@ +import { WebModule } from '../webModule'; +import { connect, disconnect, completeOAuth } from '../../modules/grammarly'; + +/** + * @shouldExport true + * @invokePath grammarly + * @overrideModuleName WebBridgeGrammarly + */ +export interface WebModuleGrammarly extends WebModule { + connect({ clientID, redirectURI }: { clientID: string; redirectURI: string }): void; + disconnect(): void; + completeOAuth({ url }: { url: string }): void; +} + +export class WebModuleGrammarlyImpl implements WebModuleGrammarly { + connect({ clientID, redirectURI }: { clientID: string; redirectURI: string }): void { + connect(clientID, redirectURI); + } + + disconnect(): void { + disconnect(); + } + + completeOAuth({ url }: { url: string }): void { + completeOAuth(url); + } +} diff --git a/CoreEditor/src/bridge/web/history.ts b/CoreEditor/src/bridge/web/history.ts new file mode 100644 index 00000000..7819a9fc --- /dev/null +++ b/CoreEditor/src/bridge/web/history.ts @@ -0,0 +1,32 @@ +import { WebModule } from '../webModule'; +import { undo, redo, canUndo, canRedo } from '../../modules/history'; + +/** + * @shouldExport true + * @invokePath history + * @overrideModuleName WebBridgeHistory + */ +export interface WebModuleHistory extends WebModule { + undo(): void; + redo(): void; + canUndo(): boolean; + canRedo(): boolean; +} + +export class WebModuleHistoryImpl implements WebModuleHistory { + undo(): void { + undo(); + } + + redo(): void { + redo(); + } + + canUndo(): boolean { + return canUndo(); + } + + canRedo(): boolean { + return canRedo(); + } +} diff --git a/CoreEditor/src/bridge/web/lineEndings.ts b/CoreEditor/src/bridge/web/lineEndings.ts new file mode 100644 index 00000000..f37324a4 --- /dev/null +++ b/CoreEditor/src/bridge/web/lineEndings.ts @@ -0,0 +1,22 @@ +import { WebModule } from '../webModule'; +import { LineEndings, getLineEndings, setLineEndings } from '../../modules/lineEndings'; + +/** + * @shouldExport true + * @invokePath lineEndings + * @overrideModuleName WebBridgeLineEndings + */ +export interface WebModuleLineEndings extends WebModule { + getLineEndings(): LineEndings; + setLineEndings({ lineEndings }: { lineEndings: LineEndings }): void; +} + +export class WebModuleLineEndingsImpl implements WebModuleLineEndings { + getLineEndings(): LineEndings { + return getLineEndings(); + } + + setLineEndings({ lineEndings }: { lineEndings: LineEndings }): void { + setLineEndings(lineEndings); + } +} diff --git a/CoreEditor/src/bridge/web/search.ts b/CoreEditor/src/bridge/web/search.ts new file mode 100644 index 00000000..a636b165 --- /dev/null +++ b/CoreEditor/src/bridge/web/search.ts @@ -0,0 +1,62 @@ +import { WebModule } from '../webModule'; +import { + SearchOptions, + setState, + updateQuery, + selectAllOccurrences, + findNext, + findPrevious, + replaceNext, + replaceAll, + numberOfMatches, +} from '../../modules/search'; + +/** + * @shouldExport true + * @invokePath search + * @overrideModuleName WebBridgeSearch + */ +export interface WebModuleSearch extends WebModule { + setState({ enabled }: { enabled: boolean }): void; + updateQuery({ options }: { options: SearchOptions }): CodeGen_Int; + findNext(): void; + findPrevious(): void; + replaceNext(): void; + replaceAll(): void; + selectAllOccurrences(): void; + numberOfMatches(): CodeGen_Int; +} + +export class WebModuleSearchImpl implements WebModuleSearch { + setState({ enabled }: { enabled: boolean }): void { + setState(enabled); + } + + updateQuery({ options }: { options: SearchOptions }): CodeGen_Int { + return updateQuery(options) as CodeGen_Int; + } + + findNext(): void { + findNext(); + } + + findPrevious(): void { + findPrevious(); + } + + replaceNext(): void { + replaceNext(); + } + + replaceAll(): void { + replaceAll(); + } + + selectAllOccurrences(): void { + selectAllOccurrences(); + } + + numberOfMatches(): CodeGen_Int { + return numberOfMatches(); + } +} diff --git a/CoreEditor/src/bridge/web/selection.ts b/CoreEditor/src/bridge/web/selection.ts new file mode 100644 index 00000000..27250c58 --- /dev/null +++ b/CoreEditor/src/bridge/web/selection.ts @@ -0,0 +1,27 @@ +import { WebModule } from '../webModule'; +import { selectedMainText, scrollToSelection, gotoLine } from '../../modules/selection'; + +/** + * @shouldExport true + * @invokePath selection + * @overrideModuleName WebBridgeSelection + */ +export interface WebModuleSelection extends WebModule { + getText(): string; + scrollToSelection(): void; + gotoLine({ lineNumber }: { lineNumber: CodeGen_Int }): void; +} + +export class WebModuleSelectionImpl implements WebModuleSelection { + getText(): string { + return selectedMainText(); + } + + scrollToSelection(): void { + scrollToSelection(); + } + + gotoLine({ lineNumber }: { lineNumber: CodeGen_Int }): void { + gotoLine(lineNumber); + } +} diff --git a/CoreEditor/src/bridge/web/textChecker.ts b/CoreEditor/src/bridge/web/textChecker.ts new file mode 100644 index 00000000..7aabcde9 --- /dev/null +++ b/CoreEditor/src/bridge/web/textChecker.ts @@ -0,0 +1,17 @@ +import { WebModule } from '../webModule'; +import { TextCheckerOptions, update } from '../../modules/textChecker'; + +/** + * @shouldExport true + * @invokePath textChecker + * @overrideModuleName WebBridgeTextChecker + */ +export interface WebModuleTextChecker extends WebModule { + update({ options }: { options: TextCheckerOptions }): void; +} + +export class WebModuleTextCheckerImpl implements WebModuleTextChecker { + update({ options }: { options: TextCheckerOptions }): void { + update(options); + } +} diff --git a/CoreEditor/src/bridge/web/toc.ts b/CoreEditor/src/bridge/web/toc.ts new file mode 100644 index 00000000..089b23f6 --- /dev/null +++ b/CoreEditor/src/bridge/web/toc.ts @@ -0,0 +1,22 @@ +import { WebModule } from '../webModule'; +import { HeadingInfo, getTableOfContents, gotoHeader } from '../../modules/toc'; + +/** + * @shouldExport true + * @invokePath toc + * @overrideModuleName WebBridgeTableOfContents + */ +export interface WebModuleTableOfContents extends WebModule { + getTableOfContents(): HeadingInfo[]; + gotoHeader({ headingInfo }: { headingInfo: HeadingInfo }): void; +} + +export class WebModuleTableOfContentsImpl implements WebModuleTableOfContents { + getTableOfContents(): HeadingInfo[] { + return getTableOfContents(); + } + + gotoHeader({ headingInfo }: { headingInfo: HeadingInfo }): void { + gotoHeader(headingInfo); + } +} diff --git a/CoreEditor/src/bridge/webModule.ts b/CoreEditor/src/bridge/webModule.ts new file mode 100644 index 00000000..c9871430 --- /dev/null +++ b/CoreEditor/src/bridge/webModule.ts @@ -0,0 +1,8 @@ +/** + * Module used to invoke web from native. + * + * Implementaions of WebModule should be wrappers to functions in modules. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WebModule {} diff --git a/CoreEditor/src/common/store.ts b/CoreEditor/src/common/store.ts new file mode 100644 index 00000000..498a7e6e --- /dev/null +++ b/CoreEditor/src/common/store.ts @@ -0,0 +1,8 @@ +import { Compartment } from '@codemirror/state'; +import StyleSheets from '../styling/config'; + +export const editedState = { isDirty: false }; +export const selectionState = { hasSelection: false }; + +export const styleSheets: StyleSheets = {}; +export const clickableLinks: Compartment[] = []; diff --git a/CoreEditor/src/common/utils.ts b/CoreEditor/src/common/utils.ts new file mode 100644 index 00000000..d0979666 --- /dev/null +++ b/CoreEditor/src/common/utils.ts @@ -0,0 +1,2 @@ +export const isProd = import.meta.env.PROD; +export const isWebKit = typeof window.webkit === 'object'; diff --git a/CoreEditor/src/config.ts b/CoreEditor/src/config.ts new file mode 100644 index 00000000..ed70ea45 --- /dev/null +++ b/CoreEditor/src/config.ts @@ -0,0 +1,53 @@ +import { Compartment } from '@codemirror/state'; + +/** + * @shouldExport true + * @overrideModuleName EditorLocalizable + */ +export interface Localizable { + // CodeMirror + controlCharacter: string; + foldedLines: string; + unfoldedLines: string; + foldedCode: string; + unfold: string; + foldLine: string; + unfoldLine: string; + // Others + previewButtonTitle: string; +} + +/** + * @shouldExport true + * @overrideModuleName EditorConfig + */ +export interface Config { + text: string; + theme: string; + fontFamily: string; + fontSize: number; + showLineNumbers: boolean; + showActiveLineIndicator: boolean; + showInvisibles: boolean; + typewriterMode: boolean; + focusMode: boolean; + lineWrapping: boolean; + lineHeight: number; + defaultLineBreak?: string; + tabKeyBehavior?: CodeGen_Int; + indentUnit?: string; + localizable?: Localizable; +} + +/** + * Dynamic configurations that can be reconfigured. + */ +export interface Dynamics { + theme: Compartment; + gutters?: Compartment; + invisibles?: Compartment; + activeLine?: Compartment; + lineWrapping?: Compartment; + lineEndings?: Compartment; + indentUnit?: Compartment; +} diff --git a/CoreEditor/src/core.ts b/CoreEditor/src/core.ts new file mode 100644 index 00000000..96b29392 --- /dev/null +++ b/CoreEditor/src/core.ts @@ -0,0 +1,67 @@ +import { EditorView } from '@codemirror/view'; +import { extensions } from './extensions'; +import { editedState } from './common/store'; + +import * as styling from './styling/config'; +import * as lineEndings from './modules/lineEndings'; + +/** + * Reset the editor to the initial state. + * + * @param doc Initial content + */ +export function resetEditor(doc: string) { + // eslint-disable-next-line + if (window.editor && window.editor.destroy) { + window.editor.destroy(); + } + + const editor = new EditorView({ + doc, + parent: document.querySelector('#editor') ?? document.body, + extensions: extensions({ + lineBreak: lineEndings.getLineBreak(doc, window.config.defaultLineBreak), + }), + }); + + editor.focus(); + window.editor = editor; + + // Recofigure, window.config might have changed + styling.setUp(window.config); + + // After calling editor.focus(), the selection is set to [Ln 1, Col 1] + window.nativeModules.core.notifySelectionDidChange({ + lineColumn: { line: 1 as CodeGen_Int, column: 1 as CodeGen_Int, length: 0 as CodeGen_Int }, + }); +} + +/** + * Clear the editor, set the content to empty. + */ +export function clearEditor() { + const editor = window.editor; + editor.dispatch({ + changes: { from: 0, to: editor.state.doc.length, insert: '' }, + }); +} + +export function getEditorText() { + const state = window.editor.state; + if (state.lineBreak === '\n') { + return state.doc.toString(); + } + + // It looks like state.doc.toString() always uses LF instead of state.lineBreak + const lines: string[] = []; + for (let index = 1; index <= state.doc.lines; ++index) { + lines.push(state.doc.line(index).text); + } + + // Re-join with specified line break, might be CRLF for example + return lines.join(state.lineBreak); +} + +export function markEditorDirty(isDirty: boolean) { + editedState.isDirty = isDirty; +} diff --git a/CoreEditor/src/dom/events/index.ts b/CoreEditor/src/dom/events/index.ts new file mode 100644 index 00000000..8eca6641 --- /dev/null +++ b/CoreEditor/src/dom/events/index.ts @@ -0,0 +1,32 @@ +import isMetaKey from './isMetaKey'; + +import * as grammarly from '../../modules/grammarly'; +import * as selection from '../../modules/selection'; +import * as link from '../../styling/nodes/link'; + +export function startObserving() { + document.addEventListener('click', event => { + grammarly.centerActiveDialog(); + selection.selectWholeLineIfNeeded(event); + }); + + document.addEventListener('keydown', event => { + if (isMetaKey(event)) { + link.startClickable(); + } else { + link.stopClickable(); + } + }); + + document.addEventListener('keyup', () => { + link.stopClickable(); + }); + + document.addEventListener('mousedown', event => { + link.handleMouseDown(event); + }, true); + + document.addEventListener('mouseup', event => { + link.handleMouseUp(event); + }, true); +} diff --git a/CoreEditor/src/dom/events/isMetaKey.ts b/CoreEditor/src/dom/events/isMetaKey.ts new file mode 100644 index 00000000..af65ae9b --- /dev/null +++ b/CoreEditor/src/dom/events/isMetaKey.ts @@ -0,0 +1,3 @@ +export default function isMetaKey(event: KeyboardEvent) { + return event.key === 'Meta'; +} diff --git a/CoreEditor/src/dom/views/index.ts b/CoreEditor/src/dom/views/index.ts new file mode 100644 index 00000000..c3f8bde1 --- /dev/null +++ b/CoreEditor/src/dom/views/index.ts @@ -0,0 +1,40 @@ +import { WidgetView } from './types'; +import { PreviewType } from '../../modules/preview'; + +/** + * Widget used to show a [preview] button after some contents, such as mermaid diagrams. + */ +export class PreviewWidget extends WidgetView { + constructor(private readonly code: string, private readonly type: PreviewType, pos: number) { + super(); + this.pos = pos; + } + + toDOM() { + const span = document.createElement('span'); + span.style.paddingLeft = '4px'; + + // Only include paddingRight for katext because it can be inline + if (this.type == PreviewType.katex) { + span.style.paddingRight = '4px'; + } + + const button = span.appendChild(document.createElement('span')); + button.setAttribute('data-code', this.code); + button.setAttribute('data-type', this.type); + button.setAttribute('data-pos', `${this.pos}`); + + button.innerText = `[${window.config.localizable?.previewButtonTitle}]`; + button.className = 'cm-md-previewButton'; + + return span; + } + + eq(other: PreviewWidget) { + return other.code === this.code && other.type === this.type && other.pos === this.pos; + } + + ignoreEvent() { + return false; + } +} diff --git a/CoreEditor/src/dom/views/types.ts b/CoreEditor/src/dom/views/types.ts new file mode 100644 index 00000000..9a6c5585 --- /dev/null +++ b/CoreEditor/src/dom/views/types.ts @@ -0,0 +1,8 @@ +import { WidgetType } from '@codemirror/view'; + +/** + * Extend WidgetType with a pos to indicate where to draw. + */ +export abstract class WidgetView extends WidgetType { + pos: number; +} diff --git a/CoreEditor/src/extensions.ts b/CoreEditor/src/extensions.ts new file mode 100644 index 00000000..2cc420b8 --- /dev/null +++ b/CoreEditor/src/extensions.ts @@ -0,0 +1,110 @@ +import { + EditorView, + highlightSpecialChars, + drawSelection, + dropCursor, + rectangularSelection, + crosshairCursor, + highlightActiveLine, + highlightActiveLineGutter, + keymap, +} from '@codemirror/view'; + +import { Compartment, EditorState } from '@codemirror/state'; +import { indentUnit as indentUnitFacet, indentOnInput, bracketMatching, foldKeymap } from '@codemirror/language'; +import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'; +import { highlightSelectionMatches, search } from '@codemirror/search'; +import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; +import { markdown, markdownLanguage } from './@vendor/lang-markdown'; +import { languages } from './@vendor/language-data'; + +import { loadTheme } from './styling/themes'; +import { markdownExtensions, renderExtensions, actionExtensions } from './styling/markdown'; +import { gutterExtensions } from './styling/nodes/gutter'; +import { invisiblesExtension } from './styling/nodes/invisible'; + +import { localizePhrases } from './modules/localization'; +import { indentationKeymap } from './modules/indentation'; +import { observeChanges, interceptInputs } from './modules/input'; + +const theme = new Compartment; +const gutters = new Compartment; +const invisibles = new Compartment; +const activeLine = new Compartment; +const lineWrapping = new Compartment; +const lineEndings = new Compartment; +const indentUnit = new Compartment; + +window.dynamics = { + theme, + gutters, + invisibles, + activeLine, + lineWrapping, + lineEndings, + indentUnit, +}; + +// Make this a function because some resources (e.g., phrases) require lazy loading +export function extensions(options: { lineBreak?: string }) { + return [ + // Basic + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentUnit.of(window.config.indentUnit !== undefined ? indentUnitFacet.of(window.config.indentUnit) : []), + indentOnInput(), + bracketMatching(), + closeBrackets(), + rectangularSelection(), + crosshairCursor(), + activeLine.of(window.config.showActiveLineIndicator ? highlightActiveLine() : []), + highlightActiveLineGutter(), + highlightSelectionMatches(), + localizePhrases(), + + // Line behaviors + lineEndings.of(options.lineBreak !== undefined ? EditorState.lineSeparator.of(options.lineBreak) : []), + gutters.of(window.config.showLineNumbers ? gutterExtensions : []), + lineWrapping.of(window.config.lineWrapping ? EditorView.lineWrapping : []), + + // Search + search({ + createPanel() { + class DummyPanel { dom = document.createElement('span'); } + return new DummyPanel(); + }, + }), + + // Keymap + keymap.of([ + // We use cmd-i to toggle italic + ...defaultKeymap.filter(keymap => keymap.key !== 'Mod-i'), + ...historyKeymap, + ...closeBracketsKeymap, + ...foldKeymap, + // By default CodeMirror disables tab (character) insertion (https://codemirror.net/examples/tab/), + // however, MarkEdit runs on a WebView instead of browsers, we do want to bind the tab key. + ...indentationKeymap, + ]), + + // Markdown + markdown({ + base: markdownLanguage, + codeLanguages: languages, + extensions: markdownExtensions, + }), + + // Styling + theme.of(loadTheme(window.config.theme)), + invisibles.of(window.config.showInvisibles ? invisiblesExtension : []), + renderExtensions, + actionExtensions, + + // Input handling + interceptInputs(), + observeChanges(), + ]; +} diff --git a/CoreEditor/src/modules/commands/index.ts b/CoreEditor/src/modules/commands/index.ts new file mode 100644 index 00000000..c11bd356 --- /dev/null +++ b/CoreEditor/src/modules/commands/index.ts @@ -0,0 +1,101 @@ +import toggleBlockWithMarks from './toggleBlockWithMarks'; +import toggleLineLeadingMark from './toggleLineLeadingMark'; +import toggleListStyle from './toggleListStyle'; +import replaceSelections from './replaceSelections'; +import insertBlockWithMarks from './insertBlockWithMarks'; + +import { EditCommand } from './types'; +import * as commands from '@codemirror/commands'; + +export function toggleBold() { + toggleBlockWithMarks('**', '**'); +} + +export function toggleItalic() { + toggleBlockWithMarks('*', '*'); +} + +export function toggleStrikethrough() { + toggleBlockWithMarks('~~', '~~'); +} + +export function toggleInlineCode() { + toggleBlockWithMarks('`', '`'); +} + +export function toggleInlineMath() { + toggleBlockWithMarks('$', '$'); +} + +export function toggleHeading(level: number) { + toggleLineLeadingMark('#', level); +} + +export function toggleBlockquote() { + toggleLineLeadingMark('>', 1); +} + +/** + * Toggle list markers like "- Item", "* Item", or "+ Item". + */ +export function toggleBullet() { + toggleListStyle(() => /^( *[-*+] )/, (_, suggested) => suggested ?? '-'); +} + +/** + * Toggle list markers like "1. Item". + */ +export function toggleNumbering() { + toggleListStyle(index => new RegExp(`^( *${index + 1}\\. )`), index => `${index + 1}.`); +} + +/** + * Toggle list markers like "- [ ] Todo" or "- [x] Done". + */ +export function toggleTodo() { + toggleListStyle( + () => /^( *- +\[[ xX]\] +)/, + () => '- [ ]', + line => { + if (!/- +\[ \]/.test(line)) { + return undefined; + } + + // Change "- [ ] Item" to "- [x] Item" + return line.replace(/(- +\[) (\].*)/, '$1x$2'); + } + ); +} + +export function insertHorizontalRule() { + const br = window.editor.state.lineBreak; + replaceSelections(`${br}---${br}`); +} + +export function insertCodeBlock() { + insertBlockWithMarks('```'); +} + +export function insertMathBlock() { + insertBlockWithMarks('$$'); +} + +/** + * Wrapper to a series of commands in CodeMirror, + * we need this because we want to show them in the application. + */ +export function performEditCommand(command: EditCommand) { + const editor = window.editor; + switch (command) { + case EditCommand.indentLess: commands.indentLess(editor); break; + case EditCommand.indentMore: commands.indentMore(editor); break; + case EditCommand.selectLine: commands.selectLine(editor); break; + case EditCommand.moveLineUp: commands.moveLineUp(editor); break; + case EditCommand.moveLineDown: commands.moveLineDown(editor); break; + case EditCommand.copyLineUp: commands.copyLineUp(editor); break; + case EditCommand.copyLineDown: commands.copyLineDown(editor); break; + default: break; + } +} + +export type { EditCommand }; diff --git a/CoreEditor/src/modules/commands/insertBlockWithMarks.ts b/CoreEditor/src/modules/commands/insertBlockWithMarks.ts new file mode 100644 index 00000000..bdb515dc --- /dev/null +++ b/CoreEditor/src/modules/commands/insertBlockWithMarks.ts @@ -0,0 +1,9 @@ +import replaceSelections from './replaceSelections'; + +/** + * Generally used to insert blocks like fenced code. + */ +export default function insertBlockWithMarks(marks: string) { + const br = window.editor.state.lineBreak; + replaceSelections(`${marks}${br}${br}${marks}`, br.length + marks.length); +} diff --git a/CoreEditor/src/modules/commands/removeListMarkers.ts b/CoreEditor/src/modules/commands/removeListMarkers.ts new file mode 100644 index 00000000..85294207 --- /dev/null +++ b/CoreEditor/src/modules/commands/removeListMarkers.ts @@ -0,0 +1,6 @@ +/** + * Remove existing list marker from text, e.g., changing "- Item" to "Item". + */ +export default function removeListMarkers(text: string): string { + return text.replace(/^(- +\[[ xX]\] )|^([-*+] )|^(\d+\. )/, ''); +} diff --git a/CoreEditor/src/modules/commands/replaceSelections.ts b/CoreEditor/src/modules/commands/replaceSelections.ts new file mode 100644 index 00000000..995dcaa5 --- /dev/null +++ b/CoreEditor/src/modules/commands/replaceSelections.ts @@ -0,0 +1,15 @@ +import { EditorSelection } from '@codemirror/state'; + +export default function replaceSelections(replacement: string, selectionMoveBack = 0) { + const editor = window.editor; + const updates = editor.state.changeByRange(({ from, to }) => { + return { + range: EditorSelection.cursor(from + replacement.length - selectionMoveBack), + changes: { + from, to, insert: replacement, + }, + }; + }); + + editor.dispatch(updates); +} diff --git a/CoreEditor/src/modules/commands/toggleBlockWithMarks.ts b/CoreEditor/src/modules/commands/toggleBlockWithMarks.ts new file mode 100644 index 00000000..64a81b7a --- /dev/null +++ b/CoreEditor/src/modules/commands/toggleBlockWithMarks.ts @@ -0,0 +1,51 @@ +import { EditorSelection } from '@codemirror/state'; + +/** + * Toggle selection with mark pairs, such as **bold**, _italic_. + * + * @param leftMark Mark on the left side + * @param rightMark Mark on the right side + */ +export default function toggleBlockWithMarks(leftMark: string, rightMark: string) { + const editor = window.editor; + const doc = editor.state.doc; + + // Take care of all updates and merge them into a single one + const updates = editor.state.changeByRange(({ from, to }) => { + const startPos = from; + const endPos = to; + const selectedText = doc.sliceString(from, to); + + const startTestPos = startPos - leftMark.length; + const endTestPos = endPos + rightMark.length; + + let matched = false; + let newPos = 0; + + if (startTestPos >= 0 && endTestPos <= doc.length) { + const leftTest = doc.sliceString(startTestPos, startTestPos + leftMark.length); + const rightTest = doc.sliceString(endTestPos - rightMark.length, endTestPos); + matched = leftTest === leftMark && rightTest === rightMark; + } + + if (matched) { + newPos = startTestPos; + return { + range: EditorSelection.range(newPos, newPos + selectedText.length), + changes: { + from: startTestPos, to: endTestPos, insert: selectedText, + }, + }; + } else { + newPos = startPos + leftMark.length; + return { + range: EditorSelection.range(newPos, newPos + selectedText.length), + changes: { + from: startPos, to: endPos, insert: `${leftMark}${selectedText}${rightMark}`, + }, + }; + } + }); + + editor.dispatch(updates); +} diff --git a/CoreEditor/src/modules/commands/toggleLineLeadingMark.ts b/CoreEditor/src/modules/commands/toggleLineLeadingMark.ts new file mode 100644 index 00000000..baca18a8 --- /dev/null +++ b/CoreEditor/src/modules/commands/toggleLineLeadingMark.ts @@ -0,0 +1,63 @@ +import { EditorSelection } from '@codemirror/state'; +import * as selection from '../selection'; + +/** + * Toggle the level of given leading mark, e.g., headings with "#". + * + * @param mark The character, such as "#" + * @param level The length of leading marks + */ +export default function toggleLineLeadingMark(mark: string, level: number) { + const editor = window.editor; + const lines = selection.reversedLines(); + const regex = new RegExp(`^(${mark}+)( +)`); + + // Remove all marks only if all lines have exactly the destination level + const removeMarks = !lines.some(line => { + const match = line.text.match(regex); + return !match || match[1].length !== level; + }); + + // Iterate multiple lines reversely + // + // Ideally we should be using state.changeByRange, + // but it doesn't work very well with multi-selection x multi-line mixed, + // here we want to treat each line as an independent update. + // + // The downside of this approach is that updates are also reversed. + for (const line of lines) { + const replace = (replacement: string) => { + editor.dispatch({ + changes: { + from: line.from, to: line.to, insert: replacement, + }, + }); + }; + + const text = line.text; + const match = text.match(regex); + const repeatedMarks = mark.repeat(level); + + if (match) { + if (match[1].length === level) { + if (removeMarks) { + // E.g., remove the leading "##" + replace(text.substring(match[0].length)); + } + } else { + // E.g., change "##" to "###" + replace(`${repeatedMarks}${text.substring(match[1].length)}`); + } + } else if (text.length > 0 || lines.length === 1) { + // E.g., change "hello" to "## hello" + replace(`${repeatedMarks} ${text}`); + + // Place cursor to the end for empty lines + if (text.length === 0) { + editor.dispatch({ + selection: EditorSelection.cursor(line.to + repeatedMarks.length + 1), + }); + } + } + } +} diff --git a/CoreEditor/src/modules/commands/toggleListStyle.ts b/CoreEditor/src/modules/commands/toggleListStyle.ts new file mode 100644 index 00000000..06353360 --- /dev/null +++ b/CoreEditor/src/modules/commands/toggleListStyle.ts @@ -0,0 +1,102 @@ +import { EditorSelection, Line } from '@codemirror/state'; +import removeListMarkers from './removeListMarkers'; +import * as selection from '../selection'; + +/** + * Toggle list style by providing pattern and customizable callbacks. + * + * @param matches RegExp that matches a given line + * @param createMark Function to create the mark + * @param toggleMark Function to toggle the mark + */ +export default function toggleListStyle( + matches: (index: number) => RegExp, + createMark: (index: number, suggested?: string) => string, + toggleMark?: (line: string) => string | undefined +) { + const editor = window.editor; + const selectedRanges = selection.reversedRanges(); + + // Iterate multiple lines reversely + // + // Ideally we should be using state.changeByRange, + // but it doesn't work very well with multi-selection x multi-line mixed, + // here we want to treat each line as an independent update. + // + // The downside of this approach is that updates are also reversed. + for (const { from, to } of selectedRanges) { + const lines = selection.linesWithRange(from, to); + const literate = (callback: (match: RegExpMatchArray | null, empty: boolean, line: Line, index: number) => void) => { + let skipped = 0; + let index = 0; + + for (; index < lines.length; ++index) { + const line = lines[index]; + const regex = matches(index - skipped); + const empty = lines.length > 1 && line.text.length === 0; + callback(line.text.match(regex), empty, line, index - skipped); + + // Indices for empty lines are skipped + if (empty) { + ++skipped; + } + } + }; + + let removeMarks = true; + let suggestedMark: string | undefined = undefined; + + // We are doing two passes, the first one detects the existing structure + literate((match, empty) => { + if (match) { + suggestedMark = match[0].substring(0, 1); + } else if (!empty) { + removeMarks = false; + } + }); + + // The second pass, figures out the actual updates + const updates: string[] = []; + literate((match, _, line, index) => { + const text = line.text; + if (match) { + if (removeMarks) { + const toggled = toggleMark ? toggleMark(text) : undefined; + if (toggled !== undefined) { + // Toggle between styles + updates.push(toggled); + } else { + // Remove the marker directly + updates.push(text.substring(match[0].length)); + } + } else { + // Not changed + updates.push(text); + } + } else if (text.length > 0 || lines.length === 1) { + // Insert list markers to the front + updates.push(`${createMark(index, suggestedMark)} ${removeListMarkers(text)}`); + } else { + // Not changed + updates.push(text); + } + }); + + const startIndex = lines[0].from; + const endIndex = lines.reverse()[0].to; + + // Dispatch all changes altogether + editor.dispatch({ + changes: { + from: startIndex, to: endIndex, insert: updates.join(editor.state.lineBreak), + }, + }); + + // Place cursor to the end for empty lines + if (lines.length === 1 && lines[0].text.length === 0) { + editor.dispatch({ + selection: EditorSelection.cursor(endIndex + updates[0].length), + }); + } + } +} diff --git a/CoreEditor/src/modules/commands/types.ts b/CoreEditor/src/modules/commands/types.ts new file mode 100644 index 00000000..807d73ce --- /dev/null +++ b/CoreEditor/src/modules/commands/types.ts @@ -0,0 +1,9 @@ +export enum EditCommand { + indentLess = 'indentLess', + indentMore = 'indentMore', + selectLine = 'selectLine', + moveLineUp = 'moveLineUp', + moveLineDown = 'moveLineDown', + copyLineUp = 'copyLineUp', + copyLineDown = 'copyLineDown', +} diff --git a/CoreEditor/src/modules/config/index.ts b/CoreEditor/src/modules/config/index.ts new file mode 100644 index 00000000..583bf20b --- /dev/null +++ b/CoreEditor/src/modules/config/index.ts @@ -0,0 +1,79 @@ +import { EditorView } from '@codemirror/view'; +import { indentUnit } from '@codemirror/language'; +import { TabKeyBehavior } from '../indentation'; +import { scrollToSelection } from '../selection'; +import { selectionState } from '../../common/store'; +import { loadTheme } from '../../styling/themes'; +import * as styling from '../../styling/config'; + +export function setTheme(name: string) { + window.config.theme = name; + styling.setTheme(loadTheme(name)); +} + +export function setFontFamily(fontFamily: string) { + window.config.fontFamily = fontFamily; + styling.setFontFamily(fontFamily); +} + +export function setFontSize(fontSize: number) { + window.config.fontSize = fontSize; + styling.setFontSize(fontSize); +} + +export function setShowLineNumbers(enabled: boolean) { + window.config.showLineNumbers = enabled; + styling.setShowLineNumbers(enabled); +} + +export function setShowActiveLineIndicator(enabled: boolean) { + window.config.showActiveLineIndicator = enabled; + styling.setShowActiveLineIndicator(enabled && !selectionState.hasSelection); +} + +export function setShowInvisibles(enabled: boolean) { + window.config.showInvisibles = enabled; + styling.setShowInvisibles(enabled); +} + +export function setTypewriterMode(enabled: boolean) { + window.config.typewriterMode = enabled; + + if (enabled) { + scrollToSelection('center'); + } +} + +export function setFocusMode(enabled: boolean) { + window.config.focusMode = enabled; + styling.setFocusMode(enabled); +} + +export function setLineWrapping(enabled: boolean) { + window.config.lineWrapping = enabled; + styling.setLineWrapping(enabled); +} + +export function setLineHeight(lineHeight: number) { + window.config.lineHeight = lineHeight; + styling.setLineHeight(lineHeight); +} + +export function setDefaultLineBreak(lineBreak?: string) { + window.config.defaultLineBreak = lineBreak; +} + +export function setIndentUnit(unit: string) { + window.config.indentUnit = unit; + + const editor = window.editor as EditorView | null; + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.indentUnit?.reconfigure(indentUnit.of(unit)), + }); + } +} + +export function setTabKeyBehavior(behavior: TabKeyBehavior) { + window.config.tabKeyBehavior = behavior as CodeGen_Int; +} diff --git a/CoreEditor/src/modules/grammarly/index.css b/CoreEditor/src/modules/grammarly/index.css new file mode 100644 index 00000000..38c10d18 --- /dev/null +++ b/CoreEditor/src/modules/grammarly/index.css @@ -0,0 +1,5 @@ +grammarly-editor-plugin { + width: 100% !important; + --grammarly-button-position-top: 6px; + --grammarly-button-position-right: 6px; +} diff --git a/CoreEditor/src/modules/grammarly/index.ts b/CoreEditor/src/modules/grammarly/index.ts new file mode 100644 index 00000000..845625a2 --- /dev/null +++ b/CoreEditor/src/modules/grammarly/index.ts @@ -0,0 +1,92 @@ +import * as Grammarly from '@grammarly/editor-sdk'; + +let grammarly: Grammarly.EditorSDK | undefined = undefined; +let plugin: Grammarly.GrammarlyEditorPluginElement | undefined = undefined; + +/** + * Connect to a Grammarly instance: https://developer.grammarly.com/. + */ +export function connect(clientID: string, redirectURI: string) { + const element = document.querySelector('div[contenteditable=true]'); + if (!(element instanceof HTMLElement)) { + console.error('Failed to retrieve contentEditable from the DOM tree'); + return; + } + + const setUp = (sdk: Grammarly.EditorSDK) => { + plugin = sdk.addPlugin(element, { + activation: 'immediate', + oauthRedirectUri: redirectURI, + }); + + // Don't let Grammarly steal the focus, typing is more important + window.editor.focus(); + grammarly = sdk; + }; + + if (grammarly === undefined) { + (async() => { + try { + setUp(await Grammarly.init(clientID)); + } catch (error) { + console.error(error); + } + })(); + } else { + // Unfornately, plugin.connect() won't bring the plugin back, + // we have to call addPlugin again + setUp(grammarly); + } +} + +export function disconnect() { + if (plugin !== undefined) { + plugin.disconnect(); + } +} + +/** + * Native code redirects openURL to finish OAuth. + * + * @param url URL that contains auth information + */ +export function completeOAuth(url: string) { + if (grammarly !== undefined) { + grammarly.handleOAuthCallback(url); + } else { + console.error('Grammarly is not initialized yet'); + } +} + +/** + * To make sure the suggestion dialog is always (kind of) visible. + */ +export function centerActiveDialog() { + setTimeout(() => { + const shadowRoot = (document.querySelector('grammarly-editor-plugin') as HTMLElement | null)?.shadowRoot; + const activeDialog = shadowRoot?.querySelector("div[role='dialog']") as HTMLElement | undefined | null; + if (activeDialog === null || activeDialog === undefined) { + return; + } + + const windowHeight = window.innerHeight; + const dialogHeight = activeDialog.clientHeight; + const padding = 20; + + const minY = activeDialog.offsetTop; + const maxY = minY + dialogHeight; + + // In a good shape, we only center the dialog when it's nearly cut + if ((minY >= padding) && (maxY <= windowHeight - padding)) { + return; + } + + const centerTop = (dialogHeight - windowHeight) * 0.5; + const scrollTop = window.editor.scrollDOM.scrollTop; + + window.editor.scrollDOM.scrollTo({ + top: minY + centerTop + scrollTop, + behavior: 'smooth', + }); + }, 500); +} diff --git a/CoreEditor/src/modules/history/index.ts b/CoreEditor/src/modules/history/index.ts new file mode 100644 index 00000000..5eeb9ffc --- /dev/null +++ b/CoreEditor/src/modules/history/index.ts @@ -0,0 +1,43 @@ +import { ChangeSet } from '@codemirror/state'; +import { historyField, undo as undoCommand, redo as redoCommand } from '@codemirror/commands'; + +// Weirdly, CodeMirror doesn't expose HistoryState as a public interface, +// define it here and leverage tests to ensure its existence. +interface HistoryState { + done?: HistoryEvent[]; + undone?: HistoryEvent[]; +} + +interface HistoryEvent { + changes?: ChangeSet; +} + +/** + * In the client codebase, we need to bind the native undo to this function. + */ +export function undo() { + undoCommand(window.editor); +} + +/** + * In the client codebase, we need to bind the native redo to this function. + */ +export function redo() { + redoCommand(window.editor); +} + +export function canUndo() { + const editor = window.editor; + const history = editor.state.field(historyField) as HistoryState; + return filterHistory(history.done).length > 0; +} + +export function canRedo() { + const editor = window.editor; + const history = editor.state.field(historyField) as HistoryState; + return filterHistory(history.undone).length > 0; +} + +function filterHistory(events?: HistoryEvent[]) { + return events?.filter(event => event.changes !== undefined) ?? []; +} diff --git a/CoreEditor/src/modules/indentation/index.ts b/CoreEditor/src/modules/indentation/index.ts new file mode 100644 index 00000000..7154d82d --- /dev/null +++ b/CoreEditor/src/modules/indentation/index.ts @@ -0,0 +1,26 @@ +import { KeyBinding } from '@codemirror/view'; +import { insertTab } from '@codemirror/commands'; +import { TabKeyBehavior } from './types'; +import replaceSelections from '../commands/replaceSelections'; + +/** + * Customized tab key behavior. + */ +export const indentationKeymap: KeyBinding[] = [{ + key: 'Tab', + preventDefault: true, + run: ({ state, dispatch }) => { + switch (window.config.tabKeyBehavior) { + case TabKeyBehavior.insertTwoSpaces: + replaceSelections(' '); + return true; + case TabKeyBehavior.insertFourSpaces: + replaceSelections(' '); + return true; + default: + return insertTab({ state, dispatch }); + } + }, +}]; + +export type { TabKeyBehavior }; diff --git a/CoreEditor/src/modules/indentation/types.ts b/CoreEditor/src/modules/indentation/types.ts new file mode 100644 index 00000000..7b1285b6 --- /dev/null +++ b/CoreEditor/src/modules/indentation/types.ts @@ -0,0 +1,5 @@ +export enum TabKeyBehavior { + insertTab = 0, + insertTwoSpaces = 1, + insertFourSpaces = 2, +} diff --git a/CoreEditor/src/modules/input/index.ts b/CoreEditor/src/modules/input/index.ts new file mode 100644 index 00000000..6560ae4f --- /dev/null +++ b/CoreEditor/src/modules/input/index.ts @@ -0,0 +1,69 @@ +import { EditorView } from '@codemirror/view'; +import { editedState, selectionState } from '../../common/store'; +import { scrollCaretToVisible, scrollToSelection } from '../../modules/selection'; +import { selectedLineColumn } from '../selection/selectedLineColumn'; +import { setShowActiveLineIndicator } from '../../styling/config'; + +import selectedRange from '../selection/selectedRanges'; +import wrapBlock from './wrapBlock'; + +/** + * Give us an opportunity to intercept user inputs. + * + * @returns True to ignore the default behavior + */ +export function interceptInputs() { + const marksToWrap = ['`', '*', '_', '~', '$']; + + return EditorView.inputHandler.of((editor, _from, _to, insert) => { + // E.g., wrap "selection" as "*selection*" + if (marksToWrap.includes(insert)) { + return wrapBlock(insert, editor); + } + + // Fallback to default behavior + return false; + }); +} + +/** + * Returns an extension that handles all the editor changes. + */ +export function observeChanges() { + return EditorView.updateListener.of(update => { + // We only notify changes when the editor is not dirty (all changes are saved) + if (update.docChanged && !editedState.isDirty) { + // It would be great if we could also provide the updated text here, + // but it's time-consuming for large payload, + // we want to be responsive for every key stroke. + window.nativeModules.core.notifyTextDidChange(); + editedState.isDirty = true; + } + + if (update.selectionSet) { + const lineColumn = selectedLineColumn(); + window.nativeModules.core.notifySelectionDidChange({ lineColumn }); + + const hasSelection = selectedRange().some(range => !range.empty); + const updateActiveLine = selectionState.hasSelection !== hasSelection; + selectionState.hasSelection = hasSelection; + + // Clear active line background when there's selection, + // it makes the selection easier to read. + if (updateActiveLine) { + setShowActiveLineIndicator(!hasSelection && window.config.showActiveLineIndicator); + } + + if (!hasSelection) { + // Make sure the main selection is always centered for typewriter mode + if (window.config.typewriterMode) { + scrollToSelection('center'); + } else { + // We need this because we have different line height for headings, + // CodeMirror doesn't by default fix the offset issue. + scrollCaretToVisible(); + } + } + } + }); +} diff --git a/CoreEditor/src/modules/input/wrapBlock.ts b/CoreEditor/src/modules/input/wrapBlock.ts new file mode 100644 index 00000000..00d69144 --- /dev/null +++ b/CoreEditor/src/modules/input/wrapBlock.ts @@ -0,0 +1,25 @@ +import { EditorSelection } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; + +/** + * Wrap selected blocks with a pair of mark. + * + * @param mark The mark, e.g., "*" + */ +export default function wrapBlock(mark: string, editor: EditorView) { + const doc = editor.state.doc; + editor.dispatch(editor.state.changeByRange(({ from, to }) => { + const selection = doc.sliceString(from, to); + const replacement = from === to ? mark : `${mark}${selection}${mark}`; + const newPos = from + mark.length; + return { + range: EditorSelection.range(newPos, newPos + selection.length), + changes: { + from, to, insert: replacement, + }, + }; + })); + + // Intercepted, default behavior is ignored + return true; +} diff --git a/CoreEditor/src/modules/lineEndings/index.ts b/CoreEditor/src/modules/lineEndings/index.ts new file mode 100644 index 00000000..78c7eb81 --- /dev/null +++ b/CoreEditor/src/modules/lineEndings/index.ts @@ -0,0 +1,70 @@ +import { EditorState } from '@codemirror/state'; +import { LineEndings } from './types'; + +const CHARS = { + LF: '\n', + CRLF: '\r\n', + CR: '\r', +}; + +export function getLineEndings() { + const editor = window.editor; + const lineBreak = editor.state.lineBreak; + + if (lineBreak === CHARS.LF) { + return LineEndings.LF; + } else if (lineBreak === CHARS.CRLF) { + return LineEndings.CRLF; + } else if (lineBreak === CHARS.CR) { + return LineEndings.CR; + } else { + return LineEndings.Unspecified; + } +} + +export function setLineEndings(lineEndings: LineEndings) { + const extension = window.dynamics.lineEndings; + if (extension === undefined) { + return; + } + + const lineBreak = (() => { + if (lineEndings === LineEndings.CRLF) { + return CHARS.CRLF; + } else if (lineEndings === LineEndings.CR) { + return CHARS.CR; + } else { + return CHARS.LF; + } + })(); + + window.editor.dispatch({ + effects: extension.reconfigure(EditorState.lineSeparator.of(lineBreak)), + }); +} + +export function getLineBreak(string: string, defaultValue?: string) { + // Default line endings + if (string.length === 0 && defaultValue !== undefined) { + // If it's set to line feed, we prefer leave it unspecified to let CodeMirror normalize line breaks + return defaultValue === '\n' ? undefined : defaultValue; + } + + // Detect characters, inspired by: https://www.npmjs.com/package/detect-newline + const lineBreaks = string.match(/(?:\r?\n|\r)/g) || []; + const LFs = lineBreaks.filter(ch => ch === CHARS.LF).length; + const CRLFs = lineBreaks.filter(ch => ch === CHARS.CRLF).length; + const CRs = lineBreaks.filter(ch => ch === CHARS.CR).length; + const usedMost = Math.max(LFs, CRLFs, CRs); + + if (CRLFs === usedMost) { + return CHARS.CRLF; + } else if (CRs === usedMost) { + return CHARS.CR; + } else { + // Unspecified, let CodeMirror handle it + return undefined; + } +} + +export type { LineEndings }; diff --git a/CoreEditor/src/modules/lineEndings/types.ts b/CoreEditor/src/modules/lineEndings/types.ts new file mode 100644 index 00000000..582e2587 --- /dev/null +++ b/CoreEditor/src/modules/lineEndings/types.ts @@ -0,0 +1,18 @@ +export enum LineEndings { + /** + * Unspecified, let CodeMirror do the normalization magic. + */ + Unspecified = 0, + /** + * Line Feed, used on macOS and Unix systems. + */ + LF = 1, + /** + * Carriage Return and Line Feed, used on Windows. + */ + CRLF = 2, + /** + * Carriage Return, previously used on Classic Mac OS. + */ + CR = 3, +} diff --git a/CoreEditor/src/modules/localization/index.ts b/CoreEditor/src/modules/localization/index.ts new file mode 100644 index 00000000..42e7cf16 --- /dev/null +++ b/CoreEditor/src/modules/localization/index.ts @@ -0,0 +1,16 @@ +import { EditorState } from '@codemirror/state'; + +// https://codemirror.net/examples/translate/ +export function localizePhrases() { + const strings = window.config.localizable; + return EditorState.phrases.of({ + // "key": "value" ?? "fallback" + 'Control character': strings?.controlCharacter ?? 'Control Character', + 'Folded lines': strings?.foldedLines ?? 'Folded Lines', + 'Unfolded lines': strings?.unfoldedLines ?? 'Unfolded Lines', + 'folded code': strings?.foldedCode ?? 'Folded Code', + 'unfold': strings?.unfold ?? 'Unfold', + 'Fold line': strings?.foldLine ?? 'Fold Line', + 'Unfold line': strings?.unfoldLine ?? 'Unfold Line', + }); +} diff --git a/CoreEditor/src/modules/preview/index.css b/CoreEditor/src/modules/preview/index.css new file mode 100644 index 00000000..426e30d1 --- /dev/null +++ b/CoreEditor/src/modules/preview/index.css @@ -0,0 +1,4 @@ +.cm-md-previewButton { + cursor: pointer; + text-decoration: underline; +} diff --git a/CoreEditor/src/modules/preview/index.ts b/CoreEditor/src/modules/preview/index.ts new file mode 100644 index 00000000..7685f336 --- /dev/null +++ b/CoreEditor/src/modules/preview/index.ts @@ -0,0 +1,43 @@ +export enum PreviewType { + mermaid = 'mermaid', + katex = 'katex', + table = 'table', +} + +/** + * Invokes native methods to show code preview. + */ +export function showPreview(event: MouseEvent) { + const target = event.target as HTMLSpanElement; + if (!(target instanceof HTMLSpanElement)) { + return; + } + + const code = target.getAttribute('data-code'); + if (code === null) { + return; + } + + const pos = target.getAttribute('data-pos'); + if (pos === null) { + return; + } + + const rect = window.editor.coordsAtPos(parseInt(pos)); + if (rect === null) { + return; + } + + const type = target.getAttribute('data-type') as PreviewType; + window.nativeModules.preview.show({ + code, type, rect: { + x: rect.left, + y: rect.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top, + }, + }); + + event.preventDefault(); + event.stopPropagation(); +} diff --git a/CoreEditor/src/modules/search/index.ts b/CoreEditor/src/modules/search/index.ts new file mode 100644 index 00000000..eb074d6d --- /dev/null +++ b/CoreEditor/src/modules/search/index.ts @@ -0,0 +1,87 @@ +import { EditorSelection } from '@codemirror/state'; +import { SearchQuery, openSearchPanel, closeSearchPanel, setSearchQuery, getSearchQuery } from '@codemirror/search'; +import { scrollSearchMatchToVisible, selectedMainText } from '../selection'; + +import SearchOptions from './options'; +import rangesFromQuery from './rangesFromQuery'; +import searchOccurrences from './searchOccurrences'; +import selectWithRanges from '../selection/selectWithRanges'; + +// Imagine this entire file as a front-end to the @codemirror/search module +import * as search from '@codemirror/search'; + +export function setState(enabled: boolean) { + if (enabled) { + openSearchPanel(window.editor); + } else { + closeSearchPanel(window.editor); + } +} + +export function updateQuery(options: SearchOptions): number { + const editor = window.editor; + const selectMatch = () => findNext() || findPrevious(); + + const query = new SearchQuery(options); + editor.dispatch({ effects: setSearchQuery.of(query) }); + + const ranges = rangesFromQuery(query); + if (ranges !== undefined) { + for (const range of ranges) { + // If there's a visible range, focus on it + const rect = editor.coordsAtPos(range.from); + if (rect !== null && rect.top >= 0 && rect.top <= editor.dom.clientHeight) { + editor.dispatch({ + selection: EditorSelection.range(range.from, range.to), + }); + + scrollSearchMatchToVisible(); + return ranges.length; + } + } + + selectMatch(); + return ranges.length; + } + + selectMatch(); + return document.querySelectorAll('.cm-searchMatch').length; +} + +export function findNext() { + const result = search.findNext(window.editor); + scrollSearchMatchToVisible(); + return result; +} + +export function findPrevious() { + const result = search.findPrevious(window.editor); + scrollSearchMatchToVisible(); + return result; +} + +export function replaceNext() { + search.replaceNext(window.editor); + scrollSearchMatchToVisible(); +} + +export function replaceAll() { + search.replaceAll(window.editor); + scrollSearchMatchToVisible(); +} + +export function selectAllOccurrences() { + const doc = window.editor.state.doc.toString(); + const query = selectedMainText(); + if (query.length > 0) { + selectWithRanges(searchOccurrences(doc, query)); + } +} + +export function numberOfMatches(): CodeGen_Int { + const query = getSearchQuery(window.editor.state); + const ranges = rangesFromQuery(query); + return (ranges !== undefined ? ranges.length : 0) as CodeGen_Int; +} + +export type { SearchOptions }; diff --git a/CoreEditor/src/modules/search/options.ts b/CoreEditor/src/modules/search/options.ts new file mode 100644 index 00000000..6edcf9de --- /dev/null +++ b/CoreEditor/src/modules/search/options.ts @@ -0,0 +1,8 @@ +export default interface SearchOptions { + search: string; + caseSensitive: boolean; + literal: boolean; + regexp: boolean; + wholeWord: boolean; + replace?: string; +} diff --git a/CoreEditor/src/modules/search/rangesFromQuery.ts b/CoreEditor/src/modules/search/rangesFromQuery.ts new file mode 100644 index 00000000..f61f8cea --- /dev/null +++ b/CoreEditor/src/modules/search/rangesFromQuery.ts @@ -0,0 +1,13 @@ +import { SelectionRange } from '@codemirror/state'; +import { SearchQuery } from '@codemirror/search'; + +export default function rangesFromQuery(query: SearchQuery): SelectionRange[] | undefined { + // Get RegExpQuery or StringQuery (we have tests to protect this quirk) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anyQuery = query as any; + if (typeof anyQuery.create === 'function') { + return anyQuery.create().matchAll(window.editor.state, 1e9); + } + + return undefined; +} diff --git a/CoreEditor/src/modules/search/searchOccurrences.ts b/CoreEditor/src/modules/search/searchOccurrences.ts new file mode 100644 index 00000000..877d1523 --- /dev/null +++ b/CoreEditor/src/modules/search/searchOccurrences.ts @@ -0,0 +1,15 @@ +import { SelectionRange, EditorSelection } from '@codemirror/state'; + +export default function searchOccurrences(text: string, query: string) { + const ranges: SelectionRange[] = []; + let index = -1; + + // Case senstive, naive search + while ((index = text.indexOf(query, index + 1)) >= 0) { + const from = index; + const to = index + query.length; + ranges.push(EditorSelection.range(from, to)); + } + + return ranges; +} diff --git a/CoreEditor/src/modules/selection/index.ts b/CoreEditor/src/modules/selection/index.ts new file mode 100644 index 00000000..f9848a6b --- /dev/null +++ b/CoreEditor/src/modules/selection/index.ts @@ -0,0 +1,118 @@ +import { EditorView } from '@codemirror/view'; +import { EditorSelection, Line } from '@codemirror/state'; + +import selectedRanges from './selectedRanges'; +import selectWholeLineAt from './selectWholeLineAt'; +import searchMatchPosition from './searchMatchPosition'; + +/** + * Reverse ranges for multi-selection to keep indices correct when updating. + */ +export function reversedRanges() { + return selectedRanges().reverse(); +} + +/** + * Reverse ranges for multi-selection to keep indices correct when updating. + */ +export function reversedLines() { + const lines: Line[] = []; + const ranges = selectedRanges(); + + for (const { from, to } of ranges) { + lines.push(...linesWithRange(from, to)); + } + + return lines.reverse(); +} + +export function linesWithRange(from: number, to: number) { + const editor = window.editor; + const doc = editor.state.doc; + + const lines: Line[] = []; + const start = doc.lineAt(from).number; + const end = doc.lineAt(to).number; + + for (let ln = start; ln <= end; ++ln) { + lines.push(doc.line(ln)); + } + + return lines; +} + +/** + * Returns the text of selection when we only care about one selection. + * + * @returns Text of the main selection + */ +export function selectedMainText(): string { + const state = window.editor.state; + const { from, to } = state.selection.main; + return state.doc.sliceString(from, to); +} + +/** + * Select line based on mouse event, e.g., clicking on gutter number to select the whole line. + * + * @param event + */ +export function selectWholeLineIfNeeded(event: MouseEvent) { + const target = event.target; + if (target instanceof HTMLDivElement && target.classList.contains('cm-gutterElement')) { + selectWholeLineAt(parseInt(target.innerText)); + } +} + +export function scrollToSelection(strategy: 'nearest' | 'start' | 'end' | 'center' = 'center', margin = 5) { + const editor = window.editor; + const range = editor.state.selection.main; + editor.dispatch({ + effects: EditorView.scrollIntoView(range, { y: strategy, yMargin: margin }), + }); +} + +export function gotoLine(lineNumber: number) { + const editor = window.editor; + const state = editor.state; + const pos = state.doc.line(lineNumber).from; + + editor.dispatch({ selection: EditorSelection.cursor(pos) }); + scrollToSelection(); +} + +/** + * Make sure caret is visible, with an additional margin to breath. + */ +export function scrollCaretToVisible() { + const editor = window.editor; + const pos = editor.state.selection.main.to; + scrollPositionToVisible(pos); +} + +/** + * Make sure selected search match is visible, with an additional margin to breath. + */ +export function scrollSearchMatchToVisible() { + const pos = searchMatchPosition(); + if (pos !== null) { + scrollPositionToVisible(pos); + } +} + +/** + * Make sure text position is visible, with an additional margin to breath. + */ +export function scrollPositionToVisible(pos: number) { + const editor = window.editor; + const coords = editor.coordsAtPos(pos); + const margin = 40; + + if (coords === null) { + return console.error('Error getting coords from pos'); + } + + if (coords.bottom + margin > editor.dom.clientHeight) { + return scrollToSelection('end', margin); + } +} diff --git a/CoreEditor/src/modules/selection/searchMatchPosition.ts b/CoreEditor/src/modules/selection/searchMatchPosition.ts new file mode 100644 index 00000000..4009f786 --- /dev/null +++ b/CoreEditor/src/modules/selection/searchMatchPosition.ts @@ -0,0 +1,11 @@ +/** + * Returns the current selected search match or null if not found. + */ +export default function searchMatchPosition() { + const element = document.querySelector('.cm-searchMatch-selected') as HTMLElement | null; + if (element === null) { + return null; + } + + return window.editor.posAtDOM(element); +} diff --git a/CoreEditor/src/modules/selection/selectWholeLineAt.ts b/CoreEditor/src/modules/selection/selectWholeLineAt.ts new file mode 100644 index 00000000..5df1c38a --- /dev/null +++ b/CoreEditor/src/modules/selection/selectWholeLineAt.ts @@ -0,0 +1,19 @@ +import { EditorSelection } from '@codemirror/state'; + +/** + * Select the whole line, it's slightly different compared to the CodeMirror built-in one, + * more specifically, it doesn't include the following linebreak. + * + * @param n 1-based line number + */ +export default function selectWholeLineAt(n: number) { + try { + const editor = window.editor; + const line = editor.state.doc.line(n); + editor.dispatch({ selection: EditorSelection.range(line.from, line.to) }); + } catch (error) { + // The state.doc.line can *sometimes* throw exceptions, haven't looked into it, + // but we don't want to make other features non-functional. + console.error(error); + } +} diff --git a/CoreEditor/src/modules/selection/selectWithRanges.ts b/CoreEditor/src/modules/selection/selectWithRanges.ts new file mode 100644 index 00000000..13011d24 --- /dev/null +++ b/CoreEditor/src/modules/selection/selectWithRanges.ts @@ -0,0 +1,8 @@ +import { EditorSelection, SelectionRange } from '@codemirror/state'; + +export default function selectWithRanges(ranges: SelectionRange[]) { + const editor = window.editor; + editor.dispatch({ + selection: EditorSelection.create(ranges), + }); +} diff --git a/CoreEditor/src/modules/selection/selectedLineColumn.ts b/CoreEditor/src/modules/selection/selectedLineColumn.ts new file mode 100644 index 00000000..e101ffcc --- /dev/null +++ b/CoreEditor/src/modules/selection/selectedLineColumn.ts @@ -0,0 +1,18 @@ +import { LineColumnInfo } from './types'; + +/** + * Get the information of the current selected line and column. + */ +export function selectedLineColumn(): LineColumnInfo { + const editor = window.editor; + const state = editor.state; + const selection = editor.state.selection.main; + const line = state.doc.lineAt(selection.head); + const column = selection.head - line.from + 1; + + return { + line: line.number as CodeGen_Int, + column: column as CodeGen_Int, + length: (selection.to - selection.from) as CodeGen_Int, + }; +} diff --git a/CoreEditor/src/modules/selection/selectedRanges.ts b/CoreEditor/src/modules/selection/selectedRanges.ts new file mode 100644 index 00000000..e009fd79 --- /dev/null +++ b/CoreEditor/src/modules/selection/selectedRanges.ts @@ -0,0 +1,3 @@ +export default function selectedRanges() { + return [...window.editor.state.selection.ranges]; +} diff --git a/CoreEditor/src/modules/selection/types.ts b/CoreEditor/src/modules/selection/types.ts new file mode 100644 index 00000000..f528807c --- /dev/null +++ b/CoreEditor/src/modules/selection/types.ts @@ -0,0 +1,5 @@ +export interface LineColumnInfo { + line: CodeGen_Int; + column: CodeGen_Int; + length: CodeGen_Int; +} diff --git a/CoreEditor/src/modules/snippets/index.ts b/CoreEditor/src/modules/snippets/index.ts new file mode 100644 index 00000000..7a3718b1 --- /dev/null +++ b/CoreEditor/src/modules/snippets/index.ts @@ -0,0 +1,19 @@ +import insertSnippet from './insertSnippet'; + +export function insertHyperLink(title: string, url: string, prefix = '') { + insertSnippet(`${prefix}[#{${title}}](#{${url}})`); +} + +export function insertTable(columnName: string, itemName: string) { + // It's merely a trivial approach, + // we don't want to spend time improving it, + // using tables in Markdown can hardly be great. + // + // Let's just use this as a hint for those who are not familiar with the syntax. + insertSnippet([ + `| #{${columnName} 1} | #{${columnName} 2} | #{${columnName} 3} |`, + '|:----|:---:|:---:|', + `| #{${itemName} 1} | #{${itemName} 2} | #{${itemName} 3} |`, + `| #{${itemName} 4} | #{${itemName} 5} | #{${itemName} 6} |`, + ].join(window.editor.state.lineBreak)); +} diff --git a/CoreEditor/src/modules/snippets/insertSnippet.ts b/CoreEditor/src/modules/snippets/insertSnippet.ts new file mode 100644 index 00000000..eb04cc6c --- /dev/null +++ b/CoreEditor/src/modules/snippets/insertSnippet.ts @@ -0,0 +1,14 @@ +import { snippet } from '@codemirror/autocomplete'; + +/** + * Insert snippet with placeholder tokens, it only handles the main selection. + * + * @param template Template string as described in https://codemirror.net/docs/ref/#autocomplete.snippet + */ +export default function insertSnippet(template: string, label = '') { + const editor = window.editor; + const { from, to } = editor.state.selection.main; + + // Make #{} the last one to be the border + snippet(template + '#{}')(editor, { label }, from, to); +} diff --git a/CoreEditor/src/modules/textChecker/index.ts b/CoreEditor/src/modules/textChecker/index.ts new file mode 100644 index 00000000..c9ed51e1 --- /dev/null +++ b/CoreEditor/src/modules/textChecker/index.ts @@ -0,0 +1,19 @@ +import TextCheckerOptions from './options'; + +/** + * Div level text checker settings. + */ +export function update(options: TextCheckerOptions) { + const contentDiv = document.querySelector('.cm-content'); + if (contentDiv === null) { + return; + } + + const toString = (value: boolean) => value ? 'true' : 'false'; + contentDiv.setAttribute('spellcheck', toString(options.spellcheck)); + contentDiv.setAttribute('autocorrect', toString(options.autocorrect)); + contentDiv.setAttribute('autocomplete', toString(options.autocomplete)); + contentDiv.setAttribute('autocapitalize', toString(options.autocapitalize)); +} + +export type { TextCheckerOptions }; diff --git a/CoreEditor/src/modules/textChecker/options.ts b/CoreEditor/src/modules/textChecker/options.ts new file mode 100644 index 00000000..14684ff2 --- /dev/null +++ b/CoreEditor/src/modules/textChecker/options.ts @@ -0,0 +1,6 @@ +export default interface TextCheckerOptions { + spellcheck: boolean; + autocorrect: boolean; + autocomplete: boolean; + autocapitalize: boolean; +} diff --git a/CoreEditor/src/modules/toc/index.ts b/CoreEditor/src/modules/toc/index.ts new file mode 100644 index 00000000..965b80a4 --- /dev/null +++ b/CoreEditor/src/modules/toc/index.ts @@ -0,0 +1,52 @@ +import { EditorSelection } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { HeadingInfo } from './types'; +import { scrollToSelection } from '../selection'; +import selectWithRanges from '../selection/selectWithRanges'; + +export function getTableOfContents() { + const editor = window.editor; + const state = editor.state; + const results: HeadingInfo[] = []; + + // Note that, it's not going to iterate the entire tree (might not have been parsed). + // + // This is by design because of potential performance issues. + syntaxTree(state).iterate({ + from: 0, to: state.doc.length, + enter: node => { + // Detect both ATXHeading and SetextHeading + const match = /^(?:ATX|Setext)Heading(\d)$/.exec(node.name); + if (match === null) { + return; + } + + // ATXHeading can have up to 3 leading spaces and arbitrary number of spaces between # and visible characters, + // example of a valid section header: " # Hello" + const title = state.doc.sliceString(node.from, node.to).replace(/ {0,3}#+ +/, ''); + const level = parseInt(match[1]); + + results.push({ + title: title.length > 64 ? title.substring(0, 64) + '...' : title, + level: level as CodeGen_Int, + from: node.from as CodeGen_Int, + to: node.to as CodeGen_Int, + }); + }, + }); + + // Indent each level with 2 spaces, the top level is not indented + const baseLevel = results.reduce((acc, cur) => Math.min(acc, cur.level), 6); + results.forEach(item => { + item.title = `${' '.repeat((item.level - baseLevel) * 2)}${item.title}`; + }); + + return results; +} + +export function gotoHeader(headingInfo: HeadingInfo) { + selectWithRanges([EditorSelection.cursor(headingInfo.from)]); + scrollToSelection('start'); +} + +export type { HeadingInfo }; diff --git a/CoreEditor/src/modules/toc/types.ts b/CoreEditor/src/modules/toc/types.ts new file mode 100644 index 00000000..f1db23ea --- /dev/null +++ b/CoreEditor/src/modules/toc/types.ts @@ -0,0 +1,6 @@ +export interface HeadingInfo { + title: string; + level: CodeGen_Int; + from: CodeGen_Int; + to: CodeGen_Int; +} diff --git a/CoreEditor/src/styling/builder.ts b/CoreEditor/src/styling/builder.ts new file mode 100644 index 00000000..217bd954 --- /dev/null +++ b/CoreEditor/src/styling/builder.ts @@ -0,0 +1,210 @@ +import { EditorView } from '@codemirror/view'; +import { HighlightStyle, TagStyle, syntaxHighlighting } from '@codemirror/language'; +import { Tag, tags as defaultTags } from '@lezer/highlight'; +import { StyleSpec } from 'style-mod'; +import { ColorScheme, BaseColors, EditorColors } from './types'; + +// Extend tags by adding Markdown-specific ones +const tags = { + ...defaultTags, + inlineCode: Tag.define(), + codeInfo: Tag.define(), + codeMark: Tag.define(), + listMark: Tag.define(), + quoteMark: Tag.define(), + linkMark: Tag.define(), +}; + +// Here we define color independent theme styles, +// it's almost the built-in "baseTheme" concept in CodeMirror but provides more flexibility. +const sharedStyles: { [selector: string]: StyleSpec } = { + // Default + '.cm-content': { + paddingRight: '12px', + paddingBottom: '50vh', + }, + '.cm-cursor': { + borderLeftWidth: '2px', + }, + '.cm-focused': { + outline: 'none', + }, + '.cm-foldGutter': { + padding: '0 4px', + opacity: '0', + transition: '0.4s', + }, + '.cm-foldGutter, .cm-foldPlaceholder': { + /* We don't use ui-monospace here because ▶︎ and ••• look very big */ + fontFamily: 'monospace', + }, + '.cm-foldPlaceholder': { + margin: '0 4px', + padding: '0 4px', + borderRadius: '4px', + border: 'none', + }, + '.cm-gutters': { + borderRight: 'none', + fontFamily: 'ui-monospace, monospace', + }, + '.cm-gutters:hover .cm-foldGutter:not(:hover), .cm-foldGutter:hover': { + opacity: '1', + }, + '.cm-activeLineGutter': { + backgroundColor: 'inherit', + }, + '.cm-snippetFieldPosition': { + borderLeft: 'none', + }, + // Extended + '.cm-visibleTab': { + backgroundImage: 'url(\'data:image/svg+xml,\')', + backgroundSize: 'auto 100%', + backgroundPosition: 'right 90%', + backgroundRepeat: 'no-repeat', + }, + '.cm-visibleSpace:before': { + content: 'attr(content)', + position: 'absolute', + pointerEvents: 'none', + }, + '.cm-md-monospace, .cm-md-fencedCode *, .cm-md-table *': { + fontFamily: 'ui-monospace, monospace', + }, +}; + +// Here we define color independent highlight styles +const sharedHighlights = [ + // Basic + { tag: tags.strong, fontWeight: 'bold' }, + { tag: [tags.emphasis, tags.quote], fontStyle: 'italic' }, + { tag: tags.strikethrough, textDecoration: 'line-through' }, + { tag: tags.link, textDecoration: 'underline' }, + { tag: tags.monospace, fontFamily: 'ui-monospace, monospace' }, + + // Markdown + { tag: tags.heading1, class: 'cm-md-header cm-md-heading1' }, + { tag: tags.heading2, class: 'cm-md-header cm-md-heading2' }, + { tag: tags.heading3, class: 'cm-md-header cm-md-heading3' }, + { tag: tags.heading4, class: 'cm-md-header cm-md-heading4' }, + { tag: tags.heading5, class: 'cm-md-header cm-md-heading5' }, + { tag: tags.heading6, class: 'cm-md-header cm-md-heading6' }, +]; + +function buildTheme(colors: EditorColors, scheme?: ColorScheme) { + const specs = { + // Root + '&': { + color: colors.text, + backgroundColor: colors.background, + }, + // Caret + '.cm-content': { + caretColor: colors.caret, + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: colors.caret, + }, + // Selection + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { + backgroundColor: colors.selection, + }, + '.cm-activeLine': { + backgroundColor: colors.activeLine, + boxShadow: innerBorder(3.0, colors.lineBorder), + }, + // Brackets + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: colors.matchingBracket, + boxShadow: innerBorder(1.5, colors.bracketBorder), + }, + // Gutters + '.cm-gutters': { + color: colors.lineNumber, + backgroundColor: colors.background, + }, + '.cm-lineNumbers > .cm-activeLineGutter': { + color: colors.text, + }, + // Handle of code folding + '.cm-foldGutter, .cm-foldPlaceholder': { + color: `${colors.text}66`, + }, + '.cm-foldPlaceholder': { + backgroundColor: colors.lighterBackground, + }, + // Search + '.cm-searchMatch': { + backgroundColor: colors.searchMatch, + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: colors.selectedMatch, + }, + '.cm-selectionMatch': { + backgroundColor: colors.selectionHighlight, + }, + // Control characters + '.cm-specialChar': { + color: '#ffffff', + backgroundColor: '#960000', + }, + // Extended + '.cm-visibleSpace:before': { + color: colors.visibleSpace, + }, + '.cm-md-inlineCode': { + backgroundColor: colors.lighterBackground, + }, + }; + + const combined = { ...sharedStyles }; + const keys = Object.keys(specs); + + // Create styles by merging two style sheets + for (const key of keys) { + const existing = combined[key] as StyleSpec | undefined; + if (existing !== undefined) { + combined[key] = { ...existing, ...specs[key] }; + } else { + combined[key] = specs[key]; + } + } + + return EditorView.theme(combined, { dark: scheme === 'dark' }); +} + +function buildHighlight(colors: BaseColors, specs: readonly TagStyle[], scheme?: ColorScheme) { + const style = HighlightStyle.define([ + ...[ + { + tag: [ + tags.typeName, tags.attributeName, tags.annotation, tags.namespace, tags.self, tags.changed, + tags.atom, tags.bool, tags.number, + tags.contentSeparator, tags.special(tags.variableName), + ], + color: colors.accent, + }, + { + tag: [ + tags.name, tags.character, tags.labelName, + tags.separator, tags.processingInstruction, tags.definition(tags.name), + ], + color: colors.text, + }, + { tag: tags.invalid, color: '#ff0000' }, + ], + ...sharedHighlights, ...specs, + ], { themeType: scheme }); + + return syntaxHighlighting(style); +} + +/** + * Please use box-shadow to create inner borders. + */ +function innerBorder(width: number, color?: string) { + return color !== undefined ? `inset 0px 0px 0px ${width}px ${color}` : 'none'; +} + +export { buildTheme, buildHighlight, tags }; diff --git a/CoreEditor/src/styling/config.ts b/CoreEditor/src/styling/config.ts new file mode 100644 index 00000000..b9e4a03f --- /dev/null +++ b/CoreEditor/src/styling/config.ts @@ -0,0 +1,153 @@ +import { EditorView, highlightActiveLine } from '@codemirror/view'; +import { EditorTheme, loadTheme } from './themes'; +import { Config } from '../config'; +import { styleSheets } from '../common/store'; +import { gutterExtensions } from './nodes/gutter'; +import { invisiblesExtension } from './nodes/invisible'; +import { calculateFontSize } from './nodes/heading'; +import { updateStyleSheet } from './helper'; + +/** + * Style sheets that can be changed dynamically. + * + * Generally, we can either disable them or update css rules inside them. + */ +export default interface StyleSheets { + accentColor?: HTMLStyleElement; + fontFamily?: HTMLStyleElement; + fontSize?: HTMLStyleElement; + focusMode?: HTMLStyleElement; + lineHeight?: HTMLStyleElement; +} + +export function setUp(config: Config) { + setAccentColor(loadTheme(config.theme).accentColor); + setFontFamily(config.fontFamily); + setFontSize(config.fontSize); + setFocusMode(config.focusMode); + setLineHeight(config.lineHeight); +} + +export function setTheme(theme: EditorTheme) { + const editor = window.editor as EditorView | null; + + // Editor may have not been initialized + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.theme.reconfigure(theme.extension), + }); + } + + setAccentColor(theme.accentColor); +} + +export function setAccentColor(accentColor: string) { + if (styleSheets.accentColor === undefined) { + const style = document.createElement('style'); + style.textContent = '.cm-md-header {}'; + + styleSheets.accentColor = style; + document.head.appendChild(style); + } + + updateStyleSheet(styleSheets.accentColor, style => style.color = accentColor); +} + +export function setFontFamily(fontFamily: string) { + if (styleSheets.fontFamily === undefined) { + const style = document.createElement('style'); + style.textContent = '.cm-content * {}'; + + styleSheets.fontFamily = style; + document.head.appendChild(style); + } + + updateStyleSheet(styleSheets.fontFamily, style => style.fontFamily = fontFamily); +} + +export function setFontSize(fontSize: number) { + if (styleSheets.fontSize === undefined) { + const style = document.createElement('style'); + style.textContent = ` + .cm-editor {} + .cm-md-heading1 {} + .cm-md-heading2 {} + .cm-md-heading3 {} + `; + + styleSheets.fontSize = style; + document.head.appendChild(style); + } + + updateStyleSheet(styleSheets.fontSize, (style, rule) => { + // E.g., .cm-md-heading1 -> 1, .cm-editor -> 0 + const headingLevel = parseInt(rule.selectorText.slice(-1)) || 0; + style.fontSize = `${calculateFontSize(fontSize, headingLevel)}px`; + }); +} + +export function setShowLineNumbers(enabled: boolean) { + const editor = window.editor as EditorView | null; + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.gutters?.reconfigure(enabled ? gutterExtensions : []), + }); + } +} + +export function setShowActiveLineIndicator(enabled: boolean) { + const editor = window.editor as EditorView | null; + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.activeLine?.reconfigure(enabled ? highlightActiveLine() : []), + }); + } +} + +export function setShowInvisibles(enabled: boolean) { + const editor = window.editor as EditorView | null; + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.invisibles?.reconfigure(enabled ? invisiblesExtension : []), + }); + } +} + +export function setFocusMode(enabled: boolean) { + if (styleSheets.focusMode === undefined) { + const style = document.createElement('style'); + style.textContent = ` + .cm-line:not(.cm-activeLine), .cm-gutterElement:not(.cm-activeLineGutter) { + filter: grayscale(1); + opacity: 0.3; + } + `; + + style.disabled = true; + styleSheets.focusMode = style; + document.head.appendChild(style); + } + + styleSheets.focusMode.disabled = !enabled; +} + +export function setLineWrapping(enabled: boolean) { + const editor = window.editor as EditorView | null; + if (typeof editor?.dispatch === 'function') { + editor.dispatch({ + effects: window.dynamics.lineWrapping?.reconfigure(enabled ? EditorView.lineWrapping : []), + }); + } +} + +export function setLineHeight(lineHeight: number) { + if (styleSheets.lineHeight === undefined) { + const style = document.createElement('style'); + style.textContent = '.cm-line {}'; + + styleSheets.lineHeight = style; + document.head.appendChild(style); + } + + updateStyleSheet(styleSheets.lineHeight, style => style.lineHeight = `${lineHeight * 100}%`); +} diff --git a/CoreEditor/src/styling/helper.ts b/CoreEditor/src/styling/helper.ts new file mode 100644 index 00000000..d05c8c46 --- /dev/null +++ b/CoreEditor/src/styling/helper.ts @@ -0,0 +1,25 @@ +import { Decoration, DOMEventHandlers, EditorView, ViewPlugin } from '@codemirror/view'; +import { RangeSet } from '@codemirror/state'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createDecoPlugin(builder: () => RangeSet, eventHandlers?: DOMEventHandlers) { + return ViewPlugin.fromClass(class {}, { + provide: () => EditorView.decorations.of(editor => { + window.editor = editor; + return builder(); + }), + eventHandlers, + }); +} + +export function updateStyleSheet(element: HTMLStyleElement | null, update: (style: CSSStyleDeclaration, rule: CSSStyleRule) => void) { + const rules = element?.sheet?.cssRules; + if (rules === undefined) { + return; + } + + for (let index = 0; index < rules.length; ++index) { + const rule = rules[index] as CSSStyleRule; + update(rule.style as CSSStyleDeclaration, rule); + } +} diff --git a/CoreEditor/src/styling/markdown.ts b/CoreEditor/src/styling/markdown.ts new file mode 100644 index 00000000..ccc1a448 --- /dev/null +++ b/CoreEditor/src/styling/markdown.ts @@ -0,0 +1,41 @@ +import { styleTags } from '@lezer/highlight'; +import { MarkdownConfig } from '@lezer/markdown'; +import { markdownMathExtension as mathExtension } from '../@vendor/joplin/markdownMathParser'; +import { tags } from './builder'; +import { inlineCodeStyle, fencedCodeStyle, previewMermaid, previewMath } from './nodes/code'; +import { previewTable, tableStyle } from './nodes/table'; + +// https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts +export const markdownExtensions: MarkdownConfig[] = [ + { + props: [ + styleTags({ + InlineCode: tags.inlineCode, + CodeInfo: tags.codeInfo, + CodeMark: tags.codeMark, + ListMark: tags.listMark, + QuoteMark: tags.quoteMark, + LinkMark: tags.linkMark, + }), + ], + }, + mathExtension, +]; + +/** + * Extensions used in all scenarios. + */ +export const renderExtensions = [ + inlineCodeStyle, + fencedCodeStyle, + tableStyle, +]; + +/** + * Extensions used only in the full editor, i.e., the preview extension doesn't use these. + */ +export const actionExtensions = [ + previewMermaid, + previewMath, + previewTable, +]; diff --git a/CoreEditor/src/styling/matchers/lexer.ts b/CoreEditor/src/styling/matchers/lexer.ts new file mode 100644 index 00000000..6220890c --- /dev/null +++ b/CoreEditor/src/styling/matchers/lexer.ts @@ -0,0 +1,63 @@ +import { Decoration } from '@codemirror/view'; +import { Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { SyntaxNodeRef } from '@lezer/common'; +import { WidgetView } from '../../dom/views/types'; + +/** + * Create mark decorations. + * + * @param nodeName Node name, such as "ATXHeading1" for headings + * @param className Class to decorate the node + */ +export function createMarkDeco(nodeName: string, className: string) { + return createDecos(nodeName, node => { + return Decoration.mark({ class: className }).range(node.from, node.to); + }); +} + +/** + * Create widget decorations. + * + * @param nodeName Node name, such as "ATXHeading1" for headings + * @param builder Closure to create a widget decoration + */ +export function createWidgetDeco(nodeName: string, builder: (node: SyntaxNodeRef) => WidgetView | null) { + return createDecos(nodeName, node => { + const widget = builder(node); + if (widget === null) { + return null; + } else { + return Decoration.widget({ widget, side: 1 }).range(widget.pos); + } + }); +} + +/** + * Build decorations by leveraging language lexers. + * + * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts + * + * @param nodeName Node name, such as "ATXHeading1" for headings + * @param builder Closure to create the Decoration + */ +function createDecos(nodeName: string, builder: (node: SyntaxNodeRef) => Range | null) { + const editor = window.editor; + const ranges: Range[] = []; + + for (const { from, to } of editor.visibleRanges) { + syntaxTree(editor.state).iterate({ + from, to, + enter: node => { + if (node.name === nodeName) { + const range = builder(node); + if (range) { + ranges.push(range); + } + } + }, + }); + } + + return Decoration.set(ranges); +} diff --git a/CoreEditor/src/styling/matchers/regex.ts b/CoreEditor/src/styling/matchers/regex.ts new file mode 100644 index 00000000..e4859169 --- /dev/null +++ b/CoreEditor/src/styling/matchers/regex.ts @@ -0,0 +1,33 @@ +import { Decoration, MatchDecorator } from '@codemirror/view'; + +/** + * Create mark decorations. + * + * @param regexp Regular expression + * @param builder Closure to build the decoration, or class name as a shortcut + */ +export function createMarkDeco(regexp: RegExp, builder: ((match: RegExpExecArray, pos: number) => Decoration | null) | string) { + return createDecos(regexp, (match, pos) => { + if (typeof builder === 'function') { + return builder(match, pos); + } else { + return Decoration.mark({ class: builder }); + } + }); +} + +/** + * Build decorations by leveraging MatchDecorator. + * + * @param regexp Regular expression + * @param builder Closure to create the decoration + */ +function createDecos(regexp: RegExp, builder: (match: RegExpExecArray, pos: number) => Decoration | null) { + const matcher = new MatchDecorator({ + regexp, + boundary: /\S/, + decoration: (match, _, pos) => builder(match, pos), + }); + + return matcher.createDeco(window.editor); +} diff --git a/CoreEditor/src/styling/matchers/stateful.ts b/CoreEditor/src/styling/matchers/stateful.ts new file mode 100644 index 00000000..b9a7a845 --- /dev/null +++ b/CoreEditor/src/styling/matchers/stateful.ts @@ -0,0 +1,32 @@ +import { Decoration, EditorView } from '@codemirror/view'; +import { Compartment, StateField, StateEffect, RangeSet } from '@codemirror/state'; + +/** + * Start stateful effects with compartment. + * + * @param compartment Compartment + * @param ranges Range set of decorations + */ +export function startEffect(compartment: Compartment, ranges: RangeSet) { + const extension = StateField.define({ + create() { return Decoration.none; }, + update() { return ranges; }, + provide: field => EditorView.decorations.from(field), + }); + + const effect = StateEffect.appendConfig.of(compartment.of(extension)); + window.editor.dispatch({ effects: [effect] }); +} + +/** + * Stop stateful effects captured in compartments. + * + * @param compartments Compartments + */ +export function stopEffect(compartments: Compartment[]) { + for (const compartment of compartments) { + const extension = StateField.define({ create() { /* no-op */ }, update() { /* no-op */ } }); + const effects = compartment.reconfigure(extension); + window.editor.dispatch({ effects }); + } +} diff --git a/CoreEditor/src/styling/nodes/code.ts b/CoreEditor/src/styling/nodes/code.ts new file mode 100644 index 00000000..b6dd879b --- /dev/null +++ b/CoreEditor/src/styling/nodes/code.ts @@ -0,0 +1,63 @@ +import { createMarkDeco, createWidgetDeco } from '../matchers/lexer'; +import { createDecoPlugin } from '../helper'; +import { PreviewWidget } from '../../dom/views'; +import { PreviewType, showPreview } from '../../modules/preview'; + +/** + * Always use monospace font for InlineCode. + */ +export const inlineCodeStyle = createDecoPlugin(() => { + return createMarkDeco('InlineCode', 'cm-md-monospace cm-md-inlineCode'); +}); + +/** + * Always use monospace font for FencedCode. + */ +export const fencedCodeStyle = createDecoPlugin(() => { + return createMarkDeco('FencedCode', 'cm-md-monospace cm-md-fencedCode'); +}); + +/** + * Enable [preview] button for https://mermaid.js.org/. + */ +export const previewMermaid = createDecoPlugin(() => { + return createWidgetDeco('CodeInfo', node => { + const state = window.editor.state; + if (state.doc.sliceString(node.from, node.to) !== 'mermaid') { + return null; + } + + const container = node.node.parent; + if (container === null || container.name !== 'FencedCode') { + return null; + } + + const boundary = container.lastChild; + if (boundary === null || boundary.name !== 'CodeMark') { + return null; + } + + const code = state.doc.sliceString(node.to + 1, boundary.from); + if (code.trim().length === 0) { + return null; + } + + // Here we finally confirmed that the code block is for mermaid + return new PreviewWidget(code, PreviewType.mermaid, node.to); + }); +}, { mouseup: showPreview }); + +/** + * Enable [preview] button for https://katex.org/. + */ +export const previewMath = createDecoPlugin(() => { + return createWidgetDeco('BlockMath', node => { + const state = window.editor.state; + const code = state.doc.sliceString(node.from + 2, node.to - 2); // 2 is the length of "$$" + if (code.trim().length === 0) { + return null; + } + + return new PreviewWidget(code, PreviewType.katex, node.from + 2); + }); +}, { mouseup: showPreview }); diff --git a/CoreEditor/src/styling/nodes/gutter.ts b/CoreEditor/src/styling/nodes/gutter.ts new file mode 100644 index 00000000..183ca730 --- /dev/null +++ b/CoreEditor/src/styling/nodes/gutter.ts @@ -0,0 +1,8 @@ +import { lineNumbers } from '@codemirror/view'; +import { codeFolding, foldGutter } from '@codemirror/language'; + +export const gutterExtensions = [ + lineNumbers(), + codeFolding({ placeholderText: '•••' }), + foldGutter({ openText: '▼', closedText: '▶︎' }), +]; diff --git a/CoreEditor/src/styling/nodes/heading.ts b/CoreEditor/src/styling/nodes/heading.ts new file mode 100644 index 00000000..3ecaa4f2 --- /dev/null +++ b/CoreEditor/src/styling/nodes/heading.ts @@ -0,0 +1,11 @@ +/** + * Calculate the font size, take headers into account. + * + * For example, if the regular font size is 15, "# Heading 1" goes with 20 (15 + 5). + * + * @param level Heading level + * @returns Font size for a *possible* header + */ +export function calculateFontSize(fontSize: number, level: number) { + return fontSize + ([0, 5, 3, 1][level] || 0); +} diff --git a/CoreEditor/src/styling/nodes/invisible.ts b/CoreEditor/src/styling/nodes/invisible.ts new file mode 100644 index 00000000..b89ceab4 --- /dev/null +++ b/CoreEditor/src/styling/nodes/invisible.ts @@ -0,0 +1,65 @@ +import { Decoration } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; +import { NodeType } from '@lezer/common'; +import { calculateFontSize } from './heading'; +import { createMarkDeco } from '../matchers/regex'; +import { createDecoPlugin } from '../helper'; + +// Originally learned from: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts +// +// We don't use the built-in highlightWhitespace extension, +// because it doesn't take custom font size into account. +// +// In Markdown rendering, we have different font sizes for headers, +// we need to figure out proper font size and set it to the pseudo class. +export const invisiblesExtension = createDecoPlugin(() => { + return createMarkDeco(/\t| +/g, (match, pos) => { + const invisible = match[0]; + return getOrCreateDeco(invisible, pos); + }); +}); + +/** + * Get or create a deco for given invisible character at a position. + */ +function getOrCreateDeco(invisible: string, pos: number) { + const state = window.editor.state; + const node = syntaxTree(state).resolve(pos); + + const fontSize = calculateFontSize(window.config.fontSize, headingLevel(node.type)); + const key = invisible + fontSize; + const cachedDeco = cachedDecos.get(key); + + // Great, exactly the same deco was created before + if (cachedDeco !== undefined) { + return cachedDeco; + } + + const fontStyle = (() => { + if (fontSize <= window.config.fontSize) { + // Only enable special style for Markdown headings where bigger font sizes are used + return ''; + } + + return `font-size: ${fontSize}px`; + })(); + + const newDeco = Decoration.mark({ + attributes: invisible === '\t' ? { 'class': 'cm-visibleTab' } : { + 'class': 'cm-visibleSpace', + 'style': fontStyle, + 'content': '·‌'.repeat(invisible.length), + }, + }); + + cachedDecos.set(key, newDeco); + return newDeco; +} + +// https://github.com/codemirror/lang-markdown/blob/main/src/markdown.ts +function headingLevel(type: NodeType) { + const match = /^(?:ATX|Setext)Heading(\d)$/.exec(type.name); + return match ? +match[1] : 0; +} + +const cachedDecos = new Map(); diff --git a/CoreEditor/src/styling/nodes/link.ts b/CoreEditor/src/styling/nodes/link.ts new file mode 100644 index 00000000..dd6f5821 --- /dev/null +++ b/CoreEditor/src/styling/nodes/link.ts @@ -0,0 +1,42 @@ +import { Compartment } from '@codemirror/state'; +import { clickableLinks as compartments } from '../../common/store'; +import { createMarkDeco } from '../matchers/regex'; +import { startEffect, stopEffect } from '../matchers/stateful'; + +// Fragile approach, but we only use it for link clicking, it should be fine +const pattern = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g; +const className = 'cm-md-link'; + +export function startClickable() { + const compartment = new Compartment; + compartments.push(compartment); + startEffect(compartment, createMarkDeco(pattern, className)); +} + +export function stopClickable() { + stopEffect(compartments); + compartments.length = 0; +} + +export function handleMouseDown(event: MouseEvent) { + if (compartments.length > 0 && extractLink(event.target) !== undefined) { + event.stopPropagation(); + event.preventDefault(); + } +} + +export function handleMouseUp(event: MouseEvent) { + if (compartments.length > 0) { + const link = extractLink(event.target); + if (link !== undefined) { + window.open(link, '_blank'); + stopClickable(); + } + } +} + +function extractLink(target: EventTarget | null) { + const selector = `.${className}`; + const element = (target as HTMLElement).closest(selector); + return element?.innerText; +} diff --git a/CoreEditor/src/styling/nodes/table.ts b/CoreEditor/src/styling/nodes/table.ts new file mode 100644 index 00000000..b759c2b4 --- /dev/null +++ b/CoreEditor/src/styling/nodes/table.ts @@ -0,0 +1,27 @@ +import { createMarkDeco, createWidgetDeco } from '../matchers/lexer'; +import { createDecoPlugin } from '../helper'; +import { PreviewWidget } from '../../dom/views'; +import { PreviewType, showPreview } from '../../modules/preview'; + +/** + * Always use monospace font for Table. + */ +export const tableStyle = createDecoPlugin(() => { + return createMarkDeco('Table', 'cm-md-monospace cm-md-table'); +}); + +/** + * Enable [preview] button for GFM tables. + */ +export const previewTable = createDecoPlugin(() => { + return createWidgetDeco('Table', node => { + const header = node.node.firstChild?.node; + if (header?.name !== 'TableHeader') { + return null; + } + + const state = window.editor.state; + const code = state.doc.sliceString(node.from, node.to); + return new PreviewWidget(code, PreviewType.table, header.to); + }); +}, { mouseup: showPreview }); diff --git a/CoreEditor/src/styling/themes/cobalt.ts b/CoreEditor/src/styling/themes/cobalt.ts new file mode 100644 index 00000000..268563d4 --- /dev/null +++ b/CoreEditor/src/styling/themes/cobalt.ts @@ -0,0 +1,54 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { darkBase as base } from './colors'; + +const colors = { + accent: '#ffc600', + text: '#e1efff', + cyan: '#9effff', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#193549', + caret: colors.accent, + selection: '#0050A4', + activeLine: '#1F4662', + matchingBracket: '#0e3a59', + lineNumber: '#aaaaaa', + searchMatch: '#cad40f66', + selectedMatch: '#ff720066', + selectionHighlight: '#0050a440', + visibleSpace: '#ffffff52', + lighterBackground: '#ffffff1a', + lineBorder: '#234e6d', + bracketBorder: '#8b8145', + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.operator, tags.operatorKeyword], color: '#ff9d00' }, + { tag: [tags.quote, tags.emphasis], color: colors.cyan, fontStyle: 'italic' }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.link, tags.escape, tags.string, tags.inserted, tags.regexp, tags.listMark, tags.special(tags.string)], color: '#a5ff90' }, + { tag: [tags.url, tags.tagName, tags.codeInfo], color: colors.cyan }, + { tag: [tags.className, tags.attributeName, tags.definition(tags.typeName), tags.function(tags.variableName)], color: colors.accent }, + { tag: tags.typeName, color: '#80ffbb' }, + { tag: [tags.meta, tags.comment], color: '#0088ff' }, + { tag: tags.strong, color: colors.cyan, fontWeight: 'bold' }, + { tag: [tags.linkMark, tags.quoteMark], color: colors.text }, + { tag: [tags.contentSeparator, tags.definition(tags.variableName), tags.function(tags.propertyName)], color: colors.accent }, + { tag: [tags.atom, tags.bool, tags.number], color: '#ff628c' }, + { tag: tags.self, color: '#fb94ff' }, + ], 'dark'); +} + +export default function Cobalt(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/colors.ts b/CoreEditor/src/styling/themes/colors.ts new file mode 100644 index 00000000..77c27e9b --- /dev/null +++ b/CoreEditor/src/styling/themes/colors.ts @@ -0,0 +1,23 @@ +const lightBase = { + red: '#82071e', + green: '#116329', + gray1: '#8e8e93', + gray2: '#aeaeb2', + gray3: '#c7c7cc', + gray4: '#d1d1d6', + gray5: '#e5e5ea', + gray6: '#f2f2f7', +}; + +const darkBase = { + red: '#ffa198', + green: '#7ee787', + gray1: '#8e8e93', + gray2: '#636366', + gray3: '#48484a', + gray4: '#3a3a3c', + gray5: '#2c2c2e', + gray6: '#1c1c1e', +}; + +export { lightBase, darkBase }; diff --git a/CoreEditor/src/styling/themes/dracula.ts b/CoreEditor/src/styling/themes/dracula.ts new file mode 100644 index 00000000..3d2df0ec --- /dev/null +++ b/CoreEditor/src/styling/themes/dracula.ts @@ -0,0 +1,52 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; + +const colors = { + accent: '#bd93f9', + text: '#f8f8f2', + yellow: '#f1fa8c', + gold: '#ffb86c', + gray: '#6272a4', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#282a36', + caret: '#aeafad', + selection: '#44475a', + activeLine: '#00000000', + matchingBracket: '#263032', + lineNumber: colors.gray, + searchMatch: '#ffffff40', + selectedMatch: '#ffb86c7f', + selectionHighlight: '#4244507f', + visibleSpace: '#ffffff1a', + lighterBackground: '#ffffff0d', + lineBorder: '#454759', + bracketBorder: '#888888', + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.link, tags.operator, tags.operatorKeyword, tags.tagName], color: '#ff79c6' }, + { tag: [tags.quote, tags.quoteMark, tags.emphasis], color: colors.yellow, fontStyle: 'italic' }, + { tag: [tags.deleted, tags.macroName], color: '#ff5555' }, + { tag: [tags.className, tags.typeName, tags.url, tags.definition(tags.typeName), tags.listMark], color: '#8be9fd' }, + { tag: [tags.inserted, tags.attributeName, tags.inlineCode, tags.codeInfo, tags.codeMark, tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#50fa7b' }, + { tag: [tags.meta, tags.comment, tags.contentSeparator], color: colors.gray }, + { tag: [tags.escape, tags.string, tags.regexp, tags.special(tags.string)], color: colors.yellow }, + { tag: tags.definition(tags.propertyName), color: colors.gold }, + { tag: tags.strong, color: colors.gold, fontWeight: 'bold' }, + { tag: tags.linkMark, color: colors.text }, + ], 'dark'); +} + +export default function Dracula(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/github-dark.ts b/CoreEditor/src/styling/themes/github-dark.ts new file mode 100644 index 00000000..54fb6007 --- /dev/null +++ b/CoreEditor/src/styling/themes/github-dark.ts @@ -0,0 +1,57 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { darkBase as base } from './colors'; + +const colors = { + accent: '#79c0ff', + text: '#c9d1d9', + activeLine: '#6e76811a', + searchMatch: '#f2cc607f', + selectedMatch: '#9e6a03', + selectionHighlight: '#3fb95021', + visibleSpace: '#484f58', + lighterBackground: '#484f5866', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#0d1116', + caret: '#58a6ff', + selection: '#264f78', + activeLine: colors.activeLine, + matchingBracket: '#24432e', + lineNumber: '#6e7681', + searchMatch: colors.searchMatch, + selectedMatch: colors.selectedMatch, + selectionHighlight: colors.selectionHighlight, + visibleSpace: colors.visibleSpace, + lighterBackground: colors.lighterBackground, + bracketBorder: '#358a43', + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.operator, tags.operatorKeyword], color: '#ff7b72' }, + { tag: [tags.literal, tags.inserted, tags.tagName], color: base.green }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.className, tags.definition(tags.propertyName), tags.definition(tags.typeName), tags.listMark], color: '#ffa657' }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#d2a8ff' }, + { tag: [tags.meta, tags.comment], color: '#8b949e' }, + { tag: [tags.link, tags.escape, tags.string, tags.regexp, tags.special(tags.string)], color: '#a5d6ff' }, + { tag: [tags.url, tags.linkMark], color: colors.text }, + { tag: tags.propertyName, color: colors.text }, + { tag: [tags.quote, tags.quoteMark], color: base.green, fontStyle: 'italic' }, + ], 'dark'); +} + +export default function GitHubDark(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} + +export { colors }; diff --git a/CoreEditor/src/styling/themes/github-light.ts b/CoreEditor/src/styling/themes/github-light.ts new file mode 100644 index 00000000..63922a3b --- /dev/null +++ b/CoreEditor/src/styling/themes/github-light.ts @@ -0,0 +1,57 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { lightBase as base } from './colors'; + +const colors = { + accent: '#0550ae', + text: '#24292f', + activeLine: '#eaeef27f', + searchMatch: '#fae17d7f', + selectedMatch: '#bf8700', + selectionHighlight: '#4ac26b50', + visibleSpace: '#afb8c1', + lighterBackground: '#afb8c133', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#ffffff', + caret: '#0a69da', + selection: '#add6ff', + activeLine: colors.activeLine, + matchingBracket: '#cee9d6', + lineNumber: '#8c959f', + searchMatch: colors.searchMatch, + selectedMatch: colors.selectedMatch, + selectionHighlight: colors.selectionHighlight, + visibleSpace: colors.visibleSpace, + lighterBackground: colors.lighterBackground, + bracketBorder: '#83d296', + }); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.operator, tags.operatorKeyword], color: '#cf222e' }, + { tag: [tags.literal, tags.inserted, tags.tagName], color: base.green }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.className, tags.definition(tags.propertyName), tags.definition(tags.typeName), tags.listMark], color: '#953800' }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#8250df' }, + { tag: [tags.meta, tags.comment], color: '#6e7781' }, + { tag: [tags.link, tags.escape, tags.string, tags.regexp, tags.special(tags.string)], color: '#0a3069' }, + { tag: [tags.url, tags.linkMark], color: colors.text }, + { tag: tags.propertyName, color: colors.text }, + { tag: [tags.quote, tags.quoteMark], color: base.green, fontStyle: 'italic' }, + ]); +} + +export default function GitHubLight(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} + +export { colors }; diff --git a/CoreEditor/src/styling/themes/index.ts b/CoreEditor/src/styling/themes/index.ts new file mode 100644 index 00000000..70d1e00f --- /dev/null +++ b/CoreEditor/src/styling/themes/index.ts @@ -0,0 +1,31 @@ +import { EditorTheme } from '../types'; + +import GitHubLight from './github-light'; +import GitHubDark from './github-dark'; +import XcodeLight from './xcode-light'; +import XcodeDark from './xcode-dark'; +import Dracula from './dracula'; +import Cobalt from './cobalt'; +import WinterIsComingLight from './winter-is-coming-light'; +import WinterIsComingDark from './winter-is-coming-dark'; +import MinimalLight from './minimal-light'; +import MinimalDark from './minimal-dark'; + +const themes = { + 'github-light': GitHubLight, + 'github-dark': GitHubDark, + 'xcode-light': XcodeLight, + 'xcode-dark': XcodeDark, + 'dracula': Dracula, + 'cobalt': Cobalt, + 'winter-is-coming-light': WinterIsComingLight, + 'winter-is-coming-dark': WinterIsComingDark, + 'minimal-light': MinimalLight, + 'minimal-dark': MinimalDark, +}; + +export function loadTheme(name: string): EditorTheme { + return (themes[name] ?? GitHubLight)(); +} + +export type { EditorTheme }; diff --git a/CoreEditor/src/styling/themes/minimal-dark.ts b/CoreEditor/src/styling/themes/minimal-dark.ts new file mode 100644 index 00000000..a9f4be1e --- /dev/null +++ b/CoreEditor/src/styling/themes/minimal-dark.ts @@ -0,0 +1,48 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { lightBase as light, darkBase as dark } from './colors'; +import { colors as fallback } from './github-dark'; + +const colors = { + accent: '#ffffff', + background: '#000000', + text: light.gray4, +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: colors.background, + caret: colors.accent, + lineNumber: dark.gray1, + matchingBracket: dark.gray3, + selection: dark.gray5, + activeLine: fallback.activeLine, + searchMatch: fallback.searchMatch, + selectedMatch: fallback.selectedMatch, + selectionHighlight: fallback.selectionHighlight, + visibleSpace: fallback.visibleSpace, + lighterBackground: fallback.lighterBackground, + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.link, tags.escape, tags.string, tags.inserted, tags.regexp, tags.listMark, tags.special(tags.string)], color: colors.accent }, + { tag: [tags.linkMark, tags.quoteMark], color: colors.accent }, + { tag: [tags.className, tags.attributeName, tags.definition(tags.typeName), tags.function(tags.variableName)], color: colors.accent }, + { tag: [tags.contentSeparator, tags.definition(tags.variableName), tags.function(tags.propertyName)], color: colors.accent }, + { tag: tags.strong, color: colors.accent, fontWeight: 'bold' }, + { tag: tags.emphasis, color: colors.accent, fontStyle: 'italic' }, + { tag: [tags.meta, tags.comment], color: light.gray1 }, + { tag: [tags.url, tags.tagName, tags.codeInfo], color: light.gray3 }, + ], 'dark'); +} + +export default function MinimalDark(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/minimal-light.ts b/CoreEditor/src/styling/themes/minimal-light.ts new file mode 100644 index 00000000..784628d2 --- /dev/null +++ b/CoreEditor/src/styling/themes/minimal-light.ts @@ -0,0 +1,48 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { lightBase as light, darkBase as dark } from './colors'; +import { colors as fallback } from './github-light'; + +const colors = { + accent: '#000000', + background: '#ffffff', + text: dark.gray4, +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: colors.background, + caret: colors.accent, + lineNumber: light.gray1, + matchingBracket: light.gray3, + selection: light.gray5, + activeLine: fallback.activeLine, + searchMatch: fallback.searchMatch, + selectedMatch: fallback.selectedMatch, + selectionHighlight: fallback.selectionHighlight, + visibleSpace: fallback.visibleSpace, + lighterBackground: fallback.lighterBackground, + }); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.link, tags.escape, tags.string, tags.inserted, tags.regexp, tags.listMark, tags.special(tags.string)], color: colors.accent }, + { tag: [tags.linkMark, tags.quoteMark], color: colors.accent }, + { tag: [tags.className, tags.attributeName, tags.definition(tags.typeName), tags.function(tags.variableName)], color: colors.accent }, + { tag: [tags.contentSeparator, tags.definition(tags.variableName), tags.function(tags.propertyName)], color: colors.accent }, + { tag: tags.strong, color: colors.accent, fontWeight: 'bold' }, + { tag: tags.emphasis, color: colors.accent, fontStyle: 'italic' }, + { tag: [tags.meta, tags.comment], color: dark.gray1 }, + { tag: [tags.url, tags.tagName, tags.codeInfo], color: dark.gray3 }, + ]); +} + +export default function MinimalLight(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/winter-is-coming-dark.ts b/CoreEditor/src/styling/themes/winter-is-coming-dark.ts new file mode 100644 index 00000000..41ccfb2b --- /dev/null +++ b/CoreEditor/src/styling/themes/winter-is-coming-dark.ts @@ -0,0 +1,58 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { darkBase as base } from './colors'; + +const colors = { + accent: '#5abeb0', + text: '#ffffff', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#282822', + caret: '#219fd5', + selection: '#103362', + activeLine: '#0c499477', + matchingBracket: '#22567e', + lineNumber: '#219fd5', + searchMatch: '#103362', + selectedMatch: '#515c6a', + selectionHighlight: '#1033627f', + visibleSpace: '#3b3a32', + lighterBackground: '#3b3a32ee', + bracketBorder: '#888888', + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier], color: '#00bff9' }, + { tag: [tags.literal, tags.quoteMark], color: '#82aaff' }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: tags.inserted, color: base.green }, + { tag: [tags.className, tags.definition(tags.typeName)], color: '#d29ffc' }, + { tag: tags.typeName, color: '#7fdbca' }, + { tag: [tags.meta, tags.comment], color: '#999999' }, + { tag: [tags.operator, tags.operatorKeyword, tags.escape, tags.string, tags.link, tags.regexp, tags.special(tags.string)], color: '#bcf0c0' }, + { tag: [tags.function(tags.variableName), tags.propertyName, tags.function(tags.propertyName)], color: '#87aff4' }, + { tag: tags.definition(tags.propertyName), color: '#a1bde6' }, + { tag: tags.propertyName, color: '#7fdbca' }, + { tag: [tags.url, tags.tagName, tags.listMark], color: '#6dbdfa' }, + { tag: tags.emphasis, color: '#c792ea', fontStyle: 'italic' }, + { tag: tags.strong, color: '#57cdff', fontWeight: 'bold' }, + { tag: tags.linkMark, color: '#f3b8c2' }, + { tag: [tags.contentSeparator, tags.monospace], color: '#a7dbf7' }, + { tag: [tags.attributeName, tags.inlineCode, tags.codeInfo, tags.codeMark], color: '#f7ecb5' }, + { tag: tags.variableName, color: '#d6deeb' }, + { tag: [tags.atom, tags.bool, tags.number], color: '#8dec95' }, + ], 'dark'); +} + +export default function WinterIsComingDark(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/winter-is-coming-light.ts b/CoreEditor/src/styling/themes/winter-is-coming-light.ts new file mode 100644 index 00000000..e6cae2e7 --- /dev/null +++ b/CoreEditor/src/styling/themes/winter-is-coming-light.ts @@ -0,0 +1,59 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { lightBase as base } from './colors'; + +const colors = { + accent: '#034c7c', + text: '#3e3e3e', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#ffffff', + caret: '#4eb4d8', + selection: '#cee1f0', + activeLine: '#b0c0b033', + matchingBracket: '#b9d9e8', + lineNumber: '#2f86d2', + searchMatch: '#cee1f0', + selectedMatch: '#a8ac94', + selectionHighlight: '#cee1f07f', + visibleSpace: '#c4c5cd', + lighterBackground: '#c4c5cd33', + bracketBorder: '#b9b9b9', + }); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier], color: '#0991b6' }, + { tag: [tags.literal, tags.quoteMark], color: '#003494' }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.className, tags.tagName, tags.definition(tags.typeName)], color: '#0444ac' }, + { tag: tags.typeName, color: '#dc3eb7' }, + { tag: [tags.inserted, tags.meta, tags.comment], color: '#357b42' }, + { tag: [tags.operator, tags.operatorKeyword, tags.escape, tags.string, tags.link, tags.regexp, tags.special(tags.string)], color: '#a44185' }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#b1108e' }, + { tag: tags.definition(tags.propertyName), color: '#4a668f' }, + { tag: tags.propertyName, color: '#358a7b' }, + { tag: tags.url, color: '#924205' }, + { tag: tags.emphasis, color: '#c792ea', fontStyle: 'italic' }, + { tag: tags.strong, color: '#4e76b5', fontWeight: 'bold' }, + { tag: tags.listMark, color: '#207bb8' }, + { tag: tags.linkMark, color: '#00ac8f' }, + { tag: [tags.contentSeparator, tags.monospace], color: '#236ebf' }, + { tag: [tags.inlineCode, tags.codeInfo, tags.codeMark], color: '#0460b1' }, + { tag: tags.attributeName, color: '#df8618' }, + { tag: tags.variableName, color: '#828282' }, + { tag: [tags.atom, tags.bool, tags.number], color: '#174781' }, + ]); +} + +export default function WinterIsComingLight(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/xcode-dark.ts b/CoreEditor/src/styling/themes/xcode-dark.ts new file mode 100644 index 00000000..fc20d97e --- /dev/null +++ b/CoreEditor/src/styling/themes/xcode-dark.ts @@ -0,0 +1,56 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { darkBase as base } from './colors'; + +const colors = { + accent: '#5dd8ff', + text: '#ffffffd9', + brown: '#bf8555', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#1f1f24', + caret: colors.text, + selection: '#515b70', + activeLine: '#23252b', + matchingBracket: '#67b7a440', + lineNumber: '#747478', + searchMatch: '#545558', + selectedMatch: '#fffb00', + selectionHighlight: '#4d5465', + visibleSpace: '#424d5b', + lighterBackground: '#424d5b40', + }, 'dark'); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.operator, tags.operatorKeyword, tags.self], color: '#fc5fa3' }, + { tag: [tags.literal, tags.inserted], color: base.green }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.className, tags.definition(tags.propertyName), tags.definition(tags.typeName)], color: '#9ef1dd' }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#a167e6' }, + { tag: [tags.meta, tags.comment], color: '#6c7986' }, + { tag: [tags.link, tags.escape, tags.string, tags.regexp, tags.special(tags.string)], color: '#fc6a5d' }, + { tag: [tags.linkMark, tags.listMark], color: '#fd8f3f' }, + { tag: tags.url, color: '#41a1c0' }, + { tag: tags.propertyName, color: colors.text }, + { tag: tags.tagName, color: colors.accent }, + { tag: tags.attributeName, color: colors.brown }, + { tag: tags.definition(tags.variableName), color: '#67b7a4' }, + { tag: [tags.quote, tags.quoteMark], color: '#92a1b1', fontStyle: 'italic' }, + { tag: [tags.atom, tags.bool, tags.number], color: '#d0bf69' }, + { tag: tags.emphasis, color: colors.brown, fontStyle: 'italic' }, + { tag: tags.strong, color: '#d0a8ff', fontWeight: 'bold' }, + ], 'dark'); +} + +export default function XcodeDark(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/themes/xcode-light.ts b/CoreEditor/src/styling/themes/xcode-light.ts new file mode 100644 index 00000000..17d14bf7 --- /dev/null +++ b/CoreEditor/src/styling/themes/xcode-light.ts @@ -0,0 +1,56 @@ +import { EditorTheme } from '../types'; +import { buildTheme, buildHighlight, tags } from '../builder'; +import { lightBase as base } from './colors'; + +const colors = { + accent: '#0b4f79', + text: '#000000', + brown: '#815f03', +}; + +function theme() { + return buildTheme({ + text: colors.text, + background: '#ffffff', + caret: colors.text, + selection: '#a4cdff', + activeLine: '#e8f2ff', + matchingBracket: '#326d7440', + lineNumber: '#a6a6a6', + searchMatch: '#e4e4e4', + selectedMatch: '#fffa5c', + selectionHighlight: '#e9eef9', + visibleSpace: '#cccccc', + lighterBackground: '#cccccc4c', + }); +} + +function highlight() { + // Order matters, don't change it unless you fully understand how it works + return buildHighlight(colors, [ + { tag: [tags.keyword, tags.modifier, tags.operator, tags.operatorKeyword, tags.self], color: '#9b2393' }, + { tag: [tags.literal, tags.inserted], color: base.green }, + { tag: [tags.deleted, tags.macroName], color: base.red }, + { tag: [tags.className, tags.definition(tags.propertyName), tags.definition(tags.typeName)], color: '#1c464a' }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#6c36a9' }, + { tag: [tags.meta, tags.comment], color: '#5d6c79' }, + { tag: [tags.link, tags.escape, tags.string, tags.regexp, tags.special(tags.string)], color: '#c41a16' }, + { tag: [tags.linkMark, tags.listMark], color: '#643820' }, + { tag: tags.url, color: '#0f68a0' }, + { tag: tags.propertyName, color: colors.text }, + { tag: tags.tagName, color: colors.accent }, + { tag: tags.attributeName, color: colors.brown }, + { tag: tags.definition(tags.variableName), color: '#326d74' }, + { tag: [tags.quote, tags.quoteMark], color: '#4a5560', fontStyle: 'italic' }, + { tag: [tags.atom, tags.bool, tags.number], color: '#1c00cf' }, + { tag: tags.emphasis, color: colors.brown, fontStyle: 'italic' }, + { tag: tags.strong, color: '#3900a0', fontWeight: 'bold' }, + ]); +} + +export default function XcodeLight(): EditorTheme { + return { + accentColor: colors.accent, + extension: [theme(), highlight()], + }; +} diff --git a/CoreEditor/src/styling/types.ts b/CoreEditor/src/styling/types.ts new file mode 100644 index 00000000..137ee18d --- /dev/null +++ b/CoreEditor/src/styling/types.ts @@ -0,0 +1,30 @@ +import { Extension } from '@codemirror/state'; + +export type ColorScheme = 'light' | 'dark'; + +export interface BaseColors { + accent: string; + text: string; +} + +export interface EditorColors { + text: string; + background: string; + caret: string; + selection: string; + activeLine: string; + matchingBracket: string; + lineNumber: string; + searchMatch: string; + selectedMatch: string; + selectionHighlight: string; + visibleSpace: string; + lighterBackground: string; + lineBorder?: string; + bracketBorder?: string; +} + +export interface EditorTheme { + accentColor: string; + extension: Extension; +} diff --git a/CoreEditor/test/commands.test.ts b/CoreEditor/test/commands.test.ts new file mode 100644 index 00000000..c290cbfb --- /dev/null +++ b/CoreEditor/test/commands.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from '@jest/globals'; + +import * as editor from '../src/@test/editor'; +import * as commands from '../src/modules/commands'; + +describe('Commands module', () => { + test('test toggleBold', () => { + editor.setUp('Hello'); + editor.selectRange(0, 2); + + commands.toggleBold(); + expect(editor.getText()).toBe('**He**llo'); + + commands.toggleBold(); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleItalic', () => { + editor.setUp('Hello'); + editor.selectAll(); + + commands.toggleItalic(); + expect(editor.getText()).toBe('*Hello*'); + + commands.toggleItalic(); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleStrikethrough', () => { + editor.setUp('Hello'); + editor.selectAll(); + + commands.toggleStrikethrough(); + expect(editor.getText()).toBe('~~Hello~~'); + + commands.toggleStrikethrough(); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleHeading', () => { + editor.setUp('Hello'); + + commands.toggleHeading(1); + expect(editor.getText()).toBe('# Hello'); + + commands.toggleHeading(2); + expect(editor.getText()).toBe('## Hello'); + + commands.toggleHeading(2); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleBlockquote', () => { + editor.setUp('Hello'); + + commands.toggleBlockquote(); + expect(editor.getText()).toBe('> Hello'); + + commands.toggleBlockquote(); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleBullet', () => { + editor.setUp('Hello'); + + commands.toggleBullet(); + expect(editor.getText()).toBe('- Hello'); + + editor.setText('* Hello'); + commands.toggleBullet(); + expect(editor.getText()).toBe('Hello'); + + editor.setText('+ Hello'); + commands.toggleBullet(); + expect(editor.getText()).toBe('Hello'); + }); + + test('test toggleNumbering', () => { + editor.setUp('Hello'); + + commands.toggleNumbering(); + expect(editor.getText()).toBe('1. Hello'); + + commands.toggleNumbering(); + expect(editor.getText()).toBe('Hello'); + + editor.setText('One\nTwo\nThree'); + editor.selectAll(); + commands.toggleNumbering(); + expect(editor.getText()).toBe('1. One\n2. Two\n3. Three'); + + commands.toggleNumbering(); + expect(editor.getText()).toBe('One\nTwo\nThree'); + }); + + test('test toggleTodo', () => { + editor.setUp('Hello'); + + commands.toggleTodo(); + expect(editor.getText()).toBe('- [ ] Hello'); + + commands.toggleTodo(); + expect(editor.getText()).toBe('- [x] Hello'); + + commands.toggleTodo(); + expect(editor.getText()).toBe('Hello'); + }); +}); diff --git a/CoreEditor/test/history.test.ts b/CoreEditor/test/history.test.ts new file mode 100644 index 00000000..428596d5 --- /dev/null +++ b/CoreEditor/test/history.test.ts @@ -0,0 +1,24 @@ +import { history, undo, redo } from '@codemirror/commands'; +import { describe, expect, test } from '@jest/globals'; +import { canUndo, canRedo } from '../src/modules/history'; +import * as editor from '../src/@test/editor'; + +describe('History module', () => { + test('test canUndo and canRedo', () => { + editor.setUp('Hello World', history()); + expect(canUndo()).toBeFalsy(); + expect(canRedo()).toBeFalsy(); + + editor.setText('Changed...'); + expect(canUndo()).toBeTruthy(); + expect(canRedo()).toBeFalsy(); + + undo(window.editor); + expect(canUndo()).toBeFalsy(); + expect(canRedo()).toBeTruthy(); + + redo(window.editor); + expect(canUndo()).toBeTruthy(); + expect(canRedo()).toBeFalsy(); + }); +}); diff --git a/CoreEditor/test/input.test.ts b/CoreEditor/test/input.test.ts new file mode 100644 index 00000000..bf14bcc9 --- /dev/null +++ b/CoreEditor/test/input.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from '@jest/globals'; +import wrapBlock from '../src/modules/input/wrapBlock'; +import * as editor from '../src/@test/editor'; + +describe('Input module', () => { + test('test wrapBlock', () => { + editor.setUp('Hello World'); + editor.selectRange(0, 5); + + wrapBlock('~', window.editor); + expect(editor.getText()).toBe('~Hello~ World'); + + wrapBlock('@', window.editor); + expect(editor.getText()).toBe('~@Hello@~ World'); + }); +}); diff --git a/CoreEditor/test/lezer.test.ts b/CoreEditor/test/lezer.test.ts new file mode 100644 index 00000000..cee507e2 --- /dev/null +++ b/CoreEditor/test/lezer.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from '@jest/globals'; +import { EditorView } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; +import * as editor from '../src/@test/editor'; + +describe('Lezer parser', () => { + test('test InlineCode', () => { + editor.setUp('`Hello` World'); + + const types = parseTypes(window.editor); + expect(types).toContain('InlineCode'); + expect(types).toContain('CodeMark'); + }); + + test('test FencedCode', () => { + editor.setUp('```\nHello World\n```'); + + const types = parseTypes(window.editor); + expect(types).toContain('FencedCode'); + expect(types).toContain('CodeMark'); + expect(types).toContain('CodeText'); + }); + + test('test CodeBlock', () => { + editor.setUp(' Hello World'); + + const types = parseTypes(window.editor); + expect(types).toContain('CodeBlock'); + expect(types).toContain('CodeText'); + }); + + test('test ATXHeading', () => { + editor.setUp('## Heading'); + + const types = parseTypes(window.editor); + expect(types).toContain('ATXHeading2'); + expect(types).toContain('HeaderMark'); + }); + + test('test SetextHeading1', () => { + editor.setUp('Heading\n======'); + + const types = parseTypes(window.editor); + expect(types).toContain('SetextHeading1'); + expect(types).toContain('HeaderMark'); + }); + + test('test SetextHeading2', () => { + editor.setUp('Heading\n------'); + + const types = parseTypes(window.editor); + expect(types).toContain('SetextHeading2'); + expect(types).toContain('HeaderMark'); + }); +}); + +function parseTypes(editor: EditorView) { + const types: string[] = []; + syntaxTree(editor.state).iterate({ + enter: node => { + types.push(node.type.name); + }, + }); + + return types; +} diff --git a/CoreEditor/test/lineEndings.test.ts b/CoreEditor/test/lineEndings.test.ts new file mode 100644 index 00000000..249bd9da --- /dev/null +++ b/CoreEditor/test/lineEndings.test.ts @@ -0,0 +1,36 @@ +import { EditorState } from '@codemirror/state'; +import { describe, expect, test } from '@jest/globals'; +import { LineEndings } from '../src/modules/lineEndings/types'; +import * as editor from '../src/@test/editor'; +import * as lineEndings from '../src/modules/lineEndings'; + +describe('LineEndings module', () => { + test('test line-ending normalization', () => { + editor.setUp('Hello\nWorld'); + expect(lineEndings.getLineEndings()).toBe(LineEndings.LF); + + editor.setText('Hello\r\nWorld'); + expect(lineEndings.getLineEndings()).toBe(LineEndings.LF); + + editor.setText('Hello\rWorld'); + expect(lineEndings.getLineEndings()).toBe(LineEndings.LF); + }); + + test('test changing line endings', () => { + editor.setUp('Hello\r\nWorld', EditorState.lineSeparator.of('\r\n')); + expect(lineEndings.getLineEndings()).toBe(LineEndings.CRLF); + + editor.setText('Hello\nWorld'); + expect(lineEndings.getLineEndings()).toBe(LineEndings.CRLF); + }); + + test('test detecting line break', () => { + expect(lineEndings.getLineBreak('Hello\nWorld', '\n')).toBe(undefined); + expect(lineEndings.getLineBreak('Hello\n\r\nWorld', '\n')).toBe('\r\n'); + expect(lineEndings.getLineBreak('Hello\n\n\r\nWorld', '\n')).toBe(undefined); + expect(lineEndings.getLineBreak('Hello\n\r\rWorld', '\n')).toBe('\r'); + expect(lineEndings.getLineBreak('', '\n')).toBe(undefined); + expect(lineEndings.getLineBreak('', '\r')).toBe('\r'); + expect(lineEndings.getLineBreak('', '\r\n')).toBe('\r\n'); + }); +}); diff --git a/CoreEditor/test/search.test.ts b/CoreEditor/test/search.test.ts new file mode 100644 index 00000000..bd7286d9 --- /dev/null +++ b/CoreEditor/test/search.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from '@jest/globals'; +import { SearchQuery } from '@codemirror/search'; + +import rangesFromQuery from '../src/modules/search/rangesFromQuery'; +import searchOccurrences from '../src/modules/search/searchOccurrences'; + +import * as editor from '../src/@test/editor'; +import * as search from '../src/modules/search'; + +describe('Search module', () => { + test('test rangesFromQuery', () => { + editor.setUp('Hello Hello'); + search.setState(true); + + const query = new SearchQuery({ + search: 'Hello', + caseSensitive: false, + literal: false, + regexp: false, + wholeWord: false, + }); + + expect(rangesFromQuery(query)?.length).toBe(2); + }); + + test('test searchOccurrences', () => { + expect(searchOccurrences('Hello, Hello, hello', 'Hello').length).toBe(2); + expect(searchOccurrences('Hello, Hello, hello', 'hello').length).toBe(1); + }); +}); diff --git a/CoreEditor/test/styling.test.ts b/CoreEditor/test/styling.test.ts new file mode 100644 index 00000000..0fed9850 --- /dev/null +++ b/CoreEditor/test/styling.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from '@jest/globals'; +import { gutterExtensions } from '../src/styling/nodes/gutter'; +import * as editor from '../src/@test/editor'; + +describe('Styling module', () => { + test('test CodeMirror class names', async () => { + editor.setUp('Hello World', [ + ...gutterExtensions, + ]); + + await sleep(200); + const elements = [...document.querySelectorAll('*')] as HTMLElement[]; + + const classNames = elements.reduce((acc, cur) => { + [...cur.classList].forEach(cls => acc.add(cls.toString())); + return acc; + }, new Set()); + + expect(classNames.has('cm-editor')).toBeTruthy(); + expect(classNames.has('cm-focused')).toBeTruthy(); + expect(classNames.has('cm-content')).toBeTruthy(); + expect(classNames.has('cm-scroller')).toBeTruthy(); + expect(classNames.has('cm-gutters')).toBeTruthy(); + expect(classNames.has('cm-gutter')).toBeTruthy(); + expect(classNames.has('cm-gutterElement')).toBeTruthy(); + expect(classNames.has('cm-foldGutter')).toBeTruthy(); + expect(classNames.has('cm-line')).toBeTruthy(); + expect(classNames.has('cm-lineNumbers')).toBeTruthy(); + }); +}); + +function sleep(milliseconds: number) { + // eslint-disable-next-line compat/compat + return new Promise(resolve => { + setTimeout(resolve, milliseconds); + }); +} diff --git a/CoreEditor/test/toc.test.ts b/CoreEditor/test/toc.test.ts new file mode 100644 index 00000000..9631539f --- /dev/null +++ b/CoreEditor/test/toc.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from '@jest/globals'; +import * as editor from '../src/@test/editor'; +import * as toc from '../src/modules/toc'; + +describe('Table of contents module', () => { + test('test wrapBlock', () => { + editor.setUp('## Hello\n\n- One\n- Two\n- Three\n\n### MarkEdit\n\nHave fun.'); + const results = toc.getTableOfContents(); + + expect(results[0].level).toBe(2); + expect(results[0].title).toBe('Hello'); + expect(results[1].level).toBe(3); + expect(results[1].title).toBe(' MarkEdit'); + }); +}); diff --git a/CoreEditor/tsconfig.json b/CoreEditor/tsconfig.json new file mode 100644 index 00000000..deae2368 --- /dev/null +++ b/CoreEditor/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "typeRoots": ["./node_modules/@types", "./src/@types"], + "module": "esnext", + "target": "esnext", + "lib": ["es2019", "dom"], + "composite": true, + "noImplicitAny": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "suppressImplicitAnyIndexErrors": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "importHelpers": true, + "skipLibCheck": true + } +} diff --git a/CoreEditor/vite.config.ts b/CoreEditor/vite.config.ts new file mode 100644 index 00000000..556cd90e --- /dev/null +++ b/CoreEditor/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; + +export default defineConfig({ + plugins: [viteSingleFile()], +}); diff --git a/CoreEditor/yarn.lock b/CoreEditor/yarn.lock new file mode 100644 index 00000000..3c7d9491 --- /dev/null +++ b/CoreEditor/yarn.lock @@ -0,0 +1,3810 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.20.5": + version "7.20.14" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" + integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" + integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.7" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helpers" "^7.20.7" + "@babel/parser" "^7.20.7" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.12" + "@babel/types" "^7.20.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/generator@^7.20.7", "@babel/generator@^7.7.2": + version "7.20.14" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" + integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== + dependencies: + "@babel/types" "^7.20.7" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.20.11": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" + integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.10" + "@babel/types" "^7.20.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.20.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" + integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.13" + "@babel/types" "^7.20.7" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.13.tgz#ddf1eb5a813588d2fb1692b70c6fce75b945c088" + integrity sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" + integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.7.2": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" + integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.7" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.13" + "@babel/types" "^7.20.7" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz#76ac9a2a411a4cc6e13103014dba5e0fe601da5a" + integrity sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.6.0" + "@lezer/common" "^1.0.0" + +"@codemirror/commands@^6.0.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.2.0.tgz#e575b7542406be8bc61efb56f1827c71587bb5f8" + integrity sha512-+00smmZBradoGFEkRjliN7BjqPh/Hx0KCHWOEibUmflUqZz2RwBTU0MrVovEEHozhx3AUSGcO/rl3/5f9e9Biw== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.2.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + +"@codemirror/lang-cpp@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz#076c98340c3beabde016d7d83e08eebe17254ef9" + integrity sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/cpp" "^1.0.0" + +"@codemirror/lang-css@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.0.2.tgz#b286d0226755a751f60599e1e2969d351aebbd4c" + integrity sha512-4V4zmUOl2Glx0GWw0HiO1oGD4zvMlIQ3zx5hXOE6ipCjhohig2bhWRAasrZylH9pRNTcl1VMa59Lsl8lZWlTzw== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@lezer/css" "^1.0.0" + +"@codemirror/lang-html@^6.0.0": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.1.tgz#fe739212aa3e4931077da5ab178c4f7df1601ac5" + integrity sha512-9NzhWKAkWEwjXC04DKM6yrHnxIPFTqZNLDhWfZiKLMxUiU++XoHz9n6D5EPp1igBmX0vXcpFb5Kud6XzIJhZ4A== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/lang-css" "^6.0.0" + "@codemirror/lang-javascript" "^6.0.0" + "@codemirror/language" "^6.4.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.2.2" + "@lezer/common" "^1.0.0" + "@lezer/css" "^1.1.0" + "@lezer/html" "^1.3.0" + +"@codemirror/lang-java@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz#03bd06334da7c8feb9dff6db01ac6d85bd2e48bb" + integrity sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/java" "^1.0.0" + +"@codemirror/lang-javascript@^6.0.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.1.2.tgz#a11812ca1d21301cdeb80e51b4c007edcf55f813" + integrity sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/javascript" "^1.0.0" + +"@codemirror/lang-json@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz#0a0be701a5619c4b0f8991f9b5e95fe33f462330" + integrity sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/json" "^1.0.0" + +"@codemirror/lang-markdown@^6.0.0": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.0.5.tgz#61393c7e2552528daee6aa4eca63428aa00832bd" + integrity sha512-qH0THRYc2M7pIJoAp6jstXZkv8ZMVhNaBm7Bs4+0SLHhHlwX53txFy98AcPwrfq0Sh8Zi6RAuj9j/GyL8E1MKw== + dependencies: + "@codemirror/lang-html" "^6.0.0" + "@codemirror/language" "^6.3.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/markdown" "^1.0.0" + +"@codemirror/lang-php@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz#fa34cc75562178325861a5731f79bd621f57ffaa" + integrity sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA== + dependencies: + "@codemirror/lang-html" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/php" "^1.0.0" + +"@codemirror/lang-python@^6.0.0": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-python/-/lang-python-6.1.1.tgz#378c69199da41e0b09eaadc56f6d70ad6001fd34" + integrity sha512-AddGMIKUssUAqaDKoxKWA5GAzy/CVE0eSY7/ANgNzdS1GYBkp6N49XKEyMElkuN04UsZ+bTIQdj+tVV75NMwJw== + dependencies: + "@codemirror/autocomplete" "^6.3.2" + "@codemirror/language" "^6.0.0" + "@lezer/python" "^1.0.0" + +"@codemirror/lang-rust@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz#d6829fc7baa39a15bcd174a41a9e0a1bf7cf6ba8" + integrity sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/rust" "^1.0.0" + +"@codemirror/lang-sql@^6.0.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.4.0.tgz#f9303e511fb9511884f90043e354d5df3bd4b032" + integrity sha512-UWGK1+zc9+JtkiT+XxHByp4N6VLgLvC2x0tIudrJG26gyNtn0hWOVoB0A8kh/NABPWkKl3tLWDYf2qOBJS9Zdw== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@codemirror/lang-wast@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz#c15bec84548a5e9b0a43fa69fb63631d087d6047" + integrity sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@codemirror/lang-xml@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz#66f75390bf8013fd8645db9cdd0b1d177e0777a4" + integrity sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.4.0" + "@codemirror/state" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/xml" "^1.0.0" + +"@codemirror/language-data@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@codemirror/language-data/-/language-data-6.1.0.tgz#479eff66289a6453493f7c8213d7b2ceb95c89f6" + integrity sha512-g9V23fuLRI9AEbpM6bDy1oquqgpFlIDHTihUhL21NPmxp+x67ZJbsKk+V71W7/Bj8SCqEO1PtqQA/tDGgt1nfw== + dependencies: + "@codemirror/lang-cpp" "^6.0.0" + "@codemirror/lang-css" "^6.0.0" + "@codemirror/lang-html" "^6.0.0" + "@codemirror/lang-java" "^6.0.0" + "@codemirror/lang-javascript" "^6.0.0" + "@codemirror/lang-json" "^6.0.0" + "@codemirror/lang-markdown" "^6.0.0" + "@codemirror/lang-php" "^6.0.0" + "@codemirror/lang-python" "^6.0.0" + "@codemirror/lang-rust" "^6.0.0" + "@codemirror/lang-sql" "^6.0.0" + "@codemirror/lang-wast" "^6.0.0" + "@codemirror/lang-xml" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/legacy-modes" "^6.1.0" + +"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.4.0.tgz#803990e0f07bbb619e915651d3a57d143765dbcc" + integrity sha512-Wzb7GnNj8vnEtbPWiOy9H0m1fBtE28kepQNGLXekU2EEZv43BF865VKITUn+NoV8OpW6gRtvm29YEhqm46927Q== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + style-mod "^4.0.0" + +"@codemirror/legacy-modes@^6.1.0": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz#77ab3f3db1ce3e47aad7a5baac3a4b12844734a5" + integrity sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw== + dependencies: + "@codemirror/language" "^6.0.0" + +"@codemirror/lint@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.1.0.tgz#f006142d3a580fdb8ffc2faa3361b2232c08e079" + integrity sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.2.3.tgz#fab933fef1b1de8ef40cda275c73d9ac7a1ff40f" + integrity sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2" + integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA== + +"@codemirror/view@^6.0.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.7.3.tgz#be2f9d0e6fc8882fb192041bf78425ca04999827" + integrity sha512-Lt+4POnhXrZFfHOdPzXEHxrzwdy7cjqYlMkOWvoFGi6/bAsjzlFfr0NY3B15B/PGx+cDFgM1hlc12wvYeZbGLw== + dependencies: + "@codemirror/state" "^6.1.4" + style-mod "^4.0.0" + w3c-keyname "^2.2.4" + +"@esbuild/android-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" + integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== + +"@esbuild/android-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" + integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== + +"@esbuild/android-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" + integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== + +"@esbuild/darwin-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" + integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== + +"@esbuild/darwin-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" + integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== + +"@esbuild/freebsd-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" + integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== + +"@esbuild/freebsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" + integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== + +"@esbuild/linux-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" + integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== + +"@esbuild/linux-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" + integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== + +"@esbuild/linux-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" + integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== + +"@esbuild/linux-loong64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" + integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== + +"@esbuild/linux-mips64el@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" + integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== + +"@esbuild/linux-ppc64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" + integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== + +"@esbuild/linux-riscv64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" + integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== + +"@esbuild/linux-s390x@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" + integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== + +"@esbuild/linux-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" + integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== + +"@esbuild/netbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" + integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== + +"@esbuild/openbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" + integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== + +"@esbuild/sunos-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" + integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== + +"@esbuild/win32-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" + integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== + +"@esbuild/win32-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" + integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== + +"@esbuild/win32-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" + integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== + +"@eslint/eslintrc@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" + integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@grammarly/editor-sdk@^2.0.0": + version "2.3.8" + resolved "https://registry.yarnpkg.com/@grammarly/editor-sdk/-/editor-sdk-2.3.8.tgz#34547370f2f35d7333dc48dfac23a81548772158" + integrity sha512-TBiAwb9v5MrhfByoGOz+qQCxJMBdOleyXj7ROoHqzPB0DixAimQrvLBN27cdBMIrLu1/epNf5pRMZ2TUpFW1pA== + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.1.tgz#cbc31d73f6329f693b3d34b365124de797704fff" + integrity sha512-m+XpwKSi3PPM9znm5NGS8bBReeAJJpSkL1OuFCqaMaJL2YX9YXLkkI+MBchMPwu+ZuM2rynL51sgfkQteQ1CKQ== + dependencies: + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.4.1" + jest-util "^29.4.1" + slash "^3.0.0" + +"@jest/core@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.1.tgz#91371179b5959951e211dfaeea4277a01dcca14f" + integrity sha512-RXFTohpBqpaTebNdg5l3I5yadnKo9zLBajMT0I38D0tDhreVBYv3fA8kywthI00sWxPztWLD3yjiUkewwu/wKA== + dependencies: + "@jest/console" "^29.4.1" + "@jest/reporters" "^29.4.1" + "@jest/test-result" "^29.4.1" + "@jest/transform" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.4.0" + jest-config "^29.4.1" + jest-haste-map "^29.4.1" + jest-message-util "^29.4.1" + jest-regex-util "^29.2.0" + jest-resolve "^29.4.1" + jest-resolve-dependencies "^29.4.1" + jest-runner "^29.4.1" + jest-runtime "^29.4.1" + jest-snapshot "^29.4.1" + jest-util "^29.4.1" + jest-validate "^29.4.1" + jest-watcher "^29.4.1" + micromatch "^4.0.4" + pretty-format "^29.4.1" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.1.tgz#52d232a85cdc995b407a940c89c86568f5a88ffe" + integrity sha512-pJ14dHGSQke7Q3mkL/UZR9ZtTOxqskZaC91NzamEH4dlKRt42W+maRBXiw/LWkdJe+P0f/zDR37+SPMplMRlPg== + dependencies: + "@jest/fake-timers" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + jest-mock "^29.4.1" + +"@jest/expect-utils@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.1.tgz#105b9f3e2c48101f09cae2f0a4d79a1b3a419cbb" + integrity sha512-w6YJMn5DlzmxjO00i9wu2YSozUYRBhIoJ6nQwpMYcBMtiqMGJm1QBzOf6DDgRao8dbtpDoaqLg6iiQTvv0UHhQ== + dependencies: + jest-get-type "^29.2.0" + +"@jest/expect@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.1.tgz#3338fa20f547bb6e550c4be37d6f82711cc13c38" + integrity sha512-ZxKJP5DTUNF2XkpJeZIzvnzF1KkfrhEF6Rz0HGG69fHl6Bgx5/GoU3XyaeFYEjuuKSOOsbqD/k72wFvFxc3iTw== + dependencies: + expect "^29.4.1" + jest-snapshot "^29.4.1" + +"@jest/fake-timers@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.1.tgz#7b673131e8ea2a2045858f08241cace5d518b42b" + integrity sha512-/1joI6rfHFmmm39JxNfmNAO3Nwm6Y0VoL5fJDy7H1AtWrD1CgRtqJbN9Ld6rhAkGO76qqp4cwhhxJ9o9kYjQMw== + dependencies: + "@jest/types" "^29.4.1" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.4.1" + jest-mock "^29.4.1" + jest-util "^29.4.1" + +"@jest/globals@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.1.tgz#3cd78c5567ab0249f09fbd81bf9f37a7328f4713" + integrity sha512-znoK2EuFytbHH0ZSf2mQK2K1xtIgmaw4Da21R2C/NE/+NnItm5mPEFQmn8gmF3f0rfOlmZ3Y3bIf7bFj7DHxAA== + dependencies: + "@jest/environment" "^29.4.1" + "@jest/expect" "^29.4.1" + "@jest/types" "^29.4.1" + jest-mock "^29.4.1" + +"@jest/reporters@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.1.tgz#50d509c08575c75e3cd2176d72ec3786419d5e04" + integrity sha512-AISY5xpt2Xpxj9R6y0RF1+O6GRy9JsGa8+vK23Lmzdy1AYcpQn5ItX79wJSsTmfzPKSAcsY1LNt/8Y5Xe5LOSg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.4.1" + "@jest/test-result" "^29.4.1" + "@jest/transform" "^29.4.1" + "@jest/types" "^29.4.1" + "@jridgewell/trace-mapping" "^0.3.15" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.4.1" + jest-util "^29.4.1" + jest-worker "^29.4.1" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.0.tgz#0d6ad358f295cc1deca0b643e6b4c86ebd539f17" + integrity sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/source-map@^29.2.0": + version "29.2.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" + integrity sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.15" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.1.tgz#997f19695e13b34779ceb3c288a416bd26c3238d" + integrity sha512-WRt29Lwt+hEgfN8QDrXqXGgCTidq1rLyFqmZ4lmJOpVArC8daXrZWkWjiaijQvgd3aOUj2fM8INclKHsQW9YyQ== + dependencies: + "@jest/console" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.1.tgz#f7a006ec7058b194a10cf833c88282ef86d578fd" + integrity sha512-v5qLBNSsM0eHzWLXsQ5fiB65xi49A3ILPSFQKPXzGL4Vyux0DPZAIN7NAFJa9b4BiTDP9MBF/Zqc/QA1vuiJ0w== + dependencies: + "@jest/test-result" "^29.4.1" + graceful-fs "^4.2.9" + jest-haste-map "^29.4.1" + slash "^3.0.0" + +"@jest/transform@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.1.tgz#e4f517841bb795c7dcdee1ba896275e2c2d26d4a" + integrity sha512-5w6YJrVAtiAgr0phzKjYd83UPbCXsBRTeYI4BXokv9Er9CcrH9hfXL/crCvP2d2nGOcovPUnlYiLPFLZrkG5Hg== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.4.1" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.4.1" + jest-regex-util "^29.2.0" + jest-util "^29.4.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^5.0.0" + +"@jest/types@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.1.tgz#f9f83d0916f50696661da72766132729dcb82ecb" + integrity sha512-zbrAXDUOnpJ+FMST2rV7QZOgec8rskg2zv8g2ajeqitp4tvZiyqTCYXANrKsM+ryj5o+LI+ZN2EgU9drrkiwSA== + dependencies: + "@jest/schemas" "^29.4.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@lezer/common@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.2.tgz#8fb9b86bdaa2ece57e7d59e5ffbcb37d71815087" + integrity sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng== + +"@lezer/cpp@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/cpp/-/cpp-1.0.0.tgz#3293fd88aaf16a6d4f18188602b4d931be8f0915" + integrity sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/css@^1.0.0", "@lezer/css@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.1.tgz#c36dcb0789317cb80c3740767dd3b85e071ad082" + integrity sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.3.tgz#bf5a36c2ee227f526d74997ac91f7777e29bd25d" + integrity sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/html@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.0.tgz#7e353dcefcdee9c4d3cd6bfc59416d0037f70c4c" + integrity sha512-jU/ah8DEoiECLTMouU/X/ujIg6k9WQMIOFMaCLebzaXfrguyGaR3DpTgmk0tbljiuIJ7hlmVJPcJcxGzmCd0Mg== + dependencies: + "@lezer/common" "^1.0.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/java@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/java/-/java-1.0.0.tgz#fe74e062350f7a4268107e7562971bfbad994f49" + integrity sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/javascript@^1.0.0": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.1.tgz#97a15042c76b5979af6a069fac83cf6485628cbf" + integrity sha512-Hqx36DJeYhKtdpc7wBYPR0XF56ZzIp0IkMO/zNNj80xcaFOV4Oj/P7TQc/8k2TxNhzl7tV5tXS8ZOCPbT4L3nA== + dependencies: + "@lezer/highlight" "^1.1.3" + "@lezer/lr" "^1.3.0" + +"@lezer/json@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.0.tgz#848ad9c2c3e812518eb02897edd5a7f649e9c160" + integrity sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/lr@^1.0.0", "@lezer/lr@^1.1.0", "@lezer/lr@^1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.2.tgz#355183318e330be05b94fed3fd7f7fd501dcef57" + integrity sha512-SDSvnHWMBH6WxoOt51AjuHOiQ0DMxxhfK5lNoyJXuv5POWz6MfXKGU9Fus9tK8NqrI1sSBNdKtG5cUXXZtGG5w== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/markdown@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@lezer/markdown/-/markdown-1.0.2.tgz#8c804a9f6fe1ccca4a20acd2fd9fbe0fae1ae178" + integrity sha512-8CY0OoZ6V5EzPjSPeJ4KLVbtXdLBd8V6sRCooN5kHnO28ytreEGTyrtU/zUwo/XLRzGr/e1g44KlzKi3yWGB5A== + dependencies: + "@lezer/common" "^1.0.0" + "@lezer/highlight" "^1.0.0" + +"@lezer/php@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.1.tgz#4496b58c980ca710c0433fd743d27e9964fd74ea" + integrity sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.1.0" + +"@lezer/python@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@lezer/python/-/python-1.1.1.tgz#6d688071ed93d063a589a7d31df3279b1eba607a" + integrity sha512-ArUGh9kvdaOVu6IkSaYUS9WFQeMAFVWKRuZo6vexnxoeCLnxf0Y9DCFEAMMa7W9SQBGYE55OarSpPqSkdOXSCA== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/rust@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/rust/-/rust-1.0.0.tgz#939f3e7b0376ebe13f4ac336ed7d59ca2c8adf52" + integrity sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/xml@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lezer/xml/-/xml-1.0.1.tgz#c4c738a407db610f0e9c59d0e9b16607cd029591" + integrity sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@mdn/browser-compat-data@^3.3.14": + version "3.3.14" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28" + integrity sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA== + +"@mdn/browser-compat-data@^4.1.5": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8" + integrity sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/babel__core@^7.1.14": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" + integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" + integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + dependencies: + "@babel/types" "^7.3.0" + +"@types/eslint@^8.4.10": + version "8.4.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.10.tgz#19731b9685c19ed1552da7052b6f668ed7eb64bb" + integrity sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206" + integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/node@*": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + +"@types/prettier@^2.1.5": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== + +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/tough-cookie@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + +"@types/uuid@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" + integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.22" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" + integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz#fb48c31cadc853ffc1dc35373f56b5e2a8908fe9" + integrity sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ== + dependencies: + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/type-utils" "5.50.0" + "@typescript-eslint/utils" "5.50.0" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.50.0.tgz#a33f44b2cc83d1b7176ec854fbecd55605b0b032" + integrity sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ== + dependencies: + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/typescript-estree" "5.50.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz#90b8a3b337ad2c52bbfe4eac38f9164614e40584" + integrity sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg== + dependencies: + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/visitor-keys" "5.50.0" + +"@typescript-eslint/type-utils@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz#509d5cc9728d520008f7157b116a42c5460e7341" + integrity sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ== + dependencies: + "@typescript-eslint/typescript-estree" "5.50.0" + "@typescript-eslint/utils" "5.50.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.50.0.tgz#c461d3671a6bec6c2f41f38ed60bd87aa8a30093" + integrity sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w== + +"@typescript-eslint/typescript-estree@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz#0b9b82975bdfa40db9a81fdabc7f93396867ea97" + integrity sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow== + dependencies: + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/visitor-keys" "5.50.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.50.0.tgz#807105f5ffb860644d30d201eefad7017b020816" + integrity sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/typescript-estree" "5.50.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz#b752ffc143841f3d7bc57d6dd01ac5c40f8c4903" + integrity sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg== + dependencies: + "@typescript-eslint/types" "5.50.0" + eslint-visitor-keys "^3.3.0" + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.0.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.1.0, acorn@^8.8.0, acorn@^8.8.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +ast-metadata-inferer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/ast-metadata-inferer/-/ast-metadata-inferer-0.7.0.tgz#c45d874cbdecabea26dc5de11fc6fa1919807c66" + integrity sha512-OkMLzd8xelb3gmnp6ToFvvsHLtS6CbagTkFQvQ+ZYFe3/AIl9iKikNR9G7pY3GfOR/2Xc222hwBjzI7HLkE76Q== + dependencies: + "@mdn/browser-compat-data" "^3.3.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +babel-jest@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.1.tgz#01fa167e27470b35c2d4a1b841d9586b1764da19" + integrity sha512-xBZa/pLSsF/1sNpkgsiT3CmY7zV1kAsZ9OxxtrFqYucnOuRftXAfcJqcDVyOPeN4lttWTwhLdu0T9f8uvoPEUg== + dependencies: + "@jest/transform" "^29.4.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.4.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.0.tgz#3fd3dfcedf645932df6d0c9fc3d9a704dd860248" + integrity sha512-a/sZRLQJEmsmejQ2rPEUe35nO1+C9dc9O1gplH1SXmJxveQSRUYdBk8yGZG/VOUuZs1u2aHZJusEGoRMbhhwCg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.0.tgz#c2b03c548b02dea0a18ae21d5759c136f9251ee4" + integrity sha512-fUB9vZflUSM3dO/6M2TCAepTzvA4VkOvl67PjErcrQMGt9Eve7uazaeyCZ2th3UtI7ljpiBJES0F7A1vBRsLZA== + dependencies: + babel-plugin-jest-hoist "^29.4.0" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.16.8, browserslist@^4.21.3: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + dependencies: + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001304, caniuse-lite@^1.0.30001449: + version "1.0.30001450" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" + integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" + integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js@^3.16.2: + version "3.27.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7" + integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w== + +crelt@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" + integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" + integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" + integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + +electron-to-chromium@^1.4.284: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +entities@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +esbuild@^0.16.3: + version "0.16.17" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" + integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== + optionalDependencies: + "@esbuild/android-arm" "0.16.17" + "@esbuild/android-arm64" "0.16.17" + "@esbuild/android-x64" "0.16.17" + "@esbuild/darwin-arm64" "0.16.17" + "@esbuild/darwin-x64" "0.16.17" + "@esbuild/freebsd-arm64" "0.16.17" + "@esbuild/freebsd-x64" "0.16.17" + "@esbuild/linux-arm" "0.16.17" + "@esbuild/linux-arm64" "0.16.17" + "@esbuild/linux-ia32" "0.16.17" + "@esbuild/linux-loong64" "0.16.17" + "@esbuild/linux-mips64el" "0.16.17" + "@esbuild/linux-ppc64" "0.16.17" + "@esbuild/linux-riscv64" "0.16.17" + "@esbuild/linux-s390x" "0.16.17" + "@esbuild/linux-x64" "0.16.17" + "@esbuild/netbsd-x64" "0.16.17" + "@esbuild/openbsd-x64" "0.16.17" + "@esbuild/sunos-x64" "0.16.17" + "@esbuild/win32-arm64" "0.16.17" + "@esbuild/win32-ia32" "0.16.17" + "@esbuild/win32-x64" "0.16.17" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-plugin-compat@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.0.2.tgz#b058627a7d25d352adf0ec16dca8fcf92d9c7af7" + integrity sha512-xqvoO54CLTVaEYGMzhu35Wzwk/As7rCvz/2dqwnFiWi0OJccEtGIn+5qq3zqIu9nboXlpdBN579fZcItC73Ycg== + dependencies: + "@mdn/browser-compat-data" "^4.1.5" + ast-metadata-inferer "^0.7.0" + browserslist "^4.16.8" + caniuse-lite "^1.0.30001304" + core-js "^3.16.2" + find-up "^5.0.0" + lodash.memoize "4.1.2" + semver "7.3.5" + +eslint-plugin-promise@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" + integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.33.0: + version "8.33.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.33.0.tgz#02f110f32998cb598c6461f24f4d306e41ca33d7" + integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== + dependencies: + "@eslint/eslintrc" "^1.4.1" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.1.tgz#58cfeea9cbf479b64ed081fd1e074ac8beb5a1fe" + integrity sha512-OKrGESHOaMxK3b6zxIq9SOW8kEXztKff/Dvg88j4xIJxur1hspEbedVkR3GpHe5LO+WB2Qw7OWN0RMTdp6as5A== + dependencies: + "@jest/expect-utils" "^29.4.1" + jest-get-type "^29.2.0" + jest-matcher-utils "^29.4.1" + jest-message-util "^29.4.1" + jest-util "^29.4.1" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.0.tgz#ac2498bcd394228f7eddcadcf928b3583bf2779d" + integrity sha512-rnI1oPxgFghoz32Y8eZsGJMjW54UlqT17ycQeCEktcxxwqqKdlj9afl8LNeO0Pbu+h2JQHThQP0BzS67eTRx4w== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.1.tgz#ff1b63eb04c3b111cefea9489e8dbadd23ce49bd" + integrity sha512-v02NuL5crMNY4CGPHBEflLzl4v91NFb85a+dH9a1pUNx6Xjggrd8l9pPy4LZ1VYNRXlb+f65+7O/MSIbLir6pA== + dependencies: + "@jest/environment" "^29.4.1" + "@jest/expect" "^29.4.1" + "@jest/test-result" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.4.1" + jest-matcher-utils "^29.4.1" + jest-message-util "^29.4.1" + jest-runtime "^29.4.1" + jest-snapshot "^29.4.1" + jest-util "^29.4.1" + p-limit "^3.1.0" + pretty-format "^29.4.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.1.tgz#7abef96944f300feb9b76f68b1eb2d68774fe553" + integrity sha512-jz7GDIhtxQ37M+9dlbv5K+/FVcIo1O/b1sX3cJgzlQUf/3VG25nvuWzlDC4F1FLLzUThJeWLu8I7JF9eWpuURQ== + dependencies: + "@jest/core" "^29.4.1" + "@jest/test-result" "^29.4.1" + "@jest/types" "^29.4.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.4.1" + jest-util "^29.4.1" + jest-validate "^29.4.1" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.1.tgz#e62670c6c980ec21d75941806ec4d0c0c6402728" + integrity sha512-g7p3q4NuXiM4hrS4XFATTkd+2z0Ml2RhFmFPM8c3WyKwVDNszbl4E7cV7WIx1YZeqqCtqbtTtZhGZWJlJqngzg== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.4.1" + "@jest/types" "^29.4.1" + babel-jest "^29.4.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.4.1" + jest-environment-node "^29.4.1" + jest-get-type "^29.2.0" + jest-regex-util "^29.2.0" + jest-resolve "^29.4.1" + jest-runner "^29.4.1" + jest-util "^29.4.1" + jest-validate "^29.4.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.4.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.1.tgz#9a6dc715037e1fa7a8a44554e7d272088c4029bd" + integrity sha512-uazdl2g331iY56CEyfbNA0Ut7Mn2ulAG5vUaEHXycf1L6IPyuImIxSz4F0VYBKi7LYIuxOwTZzK3wh5jHzASMw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.3.1" + jest-get-type "^29.2.0" + pretty-format "^29.4.1" + +jest-docblock@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.2.0.tgz#307203e20b637d97cee04809efc1d43afc641e82" + integrity sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.1.tgz#05ce9979e7486dbd0f5d41895f49ccfdd0afce01" + integrity sha512-QlYFiX3llJMWUV0BtWht/esGEz9w+0i7BHwODKCze7YzZzizgExB9MOfiivF/vVT0GSQ8wXLhvHXh3x2fVD4QQ== + dependencies: + "@jest/types" "^29.4.1" + chalk "^4.0.0" + jest-get-type "^29.2.0" + jest-util "^29.4.1" + pretty-format "^29.4.1" + +jest-environment-jsdom@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.4.1.tgz#34d491244ddd6fe3d666da603b576bd0ae6aef78" + integrity sha512-+KfYmRTl5CBHQst9hIz77TiiriHYvuWoLjMT855gx2AMxhHxpk1vtKvag1DQfyWCPVTWV/AG7SIqVh5WI1O/uw== + dependencies: + "@jest/environment" "^29.4.1" + "@jest/fake-timers" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.4.1" + jest-util "^29.4.1" + jsdom "^20.0.0" + +jest-environment-node@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.1.tgz#22550b7d0f8f0b16228639c9f88ca04bbf3c1974" + integrity sha512-x/H2kdVgxSkxWAIlIh9MfMuBa0hZySmfsC5lCsWmWr6tZySP44ediRKDUiNggX/eHLH7Cd5ZN10Rw+XF5tXsqg== + dependencies: + "@jest/environment" "^29.4.1" + "@jest/fake-timers" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + jest-mock "^29.4.1" + jest-util "^29.4.1" + +jest-get-type@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" + integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== + +jest-haste-map@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.1.tgz#b0579dc82d94b40ed9041af56ad25c2f80bedaeb" + integrity sha512-imTjcgfVVTvg02khXL11NNLTx9ZaofbAWhilrMg/G8dIkp+HYCswhxf0xxJwBkfhWb3e8dwbjuWburvxmcr58w== + dependencies: + "@jest/types" "^29.4.1" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.2.0" + jest-util "^29.4.1" + jest-worker "^29.4.1" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.1.tgz#632186c546e084da2b490b7496fee1a1c9929637" + integrity sha512-akpZv7TPyGMnH2RimOCgy+hPmWZf55EyFUvymQ4LMsQP8xSPlZumCPtXGoDhFNhUE2039RApZkTQDKU79p/FiQ== + dependencies: + jest-get-type "^29.2.0" + pretty-format "^29.4.1" + +jest-matcher-utils@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.1.tgz#73d834e305909c3b43285fbc76f78bf0ad7e1954" + integrity sha512-k5h0u8V4nAEy6lSACepxL/rw78FLDkBnXhZVgFneVpnJONhb2DhZj/Gv4eNe+1XqQ5IhgUcqj745UwH0HJmMnA== + dependencies: + chalk "^4.0.0" + jest-diff "^29.4.1" + jest-get-type "^29.2.0" + pretty-format "^29.4.1" + +jest-message-util@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.1.tgz#522623aa1df9a36ebfdffb06495c7d9d19e8a845" + integrity sha512-H4/I0cXUaLeCw6FM+i4AwCnOwHRgitdaUFOdm49022YD5nfyr8C/DrbXOBEyJaj+w/y0gGJ57klssOaUiLLQGQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.4.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.4.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.1.tgz#a218a2abf45c99c501d4665207748a6b9e29afbd" + integrity sha512-MwA4hQ7zBOcgVCVnsM8TzaFLVUD/pFWTfbkY953Y81L5ret3GFRZtmPmRFAjKQSdCKoJvvqOu6Bvfpqlwwb0dQ== + dependencies: + "@jest/types" "^29.4.1" + "@types/node" "*" + jest-util "^29.4.1" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" + integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== + +jest-resolve-dependencies@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.1.tgz#02420a2e055da105e5fca8218c471d8b9553c904" + integrity sha512-Y3QG3M1ncAMxfjbYgtqNXC5B595zmB6e//p/qpA/58JkQXu/IpLDoLeOa8YoYfsSglBKQQzNUqtfGJJT/qLmJg== + dependencies: + jest-regex-util "^29.2.0" + jest-snapshot "^29.4.1" + +jest-resolve@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.1.tgz#4c6bf71a07b8f0b79c5fdf4f2a2cf47317694c5e" + integrity sha512-j/ZFNV2lm9IJ2wmlq1uYK0Y/1PiyDq9g4HEGsNTNr3viRbJdV+8Lf1SXIiLZXFvyiisu0qUyIXGBnw+OKWkJwQ== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.4.1" + jest-pnp-resolver "^1.2.2" + jest-util "^29.4.1" + jest-validate "^29.4.1" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.1.tgz#57460d9ebb0eea2e27eeddca1816cf8537469661" + integrity sha512-8d6XXXi7GtHmsHrnaqBKWxjKb166Eyj/ksSaUYdcBK09VbjPwIgWov1VwSmtupCIz8q1Xv4Qkzt/BTo3ZqiCeg== + dependencies: + "@jest/console" "^29.4.1" + "@jest/environment" "^29.4.1" + "@jest/test-result" "^29.4.1" + "@jest/transform" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.2.0" + jest-environment-node "^29.4.1" + jest-haste-map "^29.4.1" + jest-leak-detector "^29.4.1" + jest-message-util "^29.4.1" + jest-resolve "^29.4.1" + jest-runtime "^29.4.1" + jest-util "^29.4.1" + jest-watcher "^29.4.1" + jest-worker "^29.4.1" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.1.tgz#9a50f9c69d3a391690897c01b0bfa8dc5dd45808" + integrity sha512-UXTMU9uKu2GjYwTtoAw5rn4STxWw/nadOfW7v1sx6LaJYa3V/iymdCLQM6xy3+7C6mY8GfX22vKpgxY171UIoA== + dependencies: + "@jest/environment" "^29.4.1" + "@jest/fake-timers" "^29.4.1" + "@jest/globals" "^29.4.1" + "@jest/source-map" "^29.2.0" + "@jest/test-result" "^29.4.1" + "@jest/transform" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.4.1" + jest-message-util "^29.4.1" + jest-mock "^29.4.1" + jest-regex-util "^29.2.0" + jest-resolve "^29.4.1" + jest-snapshot "^29.4.1" + jest-util "^29.4.1" + semver "^7.3.5" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.1.tgz#5692210b3690c94f19317913d4082b123bd83dd9" + integrity sha512-l4iV8EjGgQWVz3ee/LR9sULDk2pCkqb71bjvlqn+qp90lFwpnulHj4ZBT8nm1hA1C5wowXLc7MGnw321u0tsYA== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.4.1" + "@jest/transform" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.4.1" + graceful-fs "^4.2.9" + jest-diff "^29.4.1" + jest-get-type "^29.2.0" + jest-haste-map "^29.4.1" + jest-matcher-utils "^29.4.1" + jest-message-util "^29.4.1" + jest-util "^29.4.1" + natural-compare "^1.4.0" + pretty-format "^29.4.1" + semver "^7.3.5" + +jest-util@^29.0.0, jest-util@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.1.tgz#2eeed98ff4563b441b5a656ed1a786e3abc3e4c4" + integrity sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ== + dependencies: + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.1.tgz#0d5174510415083ec329d4f981bf6779211f17e9" + integrity sha512-qNZXcZQdIQx4SfUB/atWnI4/I2HUvhz8ajOSYUu40CSmf9U5emil8EDHgE7M+3j9/pavtk3knlZBDsgFvv/SWw== + dependencies: + "@jest/types" "^29.4.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.2.0" + leven "^3.1.0" + pretty-format "^29.4.1" + +jest-watcher@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.1.tgz#6e3e2486918bd778849d4d6e67fd77b814f3e6ed" + integrity sha512-vFOzflGFs27nU6h8dpnVRER3O2rFtL+VMEwnG0H3KLHcllLsU8y9DchSh0AL/Rg5nN1/wSiQ+P4ByMGpuybaVw== + dependencies: + "@jest/test-result" "^29.4.1" + "@jest/types" "^29.4.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.4.1" + string-length "^4.0.1" + +jest-worker@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.1.tgz#7cb4a99a38975679600305650f86f4807460aab1" + integrity sha512-O9doU/S1EBe+yp/mstQ0VpPwpv0Clgn68TkNwGxL6/usX/KUW9Arnn4ag8C3jc6qHcXznhsT5Na1liYzAsuAbQ== + dependencies: + "@types/node" "*" + jest-util "^29.4.1" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.1.tgz#bb34baca8e05901b49c02c62f1183a6182ea1785" + integrity sha512-cknimw7gAXPDOmj0QqztlxVtBVCw2lYY9CeIE5N6kD+kET1H4H79HSNISJmijb1HF+qk+G+ploJgiDi5k/fRlg== + dependencies: + "@jest/core" "^29.4.1" + "@jest/types" "^29.4.1" + import-local "^3.0.2" + jest-cli "^29.4.1" + +js-sdsl@^4.1.4: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.memoize@4.1.2, lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.8: + version "2.0.9" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.9.tgz#fe66405285382b0c4ac6bcfbfbe7e8a510650b4d" + integrity sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nwsapi@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" + integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@^7.0.0, parse5@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +postcss@^8.4.20: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +pretty-format@^29.0.0, pretty-format@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.1.tgz#0da99b532559097b8254298da7c75a0785b1751c" + integrity sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg== + dependencies: + "@jest/schemas" "^29.4.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" + integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + +resolve@^1.20.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^3.0.0, rollup@^3.7.0: + version "3.12.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.12.1.tgz#2975b97713e4af98c15e7024b88292d7fddb3853" + integrity sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig== + optionalDependencies: + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + +semver@7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +semver@7.x, semver@^7.3.5, semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-mod@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01" + integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tough-cookie@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +ts-gyb@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/ts-gyb/-/ts-gyb-0.8.0.tgz#f6e05e680567a7813736d0c5a617a9a43de97fd8" + integrity sha512-v+h7R9nTTFMdT3uJZnqwOC5weMMcRMmsXkHkTmN0aICGLCawyQRr5oGPgmCCRdycMcDgKvFE0gzzC1RCRuwmjA== + dependencies: + chalk "^4.1.1" + glob "^7.1.6" + mustache "^4.2.0" + typescript "^4.3.2" + yargs "^16.1.0" + +ts-jest@^29.0.5: + version "29.0.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" + integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@^4.0.0, typescript@^4.3.2: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +update-browserslist-db@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + +vite-plugin-singlefile@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/vite-plugin-singlefile/-/vite-plugin-singlefile-0.13.2.tgz#f2c3d69f8cded833e9fdc31eddcd3a758c197166" + integrity sha512-HAvrU9mxasNMn/YF0Hb9NjsWDstCWe4iLQ6IR5ppOiNMvXjcyqU3C9SDQ32xnonx3Y04JUGjD2bGiT6q0S9T8w== + dependencies: + micromatch "^4.0.5" + +vite@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.4.tgz#4612ce0b47bbb233a887a54a4ae0c6e240a0da31" + integrity sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw== + dependencies: + esbuild "^0.16.3" + postcss "^8.4.20" + resolve "^1.22.1" + rollup "^3.7.0" + optionalDependencies: + fsevents "~2.3.2" + +w3c-keyname@^2.2.4: + version "2.2.6" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f" + integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg== + +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0" + integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^8.11.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" + integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^16.1.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.3.1: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/MarkEdit.xcodeproj/project.pbxproj b/MarkEdit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..930b407b --- /dev/null +++ b/MarkEdit.xcodeproj/project.pbxproj @@ -0,0 +1,1061 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 87067E2729755D5C0052E795 /* EditorStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87067E2629755D5C0052E795 /* EditorStatusView.swift */; }; + 870A51AA294700E00095C7EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 870A51A9294700E00095C7EB /* Assets.xcassets */; }; + 870A51AD294700E00095C7EB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 870A51AB294700E00095C7EB /* Main.storyboard */; }; + 870A51B9294702AF0095C7EB /* EditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870A51B4294702AF0095C7EB /* EditorViewController.swift */; }; + 870A51BA294702AF0095C7EB /* EditorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870A51B5294702AF0095C7EB /* EditorWindowController.swift */; }; + 870A51BC294702AF0095C7EB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870A51B7294702AF0095C7EB /* AppDelegate.swift */; }; + 870A51BD294702AF0095C7EB /* EditorDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870A51B8294702AF0095C7EB /* EditorDocument.swift */; }; + 870A51C9294752530095C7EB /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 870A51C8294752530095C7EB /* index.html */; }; + 870F17662984D2C000003DA7 /* EditorViewController+LineEndings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870F17652984D2C000003DA7 /* EditorViewController+LineEndings.swift */; }; + 870F17682985285B00003DA7 /* EditorViewController+Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870F17672985285B00003DA7 /* EditorViewController+Config.swift */; }; + 870F177D29855CA500003DA7 /* MarkEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 870F177C29855CA500003DA7 /* MarkEditKit */; }; + 870F178E2985775B00003DA7 /* AppKitControls in Frameworks */ = {isa = PBXBuildFile; productRef = 870F178D2985775B00003DA7 /* AppKitControls */; }; + 870F17902985775B00003DA7 /* AppKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 870F178F2985775B00003DA7 /* AppKitExtensions */; }; + 870F17922985775B00003DA7 /* Previewer in Frameworks */ = {isa = PBXBuildFile; productRef = 870F17912985775B00003DA7 /* Previewer */; }; + 870F17942985775B00003DA7 /* Proofing in Frameworks */ = {isa = PBXBuildFile; productRef = 870F17932985775B00003DA7 /* Proofing */; }; + 870F17962985775B00003DA7 /* SettingsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 870F17952985775B00003DA7 /* SettingsUI */; }; + 870F179B2985783500003DA7 /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 870F179A2985783500003DA7 /* index.html */; }; + 870FF83A298BD7DC00E4AE99 /* MarkEditMacTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870FF839298BD7DC00E4AE99 /* MarkEditMacTests.swift */; }; + 871C995B2973F1E6003FF3A7 /* AppDelegate+Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 871C995A2973F1E6003FF3A7 /* AppDelegate+Document.swift */; }; + 871C995D2973F20E003FF3A7 /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 871C995C2973F20E003FF3A7 /* AppDelegate+Menu.swift */; }; + 8725E0802977EEB3006A4961 /* EditorToolbarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8725E07F2977EEB3006A4961 /* EditorToolbarItems.swift */; }; + 8725E0822977EF46006A4961 /* EditorViewController+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8725E0812977EF46006A4961 /* EditorViewController+Toolbar.swift */; }; + 872924062951525C007156B1 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872924052951525C007156B1 /* PreviewViewController.swift */; }; + 8729240B2951525C007156B1 /* Main.xib in Resources */ = {isa = PBXBuildFile; fileRef = 872924092951525C007156B1 /* Main.xib */; }; + 872924102951525C007156B1 /* PreviewExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 872924002951525C007156B1 /* PreviewExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8729C5A6294B5E6B006F262B /* EditorViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8729C5A5294B5E6B006F262B /* EditorViewController+Menu.swift */; }; + 873126E4297BAC9C001521A0 /* EditorViewController+Pandoc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873126E3297BAC9C001521A0 /* EditorViewController+Pandoc.swift */; }; + 873ACD4729644E2F00431498 /* EditorViewController+Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873ACD4629644E2F00431498 /* EditorViewController+Encoding.swift */; }; + 873D6402295FCC3E0095DEF7 /* AppResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873D6401295FCC3E0095DEF7 /* AppResources.swift */; }; + 873D6438295FEF220095DEF7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 873D643A295FEF220095DEF7 /* Localizable.strings */; }; + 874AAE2E2959F197000E0E84 /* EditorReplacePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874AAE2D2959F197000E0E84 /* EditorReplacePanel.swift */; }; + 8752CEB6295B2F8800BA9D5B /* EditorPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8752CEB5295B2F8800BA9D5B /* EditorPanelView.swift */; }; + 8767BBAF295A8BA500BFACAE /* EditorViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8767BBAE295A8BA500BFACAE /* EditorViewController+UI.swift */; }; + 8767BBB1295A8C1D00BFACAE /* EditorViewController+Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8767BBB0295A8C1D00BFACAE /* EditorViewController+Delegate.swift */; }; + 8767BBB5295AD62700BFACAE /* EditorReplaceButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8767BBB4295AD62700BFACAE /* EditorReplaceButtons.swift */; }; + 8772E385294AC60D00111E83 /* EditorReusePool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8772E384294AC60D00111E83 /* EditorReusePool.swift */; }; + 8775A77B29867012005867BF /* FontPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 8775A77A29867012005867BF /* FontPicker */; }; + 8780AC182982554800065EF4 /* SettingTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8780AC172982554800065EF4 /* SettingTabs.swift */; }; + 8780AC1C29829EDD00065EF4 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8780AC1B29829EDD00065EF4 /* GeneralSettingsView.swift */; }; + 8780AC1E2982A2C900065EF4 /* EditorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8780AC1D2982A2C900065EF4 /* EditorSettingsView.swift */; }; + 8780AC202982A2DD00065EF4 /* WindowSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8780AC1F2982A2DD00065EF4 /* WindowSettingsView.swift */; }; + 87850C8629482A8600D1A952 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87850C8529482A8600D1A952 /* NSApplication+Extension.swift */; }; + 8790B6EC297036B2000DFC1A /* EditorWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8790B6EB297036B2000DFC1A /* EditorWindow.swift */; }; + 87A73AB3294C4B27006A710E /* EditorWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A73AB2294C4B27006A710E /* EditorWebView.swift */; }; + 87A73AB5294C4B48006A710E /* EditorFindPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A73AB4294C4B48006A710E /* EditorFindPanel.swift */; }; + 87AA1224296560AE00BE4719 /* EditorViewController+HyperLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA1223296560AE00BE4719 /* EditorViewController+HyperLink.swift */; }; + 87AAEDF52958258C00C8E61C /* EditorFindPanel+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AAEDF42958258C00C8E61C /* EditorFindPanel+UI.swift */; }; + 87AAEDF7295825D800C8E61C /* EditorFindPanel+Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AAEDF6295825D800C8E61C /* EditorFindPanel+Delegate.swift */; }; + 87AAEDF929582C2700C8E61C /* EditorFindPanel+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AAEDF829582C2700C8E61C /* EditorFindPanel+Menu.swift */; }; + 87AAEE0229585ADC00C8E61C /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AAEE0129585ADC00C8E61C /* AppPreferences.swift */; }; + 87BD071229699A290053EF5F /* EditorViewController+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BD071129699A290053EF5F /* EditorViewController+Preview.swift */; }; + 87BDF6E42976C97100548079 /* EditorViewController+GotoLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BDF6E32976C97100548079 /* EditorViewController+GotoLine.swift */; }; + 87DE081B294DDA49004AD33A /* AppTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DE081A294DDA49004AD33A /* AppTheme.swift */; }; + 87DE0825294DF340004AD33A /* EditorFindButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DE0824294DF340004AD33A /* EditorFindButtons.swift */; }; + 87DE084B294E22C5004AD33A /* EditorViewController+TextFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DE084A294E22C5004AD33A /* EditorViewController+TextFinder.swift */; }; + 87EB9901295755DA00A56B97 /* MarkEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 87EB9900295755DA00A56B97 /* MarkEditCore */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 870FF83B298BD7DC00E4AE99 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 870A519A294700DE0095C7EB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 870A51A1294700DE0095C7EB; + remoteInfo = MarkEditMac; + }; + 8729240E2951525C007156B1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 870A519A294700DE0095C7EB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 872923FF2951525C007156B1; + remoteInfo = PreviewExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 872924112951525C007156B1 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 872924102951525C007156B1 /* PreviewExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 87067E2629755D5C0052E795 /* EditorStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorStatusView.swift; sourceTree = ""; }; + 870A51A2294700DE0095C7EB /* MarkEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MarkEdit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 870A51A9294700E00095C7EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 870A51AC294700E00095C7EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 870A51AE294700E00095C7EB /* Info.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Info.entitlements; sourceTree = ""; }; + 870A51B4294702AF0095C7EB /* EditorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorViewController.swift; sourceTree = ""; }; + 870A51B5294702AF0095C7EB /* EditorWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorWindowController.swift; sourceTree = ""; }; + 870A51B7294702AF0095C7EB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 870A51B8294702AF0095C7EB /* EditorDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorDocument.swift; sourceTree = ""; }; + 870A51BE294702FD0095C7EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 870A51C8294752530095C7EB /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = index.html; path = CoreEditor/dist/index.html; sourceTree = SOURCE_ROOT; }; + 870F17652984D2C000003DA7 /* EditorViewController+LineEndings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+LineEndings.swift"; sourceTree = ""; }; + 870F17672985285B00003DA7 /* EditorViewController+Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Config.swift"; sourceTree = ""; }; + 870F17752985546500003DA7 /* MarkEditCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MarkEditCore; sourceTree = ""; }; + 870F178C2985774D00003DA7 /* Modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Modules; sourceTree = ""; }; + 870F179A2985783500003DA7 /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = index.html; path = "CoreEditor/src/@light/dist/index.html"; sourceTree = SOURCE_ROOT; }; + 870FF837298BD7DC00E4AE99 /* MarkEditMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MarkEditMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 870FF839298BD7DC00E4AE99 /* MarkEditMacTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkEditMacTests.swift; sourceTree = ""; }; + 871C995A2973F1E6003FF3A7 /* AppDelegate+Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Document.swift"; sourceTree = ""; }; + 871C995C2973F20E003FF3A7 /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; }; + 87231E77298E77D800251D09 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; + 87231E79298E77D800251D09 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + 8725E07F2977EEB3006A4961 /* EditorToolbarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorToolbarItems.swift; sourceTree = ""; }; + 8725E0812977EF46006A4961 /* EditorViewController+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Toolbar.swift"; sourceTree = ""; }; + 872924002951525C007156B1 /* PreviewExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PreviewExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 872924052951525C007156B1 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; + 8729240A2951525C007156B1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Main.xib; sourceTree = ""; }; + 8729240C2951525C007156B1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8729240D2951525C007156B1 /* Info.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Info.entitlements; sourceTree = ""; }; + 8729C5A5294B5E6B006F262B /* EditorViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Menu.swift"; sourceTree = ""; }; + 873126E3297BAC9C001521A0 /* EditorViewController+Pandoc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Pandoc.swift"; sourceTree = ""; }; + 873ACD4629644E2F00431498 /* EditorViewController+Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Encoding.swift"; sourceTree = ""; }; + 873D6401295FCC3E0095DEF7 /* AppResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppResources.swift; sourceTree = ""; }; + 873D6439295FEF220095DEF7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 873D643B295FEF370095DEF7 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 873D643D295FEF370095DEF7 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 8743B26229593B2000F83DAE /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; + 874AAE2D2959F197000E0E84 /* EditorReplacePanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorReplacePanel.swift; sourceTree = ""; }; + 8752CEB5295B2F8800BA9D5B /* EditorPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorPanelView.swift; sourceTree = ""; }; + 8767BBAE295A8BA500BFACAE /* EditorViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+UI.swift"; sourceTree = ""; }; + 8767BBB0295A8C1D00BFACAE /* EditorViewController+Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Delegate.swift"; sourceTree = ""; }; + 8767BBB4295AD62700BFACAE /* EditorReplaceButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorReplaceButtons.swift; sourceTree = ""; }; + 8772E384294AC60D00111E83 /* EditorReusePool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorReusePool.swift; sourceTree = ""; }; + 8780AC172982554800065EF4 /* SettingTabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingTabs.swift; sourceTree = ""; }; + 8780AC1B29829EDD00065EF4 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; + 8780AC1D2982A2C900065EF4 /* EditorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorSettingsView.swift; sourceTree = ""; }; + 8780AC1F2982A2DD00065EF4 /* WindowSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSettingsView.swift; sourceTree = ""; }; + 87850C8529482A8600D1A952 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; }; + 8790B6EB297036B2000DFC1A /* EditorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorWindow.swift; sourceTree = ""; }; + 87A73AB2294C4B27006A710E /* EditorWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorWebView.swift; sourceTree = ""; }; + 87A73AB4294C4B48006A710E /* EditorFindPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorFindPanel.swift; sourceTree = ""; }; + 87AA1223296560AE00BE4719 /* EditorViewController+HyperLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+HyperLink.swift"; sourceTree = ""; }; + 87AAEDF42958258C00C8E61C /* EditorFindPanel+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorFindPanel+UI.swift"; sourceTree = ""; }; + 87AAEDF6295825D800C8E61C /* EditorFindPanel+Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorFindPanel+Delegate.swift"; sourceTree = ""; }; + 87AAEDF829582C2700C8E61C /* EditorFindPanel+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorFindPanel+Menu.swift"; sourceTree = ""; }; + 87AAEE0129585ADC00C8E61C /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = ""; }; + 87BD071129699A290053EF5F /* EditorViewController+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+Preview.swift"; sourceTree = ""; }; + 87BDF6E32976C97100548079 /* EditorViewController+GotoLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+GotoLine.swift"; sourceTree = ""; }; + 87BFF238298AAC75006C31E4 /* MarkEditTools */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MarkEditTools; sourceTree = ""; }; + 87C3CBFA29545944002A3436 /* MarkEditKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MarkEditKit; sourceTree = ""; }; + 87DE081A294DDA49004AD33A /* AppTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTheme.swift; sourceTree = ""; }; + 87DE0824294DF340004AD33A /* EditorFindButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorFindButtons.swift; sourceTree = ""; }; + 87DE084A294E22C5004AD33A /* EditorViewController+TextFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorViewController+TextFinder.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 870A519F294700DE0095C7EB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 870F177D29855CA500003DA7 /* MarkEditKit in Frameworks */, + 870F17902985775B00003DA7 /* AppKitExtensions in Frameworks */, + 870F17942985775B00003DA7 /* Proofing in Frameworks */, + 8775A77B29867012005867BF /* FontPicker in Frameworks */, + 870F17922985775B00003DA7 /* Previewer in Frameworks */, + 870F178E2985775B00003DA7 /* AppKitControls in Frameworks */, + 870F17962985775B00003DA7 /* SettingsUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 870FF834298BD7DC00E4AE99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 872923FD2951525C007156B1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 87EB9901295755DA00A56B97 /* MarkEditCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 870A5199294700DE0095C7EB = { + isa = PBXGroup; + children = ( + 870F17752985546500003DA7 /* MarkEditCore */, + 87C3CBFA29545944002A3436 /* MarkEditKit */, + 87BFF238298AAC75006C31E4 /* MarkEditTools */, + 870A51A4294700DE0095C7EB /* MarkEditMac */, + 870FF838298BD7DC00E4AE99 /* MarkEditMacTests */, + 872924042951525C007156B1 /* PreviewExtension */, + 8743B26229593B2000F83DAE /* Build.xcconfig */, + 870A51A3294700DE0095C7EB /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + wrapsLines = 1; + }; + 870A51A3294700DE0095C7EB /* Products */ = { + isa = PBXGroup; + children = ( + 870A51A2294700DE0095C7EB /* MarkEdit.app */, + 872924002951525C007156B1 /* PreviewExtension.appex */, + 870FF837298BD7DC00E4AE99 /* MarkEditMacTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 870A51A4294700DE0095C7EB /* MarkEditMac */ = { + isa = PBXGroup; + children = ( + 870F178C2985774D00003DA7 /* Modules */, + 870A51C52947103A0095C7EB /* Sources */, + 870A51C229470D5C0095C7EB /* Resources */, + 870A51C129470CAB0095C7EB /* Supporting Files */, + ); + path = MarkEditMac; + sourceTree = ""; + }; + 870A51C129470CAB0095C7EB /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 870A51BE294702FD0095C7EB /* Info.plist */, + 870A51AE294700E00095C7EB /* Info.entitlements */, + 870A51AB294700E00095C7EB /* Main.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 870A51C229470D5C0095C7EB /* Resources */ = { + isa = PBXGroup; + children = ( + 870A51C8294752530095C7EB /* index.html */, + 870A51A9294700E00095C7EB /* Assets.xcassets */, + 873D643A295FEF220095DEF7 /* Localizable.strings */, + ); + path = Resources; + sourceTree = ""; + }; + 870A51C429470D830095C7EB /* Editor */ = { + isa = PBXGroup; + children = ( + 87A73AAB294C3788006A710E /* Models */, + 87A73AB1294C4B0A006A710E /* Views */, + 8767BBAC295A893200BFACAE /* Controllers */, + 8790B6EB297036B2000DFC1A /* EditorWindow.swift */, + 870A51B5294702AF0095C7EB /* EditorWindowController.swift */, + ); + path = Editor; + sourceTree = ""; + }; + 870A51C52947103A0095C7EB /* Sources */ = { + isa = PBXGroup; + children = ( + 873D6403295FD2FF0095DEF7 /* Main */, + 870A51C429470D830095C7EB /* Editor */, + 870F1797298577A000003DA7 /* Panels */, + 8780AC0A29822C5500065EF4 /* Settings */, + 87850C8429482A6A00D1A952 /* Extensions */, + ); + path = Sources; + sourceTree = ""; + }; + 870F1797298577A000003DA7 /* Panels */ = { + isa = PBXGroup; + children = ( + 8767BBAD295A895200BFACAE /* Find */, + 87AAEDF12958244D00C8E61C /* Replace */, + ); + path = Panels; + sourceTree = ""; + }; + 870F1799298577F500003DA7 /* Resources */ = { + isa = PBXGroup; + children = ( + 870F179A2985783500003DA7 /* index.html */, + ); + path = Resources; + sourceTree = ""; + }; + 870FF838298BD7DC00E4AE99 /* MarkEditMacTests */ = { + isa = PBXGroup; + children = ( + 870FF839298BD7DC00E4AE99 /* MarkEditMacTests.swift */, + ); + path = MarkEditMacTests; + sourceTree = ""; + }; + 871C99592973F1C2003FF3A7 /* Application */ = { + isa = PBXGroup; + children = ( + 870A51B7294702AF0095C7EB /* AppDelegate.swift */, + 871C995C2973F20E003FF3A7 /* AppDelegate+Menu.swift */, + 871C995A2973F1E6003FF3A7 /* AppDelegate+Document.swift */, + ); + path = Application; + sourceTree = ""; + }; + 872924042951525C007156B1 /* PreviewExtension */ = { + isa = PBXGroup; + children = ( + 872924052951525C007156B1 /* PreviewViewController.swift */, + 870F1799298577F500003DA7 /* Resources */, + 872924262951A134007156B1 /* Supporting Files */, + ); + path = PreviewExtension; + sourceTree = ""; + }; + 872924262951A134007156B1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 8729240C2951525C007156B1 /* Info.plist */, + 8729240D2951525C007156B1 /* Info.entitlements */, + 872924092951525C007156B1 /* Main.xib */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 873D6403295FD2FF0095DEF7 /* Main */ = { + isa = PBXGroup; + children = ( + 871C99592973F1C2003FF3A7 /* Application */, + 873D6401295FCC3E0095DEF7 /* AppResources.swift */, + 87DE081A294DDA49004AD33A /* AppTheme.swift */, + 87AAEE0129585ADC00C8E61C /* AppPreferences.swift */, + ); + path = Main; + sourceTree = ""; + }; + 8767BBAC295A893200BFACAE /* Controllers */ = { + isa = PBXGroup; + children = ( + 870A51B4294702AF0095C7EB /* EditorViewController.swift */, + 8767BBAE295A8BA500BFACAE /* EditorViewController+UI.swift */, + 870F17672985285B00003DA7 /* EditorViewController+Config.swift */, + 8767BBB0295A8C1D00BFACAE /* EditorViewController+Delegate.swift */, + 8729C5A5294B5E6B006F262B /* EditorViewController+Menu.swift */, + 8725E0812977EF46006A4961 /* EditorViewController+Toolbar.swift */, + 87DE084A294E22C5004AD33A /* EditorViewController+TextFinder.swift */, + 873ACD4629644E2F00431498 /* EditorViewController+Encoding.swift */, + 870F17652984D2C000003DA7 /* EditorViewController+LineEndings.swift */, + 87AA1223296560AE00BE4719 /* EditorViewController+HyperLink.swift */, + 87BD071129699A290053EF5F /* EditorViewController+Preview.swift */, + 87BDF6E32976C97100548079 /* EditorViewController+GotoLine.swift */, + 873126E3297BAC9C001521A0 /* EditorViewController+Pandoc.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 8767BBAD295A895200BFACAE /* Find */ = { + isa = PBXGroup; + children = ( + 87A73AB4294C4B48006A710E /* EditorFindPanel.swift */, + 87AAEDF42958258C00C8E61C /* EditorFindPanel+UI.swift */, + 87AAEDF6295825D800C8E61C /* EditorFindPanel+Delegate.swift */, + 87AAEDF829582C2700C8E61C /* EditorFindPanel+Menu.swift */, + 87DE0824294DF340004AD33A /* EditorFindButtons.swift */, + ); + path = Find; + sourceTree = ""; + }; + 8780AC0A29822C5500065EF4 /* Settings */ = { + isa = PBXGroup; + children = ( + 8780AC172982554800065EF4 /* SettingTabs.swift */, + 8780AC1D2982A2C900065EF4 /* EditorSettingsView.swift */, + 8780AC1B29829EDD00065EF4 /* GeneralSettingsView.swift */, + 8780AC1F2982A2DD00065EF4 /* WindowSettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 87850C8429482A6A00D1A952 /* Extensions */ = { + isa = PBXGroup; + children = ( + 87850C8529482A8600D1A952 /* NSApplication+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 87A73AAB294C3788006A710E /* Models */ = { + isa = PBXGroup; + children = ( + 870A51B8294702AF0095C7EB /* EditorDocument.swift */, + 8772E384294AC60D00111E83 /* EditorReusePool.swift */, + 8725E07F2977EEB3006A4961 /* EditorToolbarItems.swift */, + ); + path = Models; + sourceTree = ""; + }; + 87A73AB1294C4B0A006A710E /* Views */ = { + isa = PBXGroup; + children = ( + 8752CEB5295B2F8800BA9D5B /* EditorPanelView.swift */, + 87A73AB2294C4B27006A710E /* EditorWebView.swift */, + 87067E2629755D5C0052E795 /* EditorStatusView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 87AAEDF12958244D00C8E61C /* Replace */ = { + isa = PBXGroup; + children = ( + 874AAE2D2959F197000E0E84 /* EditorReplacePanel.swift */, + 8767BBB4295AD62700BFACAE /* EditorReplaceButtons.swift */, + ); + path = Replace; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 870A51A1294700DE0095C7EB /* MarkEditMac */ = { + isa = PBXNativeTarget; + buildConfigurationList = 870A51B1294700E00095C7EB /* Build configuration list for PBXNativeTarget "MarkEditMac" */; + buildPhases = ( + 870A519E294700DE0095C7EB /* Sources */, + 870A519F294700DE0095C7EB /* Frameworks */, + 870A51A0294700DE0095C7EB /* Resources */, + 872924112951525C007156B1 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 87BFF24E298AB754006C31E4 /* PBXTargetDependency */, + 8729240F2951525C007156B1 /* PBXTargetDependency */, + ); + name = MarkEditMac; + packageProductDependencies = ( + 870F177C29855CA500003DA7 /* MarkEditKit */, + 870F178D2985775B00003DA7 /* AppKitControls */, + 870F178F2985775B00003DA7 /* AppKitExtensions */, + 870F17912985775B00003DA7 /* Previewer */, + 870F17932985775B00003DA7 /* Proofing */, + 870F17952985775B00003DA7 /* SettingsUI */, + 8775A77A29867012005867BF /* FontPicker */, + ); + productName = MarkEdit; + productReference = 870A51A2294700DE0095C7EB /* MarkEdit.app */; + productType = "com.apple.product-type.application"; + }; + 870FF836298BD7DC00E4AE99 /* MarkEditMacTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 870FF83F298BD7DC00E4AE99 /* Build configuration list for PBXNativeTarget "MarkEditMacTests" */; + buildPhases = ( + 870FF833298BD7DC00E4AE99 /* Sources */, + 870FF834298BD7DC00E4AE99 /* Frameworks */, + 870FF835298BD7DC00E4AE99 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 870FF83C298BD7DC00E4AE99 /* PBXTargetDependency */, + ); + name = MarkEditMacTests; + productName = MarkEditMacTests; + productReference = 870FF837298BD7DC00E4AE99 /* MarkEditMacTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 872923FF2951525C007156B1 /* PreviewExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 872924142951525C007156B1 /* Build configuration list for PBXNativeTarget "PreviewExtension" */; + buildPhases = ( + 872923FC2951525C007156B1 /* Sources */, + 872923FD2951525C007156B1 /* Frameworks */, + 872923FE2951525C007156B1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PreviewExtension; + packageProductDependencies = ( + 87EB9900295755DA00A56B97 /* MarkEditCore */, + ); + productName = PreviewExtension; + productReference = 872924002951525C007156B1 /* PreviewExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 870A519A294700DE0095C7EB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 870A51A1294700DE0095C7EB = { + CreatedOnToolsVersion = 14.1; + LastSwiftMigration = 1410; + }; + 870FF836298BD7DC00E4AE99 = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 870A51A1294700DE0095C7EB; + }; + 872923FF2951525C007156B1 = { + CreatedOnToolsVersion = 14.1; + }; + }; + }; + buildConfigurationList = 870A519D294700DE0095C7EB /* Build configuration list for PBXProject "MarkEdit" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + "zh-Hant", + ); + mainGroup = 870A5199294700DE0095C7EB; + productRefGroup = 870A51A3294700DE0095C7EB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 870A51A1294700DE0095C7EB /* MarkEditMac */, + 870FF836298BD7DC00E4AE99 /* MarkEditMacTests */, + 872923FF2951525C007156B1 /* PreviewExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 870A51A0294700DE0095C7EB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 870A51C9294752530095C7EB /* index.html in Resources */, + 873D6438295FEF220095DEF7 /* Localizable.strings in Resources */, + 870A51AA294700E00095C7EB /* Assets.xcassets in Resources */, + 870A51AD294700E00095C7EB /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 870FF835298BD7DC00E4AE99 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 872923FE2951525C007156B1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8729240B2951525C007156B1 /* Main.xib in Resources */, + 870F179B2985783500003DA7 /* index.html in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 870A519E294700DE0095C7EB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8725E0822977EF46006A4961 /* EditorViewController+Toolbar.swift in Sources */, + 8780AC202982A2DD00065EF4 /* WindowSettingsView.swift in Sources */, + 87BD071229699A290053EF5F /* EditorViewController+Preview.swift in Sources */, + 873D6402295FCC3E0095DEF7 /* AppResources.swift in Sources */, + 870A51BC294702AF0095C7EB /* AppDelegate.swift in Sources */, + 8780AC1E2982A2C900065EF4 /* EditorSettingsView.swift in Sources */, + 8767BBB5295AD62700BFACAE /* EditorReplaceButtons.swift in Sources */, + 87BDF6E42976C97100548079 /* EditorViewController+GotoLine.swift in Sources */, + 8772E385294AC60D00111E83 /* EditorReusePool.swift in Sources */, + 870F17662984D2C000003DA7 /* EditorViewController+LineEndings.swift in Sources */, + 8790B6EC297036B2000DFC1A /* EditorWindow.swift in Sources */, + 87AA1224296560AE00BE4719 /* EditorViewController+HyperLink.swift in Sources */, + 8725E0802977EEB3006A4961 /* EditorToolbarItems.swift in Sources */, + 870A51B9294702AF0095C7EB /* EditorViewController.swift in Sources */, + 8729C5A6294B5E6B006F262B /* EditorViewController+Menu.swift in Sources */, + 87AAEDF7295825D800C8E61C /* EditorFindPanel+Delegate.swift in Sources */, + 871C995B2973F1E6003FF3A7 /* AppDelegate+Document.swift in Sources */, + 87AAEE0229585ADC00C8E61C /* AppPreferences.swift in Sources */, + 873126E4297BAC9C001521A0 /* EditorViewController+Pandoc.swift in Sources */, + 87850C8629482A8600D1A952 /* NSApplication+Extension.swift in Sources */, + 8767BBB1295A8C1D00BFACAE /* EditorViewController+Delegate.swift in Sources */, + 87DE081B294DDA49004AD33A /* AppTheme.swift in Sources */, + 870F17682985285B00003DA7 /* EditorViewController+Config.swift in Sources */, + 8780AC1C29829EDD00065EF4 /* GeneralSettingsView.swift in Sources */, + 871C995D2973F20E003FF3A7 /* AppDelegate+Menu.swift in Sources */, + 87DE084B294E22C5004AD33A /* EditorViewController+TextFinder.swift in Sources */, + 8752CEB6295B2F8800BA9D5B /* EditorPanelView.swift in Sources */, + 870A51BA294702AF0095C7EB /* EditorWindowController.swift in Sources */, + 874AAE2E2959F197000E0E84 /* EditorReplacePanel.swift in Sources */, + 87A73AB3294C4B27006A710E /* EditorWebView.swift in Sources */, + 8767BBAF295A8BA500BFACAE /* EditorViewController+UI.swift in Sources */, + 87AAEDF52958258C00C8E61C /* EditorFindPanel+UI.swift in Sources */, + 87A73AB5294C4B48006A710E /* EditorFindPanel.swift in Sources */, + 870A51BD294702AF0095C7EB /* EditorDocument.swift in Sources */, + 8780AC182982554800065EF4 /* SettingTabs.swift in Sources */, + 87AAEDF929582C2700C8E61C /* EditorFindPanel+Menu.swift in Sources */, + 873ACD4729644E2F00431498 /* EditorViewController+Encoding.swift in Sources */, + 87DE0825294DF340004AD33A /* EditorFindButtons.swift in Sources */, + 87067E2729755D5C0052E795 /* EditorStatusView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 870FF833298BD7DC00E4AE99 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 870FF83A298BD7DC00E4AE99 /* MarkEditMacTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 872923FC2951525C007156B1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 872924062951525C007156B1 /* PreviewViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 870FF83C298BD7DC00E4AE99 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 870A51A1294700DE0095C7EB /* MarkEditMac */; + targetProxy = 870FF83B298BD7DC00E4AE99 /* PBXContainerItemProxy */; + }; + 8729240F2951525C007156B1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 872923FF2951525C007156B1 /* PreviewExtension */; + targetProxy = 8729240E2951525C007156B1 /* PBXContainerItemProxy */; + }; + 87BFF24E298AB754006C31E4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 87BFF24D298AB754006C31E4 /* SwiftLint */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 870A51AB294700E00095C7EB /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 870A51AC294700E00095C7EB /* Base */, + 873D643B295FEF370095DEF7 /* zh-Hans */, + 87231E77298E77D800251D09 /* zh-Hant */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 872924092951525C007156B1 /* Main.xib */ = { + isa = PBXVariantGroup; + children = ( + 8729240A2951525C007156B1 /* Base */, + ); + name = Main.xib; + sourceTree = ""; + }; + 873D643A295FEF220095DEF7 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 873D6439295FEF220095DEF7 /* en */, + 873D643D295FEF370095DEF7 /* zh-Hans */, + 87231E79298E77D800251D09 /* zh-Hant */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 870A51AF294700E00095C7EB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = 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; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + }; + name = Debug; + }; + 870A51B0294700E00095C7EB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = 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; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + }; + name = Release; + }; + 870A51B2294700E00095C7EB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = MarkEditMac/Info.entitlements; + CODE_SIGN_IDENTITY = "$(inherited)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = "$(inherited)"; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MarkEditMac/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MarkEdit; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; + PRODUCT_NAME = MarkEdit; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 870A51B3294700E00095C7EB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = MarkEditMac/Info.entitlements; + CODE_SIGN_IDENTITY = "$(inherited)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = "$(inherited)"; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MarkEditMac/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MarkEdit; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; + PRODUCT_NAME = MarkEdit; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 870FF83D298BD7DC00E4AE99 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEVELOPMENT_TEAM = "$(inherited)"; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "app.cyan.markedit-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MarkEdit.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MarkEdit"; + }; + name = Debug; + }; + 870FF83E298BD7DC00E4AE99 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEVELOPMENT_TEAM = "$(inherited)"; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "app.cyan.markedit-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MarkEdit.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MarkEdit"; + }; + name = Release; + }; + 872924122951525C007156B1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = PreviewExtension/Info.entitlements; + CODE_SIGN_IDENTITY = "$(inherited)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = "$(inherited)"; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PreviewExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PreviewExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).preview-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 872924132951525C007156B1 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8743B26229593B2000F83DAE /* Build.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = PreviewExtension/Info.entitlements; + CODE_SIGN_IDENTITY = "$(inherited)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(inherited)"; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = "$(inherited)"; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PreviewExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PreviewExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).preview-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 870A519D294700DE0095C7EB /* Build configuration list for PBXProject "MarkEdit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 870A51AF294700E00095C7EB /* Debug */, + 870A51B0294700E00095C7EB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 870A51B1294700E00095C7EB /* Build configuration list for PBXNativeTarget "MarkEditMac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 870A51B2294700E00095C7EB /* Debug */, + 870A51B3294700E00095C7EB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 870FF83F298BD7DC00E4AE99 /* Build configuration list for PBXNativeTarget "MarkEditMacTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 870FF83D298BD7DC00E4AE99 /* Debug */, + 870FF83E298BD7DC00E4AE99 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 872924142951525C007156B1 /* Build configuration list for PBXNativeTarget "PreviewExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 872924122951525C007156B1 /* Debug */, + 872924132951525C007156B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 870F177C29855CA500003DA7 /* MarkEditKit */ = { + isa = XCSwiftPackageProductDependency; + productName = MarkEditKit; + }; + 870F178D2985775B00003DA7 /* AppKitControls */ = { + isa = XCSwiftPackageProductDependency; + productName = AppKitControls; + }; + 870F178F2985775B00003DA7 /* AppKitExtensions */ = { + isa = XCSwiftPackageProductDependency; + productName = AppKitExtensions; + }; + 870F17912985775B00003DA7 /* Previewer */ = { + isa = XCSwiftPackageProductDependency; + productName = Previewer; + }; + 870F17932985775B00003DA7 /* Proofing */ = { + isa = XCSwiftPackageProductDependency; + productName = Proofing; + }; + 870F17952985775B00003DA7 /* SettingsUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SettingsUI; + }; + 8775A77A29867012005867BF /* FontPicker */ = { + isa = XCSwiftPackageProductDependency; + productName = FontPicker; + }; + 87BFF24D298AB754006C31E4 /* SwiftLint */ = { + isa = XCSwiftPackageProductDependency; + productName = "plugin:SwiftLint"; + }; + 87EB9900295755DA00A56B97 /* MarkEditCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MarkEditCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 870A519A294700DE0095C7EB /* Project object */; +} diff --git a/MarkEdit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MarkEdit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/MarkEdit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MarkEdit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MarkEdit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/MarkEdit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hans).xcscheme b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hans).xcscheme new file mode 100644 index 00000000..cb206b81 --- /dev/null +++ b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hans).xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hant).xcscheme b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hant).xcscheme new file mode 100644 index 00000000..c8c0ab70 --- /dev/null +++ b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac (zh-Hant).xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac.xcscheme b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac.xcscheme new file mode 100644 index 00000000..edfa08b0 --- /dev/null +++ b/MarkEdit.xcodeproj/xcshareddata/xcschemes/MarkEditMac.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEdit.xcodeproj/xcshareddata/xcschemes/PreviewExtension.xcscheme b/MarkEdit.xcodeproj/xcshareddata/xcschemes/PreviewExtension.xcscheme new file mode 100644 index 00000000..22945098 --- /dev/null +++ b/MarkEdit.xcodeproj/xcshareddata/xcschemes/PreviewExtension.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditCore/.gitignore b/MarkEditCore/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/MarkEditCore/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/MarkEditCore/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditCore.xcscheme b/MarkEditCore/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditCore.xcscheme new file mode 100644 index 00000000..9bac1e1b --- /dev/null +++ b/MarkEditCore/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditCore.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditCore/Package.swift b/MarkEditCore/Package.swift new file mode 100644 index 00000000..23e9f3ba --- /dev/null +++ b/MarkEditCore/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MarkEditCore", + platforms: [ + .iOS(.v15), + .macOS(.v12), + ], + products: [ + .library( + name: "MarkEditCore", + targets: ["MarkEditCore"] + ), + ], + dependencies: [ + .package(path: "../MarkEditTools"), + ], + targets: [ + .target( + name: "MarkEditCore", + path: "Sources", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + + .testTarget( + name: "MarkEditCoreTests", + dependencies: ["MarkEditCore"], + path: "Tests", + resources: [ + .process("Files"), + ], + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + ] +) diff --git a/MarkEditCore/README.md b/MarkEditCore/README.md new file mode 100644 index 00000000..026a2d68 --- /dev/null +++ b/MarkEditCore/README.md @@ -0,0 +1,7 @@ +# MarkEditCore + +This package provides core capabilities to bootstrap the CoreEditor, including configurations, encoding, and decoding. + +It can be used in a full-fledged editor like `MarkEditMac`, or a light version like `PreviewExtension`. + +> Note that this package should be platform-independent. \ No newline at end of file diff --git a/MarkEditCore/Sources/EditorConfig.swift b/MarkEditCore/Sources/EditorConfig.swift new file mode 100644 index 00000000..1bef7a50 --- /dev/null +++ b/MarkEditCore/Sources/EditorConfig.swift @@ -0,0 +1,62 @@ +// +// EditorConfig.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public struct EditorConfig: Encodable { + let text: String + let theme: String + let fontFamily: String + let fontSize: Double + let showLineNumbers: Bool + let showActiveLineIndicator: Bool + let showInvisibles: Bool + let typewriterMode: Bool + let focusMode: Bool + let lineWrapping: Bool + let lineHeight: Double + let defaultLineBreak: String? + let tabKeyBehavior: Int? + let indentUnit: String? + let localizable: EditorLocalizable? + + public init( + text: String, + theme: String, + fontFamily: String, + fontSize: Double, + showLineNumbers: Bool, + showActiveLineIndicator: Bool, + showInvisibles: Bool, + typewriterMode: Bool, + focusMode: Bool, + lineWrapping: Bool, + lineHeight: Double, + defaultLineBreak: String?, + tabKeyBehavior: Int?, + indentUnit: String?, + localizable: EditorLocalizable? + ) { + self.text = text + self.theme = theme + self.fontFamily = fontFamily + self.fontSize = fontSize + self.showLineNumbers = showLineNumbers + self.showActiveLineIndicator = showActiveLineIndicator + self.showInvisibles = showInvisibles + self.typewriterMode = typewriterMode + self.focusMode = focusMode + self.lineWrapping = lineWrapping + self.lineHeight = lineHeight + self.defaultLineBreak = defaultLineBreak + self.tabKeyBehavior = tabKeyBehavior + self.indentUnit = indentUnit + self.localizable = localizable + } +} diff --git a/MarkEditCore/Sources/EditorLocalizable.swift b/MarkEditCore/Sources/EditorLocalizable.swift new file mode 100644 index 00000000..e725718b --- /dev/null +++ b/MarkEditCore/Sources/EditorLocalizable.swift @@ -0,0 +1,41 @@ +// +// EditorLocalizable.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public struct EditorLocalizable: Encodable { + let controlCharacter: String + let foldedLines: String + let unfoldedLines: String + let foldedCode: String + let unfold: String + let foldLine: String + let unfoldLine: String + let previewButtonTitle: String + + public init( + controlCharacter: String, + foldedLines: String, + unfoldedLines: String, + foldedCode: String, + unfold: String, + foldLine: String, + unfoldLine: String, + previewButtonTitle: String + ) { + self.controlCharacter = controlCharacter + self.foldedLines = foldedLines + self.unfoldedLines = unfoldedLines + self.foldedCode = foldedCode + self.unfold = unfold + self.foldLine = foldLine + self.unfoldLine = unfoldLine + self.previewButtonTitle = previewButtonTitle + } +} diff --git a/MarkEditCore/Sources/Extensions/Data+Extension.swift b/MarkEditCore/Sources/Extensions/Data+Extension.swift new file mode 100644 index 00000000..5e446e5d --- /dev/null +++ b/MarkEditCore/Sources/Extensions/Data+Extension.swift @@ -0,0 +1,41 @@ +// +// Data+Extension.swift +// +// Created by cyan on 12/28/22. +// + +import Foundation + +public extension Data { + /// Handle text encoding in Cocoa apps: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/introStrings.html + /// + /// Ideally, the encoding for Markdown should always be utf-8 as described in: https://daringfireball.net/linked/2011/08/05/markdown-uti + func toString(encoding: String.Encoding = .utf8) -> String? { + // Perfect, successfully decoded it with the preferred encoding + if let decoded = String(data: self, encoding: encoding) { + return decoded + } + + // Oh no, guess the encoding since we failed to decode it directly + var converted: NSString? + NSString.stringEncoding( + for: self, + encodingOptions: [ + // Just a blind guess, it's not possible to know without extra information + .suggestedEncodingsKey: [ + String.Encoding(from: .GB_18030_2000).rawValue, + String.Encoding(from: .big5).rawValue, + String.Encoding.japaneseEUC.rawValue, + String.Encoding.shiftJIS.rawValue, + String.Encoding(from: .EUC_KR).rawValue, + encoding.rawValue, + ], + ], + convertedString: &converted, + usedLossyConversion: nil + ) + + // It can still be nil, in that case we should allow users to reopen with an encoding + return converted as? String + } +} diff --git a/MarkEditCore/Sources/Extensions/EditorConfig+Extension.swift b/MarkEditCore/Sources/Extensions/EditorConfig+Extension.swift new file mode 100644 index 00000000..19324019 --- /dev/null +++ b/MarkEditCore/Sources/Extensions/EditorConfig+Extension.swift @@ -0,0 +1,24 @@ +// +// EditorConfig+Extension.swift +// +// Created by cyan on 12/23/22. +// + +import Foundation + +public extension EditorConfig { + var toHtml: String { + indexHtml?.replacingOccurrences(of: "\"{{EDITOR_CONFIG}}\"", with: jsonEncoded) ?? "" + } +} + +extension EditorConfig { + /// index.html built by CoreEditor + private var indexHtml: String? { + guard let path = Bundle.main.url(forResource: "index", withExtension: "html") else { + fatalError("Missing index.html to set up the editor") + } + + return try? Data(contentsOf: path).toString() + } +} diff --git a/MarkEditCore/Sources/Extensions/Encodable+Extension.swift b/MarkEditCore/Sources/Extensions/Encodable+Extension.swift new file mode 100644 index 00000000..e30f9fbf --- /dev/null +++ b/MarkEditCore/Sources/Extensions/Encodable+Extension.swift @@ -0,0 +1,13 @@ +// +// Encodable+Extension.swift +// +// Created by cyan on 12/22/22. +// + +import Foundation + +public extension Encodable { + var jsonEncoded: String { + (try? JSONEncoder().encode(self).toString()) ?? "{}" + } +} diff --git a/MarkEditCore/Sources/Extensions/String+Extension.swift b/MarkEditCore/Sources/Extensions/String+Extension.swift new file mode 100644 index 00000000..71162c34 --- /dev/null +++ b/MarkEditCore/Sources/Extensions/String+Extension.swift @@ -0,0 +1,30 @@ +// +// String+Extension.swift +// +// Created by cyan on 12/28/22. +// + +import Foundation + +public extension String { + /// Overload of the String.Encoding version + init?(data: Data, encoding: CFStringEncodings) { + self.init(data: data, encoding: String.Encoding(from: encoding)) + } + + /// Overload of the String.Encoding version + func data(using encoding: CFStringEncodings, allowLossyConversion: Bool = false) -> Data? { + data(using: String.Encoding(from: encoding), allowLossyConversion: allowLossyConversion) + } + + func toData(encoding: String.Encoding = .utf8) -> Data? { + data(using: encoding) + } +} + +extension String.Encoding { + init(from: CFStringEncodings) { + let encoding = CFStringEncoding(from.rawValue) + self.init(rawValue: CFStringConvertEncodingToNSStringEncoding(encoding)) + } +} diff --git a/MarkEditCore/Tests/EncodingTests.swift b/MarkEditCore/Tests/EncodingTests.swift new file mode 100644 index 00000000..1e1a0ab1 --- /dev/null +++ b/MarkEditCore/Tests/EncodingTests.swift @@ -0,0 +1,41 @@ +// +// EncodingTests.swift +// +// Created by cyan on 2/2/23. +// + +import MarkEditCore +import XCTest + +final class EncodingTests: XCTestCase { + func testDecodeUTF8() { + let data = fileData(of: "sample-utf8.md") + XCTAssertEqual(data.toString(), "Hello, World!\n") + } + + func testDecodeGB18030() { + let data = fileData(of: "sample-gb18030.md") + XCTAssertEqual(data.toString(), "你好,世界!\n") + } + + func testDecodeJapaneseEUC() { + let data = fileData(of: "sample-japanese-euc.md") + XCTAssertEqual(data.toString(), "ゼルダの伝説\n") + } + + func testDecodeKoreanEUC() { + let data = fileData(of: "sample-korean-euc.md") + XCTAssertEqual(data.toString(), "오징어 게임\n") + } +} + +// MARK: - Private + +private extension EncodingTests { + func fileData(of name: String) -> Data { + // swiftlint:disable:next force_unwrapping + let url = Bundle.module.url(forResource: name, withExtension: nil)! + // swiftlint:disable:next force_try + return try! Data(contentsOf: url) + } +} diff --git a/MarkEditCore/Tests/Files/sample-gb18030.md b/MarkEditCore/Tests/Files/sample-gb18030.md new file mode 100644 index 00000000..b6e259f1 --- /dev/null +++ b/MarkEditCore/Tests/Files/sample-gb18030.md @@ -0,0 +1 @@ +ã磡 diff --git a/MarkEditCore/Tests/Files/sample-japanese-euc.md b/MarkEditCore/Tests/Files/sample-japanese-euc.md new file mode 100644 index 00000000..8c016cec --- /dev/null +++ b/MarkEditCore/Tests/Files/sample-japanese-euc.md @@ -0,0 +1 @@ + diff --git a/MarkEditCore/Tests/Files/sample-korean-euc.md b/MarkEditCore/Tests/Files/sample-korean-euc.md new file mode 100644 index 00000000..f734e0a6 --- /dev/null +++ b/MarkEditCore/Tests/Files/sample-korean-euc.md @@ -0,0 +1 @@ +¡ diff --git a/MarkEditCore/Tests/Files/sample-utf8.md b/MarkEditCore/Tests/Files/sample-utf8.md new file mode 100644 index 00000000..8ab686ea --- /dev/null +++ b/MarkEditCore/Tests/Files/sample-utf8.md @@ -0,0 +1 @@ +Hello, World! diff --git a/MarkEditKit/.gitignore b/MarkEditKit/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/MarkEditKit/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/MarkEditKit/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditKit.xcscheme b/MarkEditKit/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditKit.xcscheme new file mode 100644 index 00000000..cdb1ec7f --- /dev/null +++ b/MarkEditKit/.swiftpm/xcode/xcshareddata/xcschemes/MarkEditKit.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditKit/Package.swift b/MarkEditKit/Package.swift new file mode 100644 index 00000000..b035b34c --- /dev/null +++ b/MarkEditKit/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MarkEditKit", + platforms: [ + .iOS(.v15), + .macOS(.v12), + ], + products: [ + .library( + name: "MarkEditKit", + targets: ["MarkEditKit"] + ), + ], + dependencies: [ + .package(path: "../MarkEditCore"), + .package(path: "../MarkEditTools"), + ], + targets: [ + .target( + name: "MarkEditKit", + dependencies: ["MarkEditCore"], + path: "Sources", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + ] +) diff --git a/MarkEditKit/README.md b/MarkEditKit/README.md new file mode 100644 index 00000000..eb0c12e4 --- /dev/null +++ b/MarkEditKit/README.md @@ -0,0 +1,7 @@ +# MarkEditKit + +This package provides most functionalities to use the CoreEditor, including a WKWebView wrapper, and bi-directional communication between web application and native code. + +Most code in this package is automatically generated by inferring the TypeScript code, which is located in the CoreEditor folder. To understand how it works, take a look at [ts-gyb](https://github.com/microsoft/ts-gyb). + +> Ideally, this package should be able to run on both macOS and iOS. \ No newline at end of file diff --git a/MarkEditKit/Sources/Bridge/Native/Generated/NativeModuleCore.swift b/MarkEditKit/Sources/Bridge/Native/Generated/NativeModuleCore.swift new file mode 100644 index 00000000..cee611c2 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Native/Generated/NativeModuleCore.swift @@ -0,0 +1,81 @@ +// +// NativeModuleCore.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public protocol NativeModuleCore: NativeModule { + func notifyWindowDidLoad() + func notifyTextDidChange() + func notifySelectionDidChange(lineColumn: LineColumnInfo) +} + +public extension NativeModuleCore { + var bridge: NativeBridge { NativeBridgeCore(self) } +} + +final class NativeBridgeCore: NativeBridge { + static let name = "core" + lazy var methods: [String: NativeMethod] = [ + "notifyWindowDidLoad": { [weak self] in + await self?.notifyWindowDidLoad(parameters: $0) + }, + "notifyTextDidChange": { [weak self] in + await self?.notifyTextDidChange(parameters: $0) + }, + "notifySelectionDidChange": { [weak self] in + await self?.notifySelectionDidChange(parameters: $0) + }, + ] + + private let module: NativeModuleCore + private lazy var decoder = JSONDecoder() + + init(_ module: NativeModuleCore) { + self.module = module + } + + @MainActor private func notifyWindowDidLoad(parameters: Data) async -> Result? { + module.notifyWindowDidLoad() + return .success(nil) + } + + @MainActor private func notifyTextDidChange(parameters: Data) async -> Result? { + module.notifyTextDidChange() + return .success(nil) + } + + @MainActor private func notifySelectionDidChange(parameters: Data) async -> Result? { + struct Message: Decodable { + var lineColumn: LineColumnInfo + } + + let message: Message + do { + message = try decoder.decode(Message.self, from: parameters) + } catch { + Logger.assertFail("Failed to decode parameters: \(parameters)") + return .failure(error) + } + + module.notifySelectionDidChange(lineColumn: message.lineColumn) + return .success(nil) + } +} + +public struct LineColumnInfo: Decodable, Equatable { + public var line: Int + public var column: Int + public var length: Int + + public init(line: Int, column: Int, length: Int) { + self.line = line + self.column = column + self.length = length + } +} diff --git a/MarkEditKit/Sources/Bridge/Native/Generated/NativeModulePreview.swift b/MarkEditKit/Sources/Bridge/Native/Generated/NativeModulePreview.swift new file mode 100644 index 00000000..2740dcc2 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Native/Generated/NativeModulePreview.swift @@ -0,0 +1,74 @@ +// +// NativeModulePreview.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import Foundation + +public protocol NativeModulePreview: NativeModule { + func show(code: String, type: PreviewType, rect: JSRect) +} + +public extension NativeModulePreview { + var bridge: NativeBridge { NativeBridgePreview(self) } +} + +final class NativeBridgePreview: NativeBridge { + static let name = "preview" + lazy var methods: [String: NativeMethod] = [ + "show": { [weak self] in + await self?.show(parameters: $0) + }, + ] + + private let module: NativeModulePreview + private lazy var decoder = JSONDecoder() + + init(_ module: NativeModulePreview) { + self.module = module + } + + @MainActor private func show(parameters: Data) async -> Result? { + struct Message: Decodable { + var code: String + var type: PreviewType + var rect: JSRect + } + + let message: Message + do { + message = try decoder.decode(Message.self, from: parameters) + } catch { + Logger.assertFail("Failed to decode parameters: \(parameters)") + return .failure(error) + } + + module.show(code: message.code, type: message.type, rect: message.rect) + return .success(nil) + } +} + +public enum PreviewType: String, Codable { + case mermaid = "mermaid" + case katex = "katex" + case table = "table" +} + +/// "CGRect-fashion" rect. +public struct JSRect: Decodable, Equatable { + public var x: Double + public var y: Double + public var width: Double + public var height: Double + + public init(x: Double, y: Double, width: Double, height: Double) { + self.x = x + self.y = y + self.width = width + self.height = height + } +} diff --git a/MarkEditKit/Sources/Bridge/Native/Modules/EditorModuleCore.swift b/MarkEditKit/Sources/Bridge/Native/Modules/EditorModuleCore.swift new file mode 100644 index 00000000..196c3bbc --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Native/Modules/EditorModuleCore.swift @@ -0,0 +1,33 @@ +// +// EditorModuleCore.swift +// +// Created by cyan on 12/24/22. +// + +import Foundation + +public protocol EditorModuleCoreDelegate: AnyObject { + func editorModuleCoreWindowDidLoad(_ sender: EditorModuleCore) + func editorModuleCoreTextDidChange(_ sender: EditorModuleCore) + func editorModuleCore(_ sender: EditorModuleCore, selectionDidChange lineColumn: LineColumnInfo) +} + +public final class EditorModuleCore: NativeModuleCore { + private weak var delegate: EditorModuleCoreDelegate? + + public init(delegate: EditorModuleCoreDelegate) { + self.delegate = delegate + } + + public func notifyWindowDidLoad() { + delegate?.editorModuleCoreWindowDidLoad(self) + } + + public func notifyTextDidChange() { + delegate?.editorModuleCoreTextDidChange(self) + } + + public func notifySelectionDidChange(lineColumn: LineColumnInfo) { + delegate?.editorModuleCore(self, selectionDidChange: lineColumn) + } +} diff --git a/MarkEditKit/Sources/Bridge/Native/Modules/EditorModulePreview.swift b/MarkEditKit/Sources/Bridge/Native/Modules/EditorModulePreview.swift new file mode 100644 index 00000000..f4c3a4e1 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Native/Modules/EditorModulePreview.swift @@ -0,0 +1,23 @@ +// +// EditorModulePreview.swift +// +// Created by cyan on 1/7/23. +// + +import Foundation + +public protocol EditorModulePreviewDelegate: AnyObject { + func editorModulePreview(_ sender: NativeModulePreview, show code: String, type: PreviewType, rect: CGRect) +} + +public final class EditorModulePreview: NativeModulePreview { + private weak var delegate: EditorModulePreviewDelegate? + + public init(delegate: EditorModulePreviewDelegate) { + self.delegate = delegate + } + + public func show(code: String, type: PreviewType, rect: JSRect) { + delegate?.editorModulePreview(self, show: code, type: type, rect: rect.cgRect) + } +} diff --git a/MarkEditKit/Sources/Bridge/Native/NativeModules.swift b/MarkEditKit/Sources/Bridge/Native/NativeModules.swift new file mode 100644 index 00000000..00d3227e --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Native/NativeModules.swift @@ -0,0 +1,49 @@ +// +// NativeModules.swift +// +// Created by cyan on 12/24/22. +// + +import Foundation + +/// Native method that will be invoked by JavaScript +public typealias NativeMethod = (_ parameters: Data) async -> Result? + +public protocol NativeBridge: AnyObject { + static var name: String { get } + var methods: [String: NativeMethod] { get } +} + +/** + Native module that implements JavaScript functions. + + Don't implement NativeModule directly with controllers, it will easily introduce retain cycles. + */ +public protocol NativeModule: AnyObject { + var bridge: NativeBridge { get } +} + +public struct NativeModules { + private let bridges: [String: NativeBridge] + + public init(modules: [NativeModule]) { + self.bridges = modules.reduce(into: [String: NativeBridge]()) { result, module in + let bridge = module.bridge + result[type(of: bridge).name] = bridge + } + } +} + +// MARK: - Internal + +extension NativeBridge { + subscript(name: String) -> NativeMethod? { + methods[name] + } +} + +extension NativeModules { + subscript(name: String) -> NativeBridge? { + bridges[name] + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeConfig.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeConfig.swift new file mode 100644 index 00000000..1a56b10c --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeConfig.swift @@ -0,0 +1,180 @@ +// +// WebBridgeConfig.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeConfig { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func setTheme(name: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let name: String + } + + let message = Message( + name: name + ) + + webView?.invoke(path: "webModules.config.setTheme", message: message, completion: completion) + } + + public func setFontFamily(fontFamily: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let fontFamily: String + } + + let message = Message( + fontFamily: fontFamily + ) + + webView?.invoke(path: "webModules.config.setFontFamily", message: message, completion: completion) + } + + public func setFontSize(fontSize: Double, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let fontSize: Double + } + + let message = Message( + fontSize: fontSize + ) + + webView?.invoke(path: "webModules.config.setFontSize", message: message, completion: completion) + } + + public func setShowLineNumbers(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setShowLineNumbers", message: message, completion: completion) + } + + public func setShowActiveLineIndicator(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setShowActiveLineIndicator", message: message, completion: completion) + } + + public func setShowInvisibles(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setShowInvisibles", message: message, completion: completion) + } + + public func setTypewriterMode(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setTypewriterMode", message: message, completion: completion) + } + + public func setFocusMode(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setFocusMode", message: message, completion: completion) + } + + public func setLineWrapping(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.config.setLineWrapping", message: message, completion: completion) + } + + public func setLineHeight(lineHeight: Double, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let lineHeight: Double + } + + let message = Message( + lineHeight: lineHeight + ) + + webView?.invoke(path: "webModules.config.setLineHeight", message: message, completion: completion) + } + + public func setDefaultLineBreak(lineBreak: String?, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let lineBreak: String? + } + + let message = Message( + lineBreak: lineBreak + ) + + webView?.invoke(path: "webModules.config.setDefaultLineBreak", message: message, completion: completion) + } + + public func setTabKeyBehavior(behavior: TabKeyBehavior, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let behavior: TabKeyBehavior + } + + let message = Message( + behavior: behavior + ) + + webView?.invoke(path: "webModules.config.setTabKeyBehavior", message: message, completion: completion) + } + + public func setIndentUnit(unit: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let unit: String + } + + let message = Message( + unit: unit + ) + + webView?.invoke(path: "webModules.config.setIndentUnit", message: message, completion: completion) + } +} + +public enum TabKeyBehavior: Int, Codable { + case insertTab = 0 + case insertTwoSpaces = 1 + case insertFourSpaces = 2 +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeCore.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeCore.swift new file mode 100644 index 00000000..a637290a --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeCore.swift @@ -0,0 +1,54 @@ +// +// WebBridgeCore.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeCore { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func resetEditor(text: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let text: String + } + + let message = Message( + text: text + ) + + webView?.invoke(path: "webModules.core.resetEditor", message: message, completion: completion) + } + + public func clearEditor(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.core.clearEditor", completion: completion) + } + + @MainActor public func getEditorText() async throws -> String { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.core.getEditorText") { + continuation.resume(with: $0) + } + } + } + + public func markEditorDirty(isDirty: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let isDirty: Bool + } + + let message = Message( + isDirty: isDirty + ) + + webView?.invoke(path: "webModules.core.markEditorDirty", message: message, completion: completion) + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeFormat.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeFormat.swift new file mode 100644 index 00000000..92b3a0a0 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeFormat.swift @@ -0,0 +1,130 @@ +// +// WebBridgeFormat.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeFormat { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func toggleBold(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleBold", completion: completion) + } + + public func toggleItalic(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleItalic", completion: completion) + } + + public func toggleStrikethrough(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleStrikethrough", completion: completion) + } + + public func toggleHeading(level: Int, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let level: Int + } + + let message = Message( + level: level + ) + + webView?.invoke(path: "webModules.format.toggleHeading", message: message, completion: completion) + } + + public func toggleBullet(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleBullet", completion: completion) + } + + public func toggleNumbering(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleNumbering", completion: completion) + } + + public func toggleTodo(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleTodo", completion: completion) + } + + public func toggleBlockquote(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleBlockquote", completion: completion) + } + + public func toggleInlineCode(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleInlineCode", completion: completion) + } + + public func toggleInlineMath(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.toggleInlineMath", completion: completion) + } + + public func insertCodeBlock(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.insertCodeBlock", completion: completion) + } + + public func insertMathBlock(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.insertMathBlock", completion: completion) + } + + public func insertHorizontalRule(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.format.insertHorizontalRule", completion: completion) + } + + public func insertHyperLink(title: String, url: String, prefix: String?, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let title: String + let url: String + let prefix: String? + } + + let message = Message( + title: title, + url: url, + prefix: prefix + ) + + webView?.invoke(path: "webModules.format.insertHyperLink", message: message, completion: completion) + } + + public func insertTable(columnName: String, itemName: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let columnName: String + let itemName: String + } + + let message = Message( + columnName: columnName, + itemName: itemName + ) + + webView?.invoke(path: "webModules.format.insertTable", message: message, completion: completion) + } + + public func performEditCommand(command: EditCommand, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let command: EditCommand + } + + let message = Message( + command: command + ) + + webView?.invoke(path: "webModules.format.performEditCommand", message: message, completion: completion) + } +} + +public enum EditCommand: String, Codable { + case indentLess = "indentLess" + case indentMore = "indentMore" + case selectLine = "selectLine" + case moveLineUp = "moveLineUp" + case moveLineDown = "moveLineDown" + case copyLineUp = "copyLineUp" + case copyLineDown = "copyLineDown" +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeGrammarly.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeGrammarly.swift new file mode 100644 index 00000000..42058151 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeGrammarly.swift @@ -0,0 +1,48 @@ +// +// WebBridgeGrammarly.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeGrammarly { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func connect(clientID: String, redirectURI: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let clientID: String + let redirectURI: String + } + + let message = Message( + clientID: clientID, + redirectURI: redirectURI + ) + + webView?.invoke(path: "webModules.grammarly.connect", message: message, completion: completion) + } + + public func disconnect(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.grammarly.disconnect", completion: completion) + } + + public func completeOAuth(url: String, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let url: String + } + + let message = Message( + url: url + ) + + webView?.invoke(path: "webModules.grammarly.completeOAuth", message: message, completion: completion) + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeHistory.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeHistory.swift new file mode 100644 index 00000000..8ed88b22 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeHistory.swift @@ -0,0 +1,42 @@ +// +// WebBridgeHistory.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeHistory { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func undo(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.history.undo", completion: completion) + } + + public func redo(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.history.redo", completion: completion) + } + + @MainActor public func canUndo() async throws -> Bool { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.history.canUndo") { + continuation.resume(with: $0) + } + } + } + + @MainActor public func canRedo() async throws -> Bool { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.history.canRedo") { + continuation.resume(with: $0) + } + } + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeLineEndings.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeLineEndings.swift new file mode 100644 index 00000000..de590971 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeLineEndings.swift @@ -0,0 +1,49 @@ +// +// WebBridgeLineEndings.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeLineEndings { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + @MainActor public func getLineEndings() async throws -> LineEndings { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.lineEndings.getLineEndings") { + continuation.resume(with: $0) + } + } + } + + public func setLineEndings(lineEndings: LineEndings, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let lineEndings: LineEndings + } + + let message = Message( + lineEndings: lineEndings + ) + + webView?.invoke(path: "webModules.lineEndings.setLineEndings", message: message, completion: completion) + } +} + +public enum LineEndings: Int, Codable { + /// Unspecified, let CodeMirror do the normalization magic. + case unspecified = 0 + /// Line Feed, used on macOS and Unix systems. + case lf = 1 + /// Carriage Return and Line Feed, used on Windows. + case crlf = 2 + /// Carriage Return, previously used on Classic Mac OS. + case cr = 3 +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSearch.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSearch.swift new file mode 100644 index 00000000..859d54f0 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSearch.swift @@ -0,0 +1,92 @@ +// +// WebBridgeSearch.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeSearch { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func setState(enabled: Bool, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let enabled: Bool + } + + let message = Message( + enabled: enabled + ) + + webView?.invoke(path: "webModules.search.setState", message: message, completion: completion) + } + + @MainActor public func updateQuery(options: SearchOptions) async throws -> Int { + struct Message: Encodable { + let options: SearchOptions + } + + let message = Message( + options: options + ) + + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.search.updateQuery", message: message) { + continuation.resume(with: $0) + } + } + } + + public func findNext(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.search.findNext", completion: completion) + } + + public func findPrevious(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.search.findPrevious", completion: completion) + } + + public func replaceNext(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.search.replaceNext", completion: completion) + } + + public func replaceAll(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.search.replaceAll", completion: completion) + } + + public func selectAllOccurrences(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.search.selectAllOccurrences", completion: completion) + } + + @MainActor public func numberOfMatches() async throws -> Int { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.search.numberOfMatches") { + continuation.resume(with: $0) + } + } + } +} + +public struct SearchOptions: Codable { + public var search: String + public var caseSensitive: Bool + public var literal: Bool + public var regexp: Bool + public var wholeWord: Bool + public var replace: String? + + public init(search: String, caseSensitive: Bool, literal: Bool, regexp: Bool, wholeWord: Bool, replace: String?) { + self.search = search + self.caseSensitive = caseSensitive + self.literal = literal + self.regexp = regexp + self.wholeWord = wholeWord + self.replace = replace + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSelection.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSelection.swift new file mode 100644 index 00000000..90760334 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeSelection.swift @@ -0,0 +1,42 @@ +// +// WebBridgeSelection.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeSelection { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + @MainActor public func getText() async throws -> String { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.selection.getText") { + continuation.resume(with: $0) + } + } + } + + public func scrollToSelection(completion: ((Result) -> Void)? = nil) { + webView?.invoke(path: "webModules.selection.scrollToSelection", completion: completion) + } + + public func gotoLine(lineNumber: Int, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let lineNumber: Int + } + + let message = Message( + lineNumber: lineNumber + ) + + webView?.invoke(path: "webModules.selection.gotoLine", message: message, completion: completion) + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTableOfContents.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTableOfContents.swift new file mode 100644 index 00000000..72ca92a3 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTableOfContents.swift @@ -0,0 +1,52 @@ +// +// WebBridgeTableOfContents.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeTableOfContents { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + @MainActor public func getTableOfContents() async throws -> [HeadingInfo] { + return try await withCheckedThrowingContinuation { continuation in + webView?.invoke(path: "webModules.toc.getTableOfContents") { + continuation.resume(with: $0) + } + } + } + + public func gotoHeader(headingInfo: HeadingInfo, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let headingInfo: HeadingInfo + } + + let message = Message( + headingInfo: headingInfo + ) + + webView?.invoke(path: "webModules.toc.gotoHeader", message: message, completion: completion) + } +} + +public struct HeadingInfo: Codable { + public var title: String + public var level: Int + public var from: Int + public var to: Int + + public init(title: String, level: Int, from: Int, to: Int) { + self.title = title + self.level = level + self.from = from + self.to = to + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTextChecker.swift b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTextChecker.swift new file mode 100644 index 00000000..85d8b2c5 --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeTextChecker.swift @@ -0,0 +1,44 @@ +// +// WebBridgeTextChecker.swift +// +// Generated using https://github.com/microsoft/ts-gyb +// +// Don't modify this file manually, it's auto generated. +// +// To make changes, edit template files under /CoreEditor/src/@codegen + +import WebKit + +public final class WebBridgeTextChecker { + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + } + + public func update(options: TextCheckerOptions, completion: ((Result) -> Void)? = nil) { + struct Message: Encodable { + let options: TextCheckerOptions + } + + let message = Message( + options: options + ) + + webView?.invoke(path: "webModules.textChecker.update", message: message, completion: completion) + } +} + +public struct TextCheckerOptions: Codable { + public var spellcheck: Bool + public var autocorrect: Bool + public var autocomplete: Bool + public var autocapitalize: Bool + + public init(spellcheck: Bool, autocorrect: Bool, autocomplete: Bool, autocapitalize: Bool) { + self.spellcheck = spellcheck + self.autocorrect = autocorrect + self.autocomplete = autocomplete + self.autocapitalize = autocapitalize + } +} diff --git a/MarkEditKit/Sources/Bridge/Web/WebModuleBridge.swift b/MarkEditKit/Sources/Bridge/Web/WebModuleBridge.swift new file mode 100644 index 00000000..15cc9b8a --- /dev/null +++ b/MarkEditKit/Sources/Bridge/Web/WebModuleBridge.swift @@ -0,0 +1,36 @@ +// +// WebModuleBridge.swift +// +// Created by cyan on 12/16/22. +// + +import WebKit + +/** + Wrapper for all web bridges. + */ +public struct WebModuleBridge { + public let config: WebBridgeConfig + public let core: WebBridgeCore + public let history: WebBridgeHistory + public let lineEndings: WebBridgeLineEndings + public let textChecker: WebBridgeTextChecker + public let selection: WebBridgeSelection + public let format: WebBridgeFormat + public let search: WebBridgeSearch + public let toc: WebBridgeTableOfContents + public let grammarly: WebBridgeGrammarly + + public init(webView: WKWebView) { + self.config = WebBridgeConfig(webView: webView) + self.core = WebBridgeCore(webView: webView) + self.history = WebBridgeHistory(webView: webView) + self.lineEndings = WebBridgeLineEndings(webView: webView) + self.textChecker = WebBridgeTextChecker(webView: webView) + self.selection = WebBridgeSelection(webView: webView) + self.format = WebBridgeFormat(webView: webView) + self.search = WebBridgeSearch(webView: webView) + self.toc = WebBridgeTableOfContents(webView: webView) + self.grammarly = WebBridgeGrammarly(webView: webView) + } +} diff --git a/MarkEditKit/Sources/EditorLogger.swift b/MarkEditKit/Sources/EditorLogger.swift new file mode 100644 index 00000000..29cdc7ed --- /dev/null +++ b/MarkEditKit/Sources/EditorLogger.swift @@ -0,0 +1,29 @@ +// +// EditorLogger.swift +// +// Created by cyan on 12/22/22. +// + +import Foundation +import os.log + +public enum Logger { + public static func log(_ level: OSLogType, _ message: @autoclosure @escaping () -> String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { + var file: String = "\(file)" + if let url = URL(string: file) { + file = url.lastPathComponent + } + + os_log(level, log: .default, "\(file):\(line), \(function) -> \(message())") + } + + public static func assertFail(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { + assertionFailure(message(), file: file, line: line) + } + + public static func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { + if !condition() { + assertionFailure(message(), file: file, line: line) + } + } +} diff --git a/MarkEditKit/Sources/EditorMessageHandler.swift b/MarkEditKit/Sources/EditorMessageHandler.swift new file mode 100644 index 00000000..bc7e5950 --- /dev/null +++ b/MarkEditKit/Sources/EditorMessageHandler.swift @@ -0,0 +1,115 @@ +// +// EditorMessageHandler.swift +// +// Created by cyan on 12/22/22. +// + +import WebKit + +/** + Receive messages sent from the web, execute functions and get back to the web. + */ +public final class EditorMessageHandler: NSObject, WKScriptMessageHandler { + private let modules: NativeModules + private let webViewProvider: (() -> WKWebView?) + + public init(modules: NativeModules, webViewProvider: @escaping (() -> WKWebView?)) { + self.modules = modules + self.webViewProvider = webViewProvider + } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.name == "bridge", let body = message.body as? [String: Any] else { + Logger.assertFail("Invalid message payload") + return + } + + guard let moduleName = body["moduleName"] as? String else { + Logger.assertFail("Invalid module name") + return + } + + guard let methodName = body["methodName"] as? String else { + Logger.assertFail("Invalid method name") + return + } + + guard let invokeNative = modules[moduleName]?[methodName] else { + Logger.assertFail("Invalid native method") + return + } + + guard let parameters = (body["parameters"] as? String)?.toData() else { + Logger.assertFail("Invalid parameters") + return + } + + guard let messageID = body["id"] as? String else { + Logger.assertFail("Invalid message id") + return + } + + Task { + Logger.log(.debug, "Invoke native: \(moduleName).\(methodName)") + if let result = await invokeNative(parameters) { + reply(id: messageID, result: result) + } + } + } +} + +// MARK: - Private + +private extension EditorMessageHandler { + /// Reply to a message sent by JavaScript + func reply(id: String, result: Result) { + guard let webView = webViewProvider() else { + Logger.log(.error, "Missing WebView to proceed") + return + } + + struct NativeReply: Encodable { + let id: String + let result: AnyEncodable? + let error: String? + } + + if case .failure(let error) = result { + Logger.assertFail(error.localizedDescription) + } + + let reply: NativeReply + switch result { + case .success(let value): + if let value = value { + reply = NativeReply(id: id, result: AnyEncodable(value: value), error: nil) + } else { + reply = NativeReply(id: id, result: nil, error: nil) + } + case .failure(let error): + reply = NativeReply(id: id, result: nil, error: error.localizedDescription) + } + + DispatchQueue.onMainThread { + webView.invoke(path: "window.handleNativeReply", message: reply) + } + } +} + +/// Encodable wrapper for generics +/// +/// https://www.dabby.dev/article/2019-04-25-any-encodable +private struct AnyEncodable: Encodable { + let value: Encodable + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try value.encode(to: &container) + } +} + +private extension Encodable { + func encode(to container: inout SingleValueEncodingContainer) throws { + try container.encode(self) + } +} diff --git a/MarkEditKit/Sources/EditorTextEncoding.swift b/MarkEditKit/Sources/EditorTextEncoding.swift new file mode 100644 index 00000000..1f33cdaa --- /dev/null +++ b/MarkEditKit/Sources/EditorTextEncoding.swift @@ -0,0 +1,95 @@ +// +// EditorTextEncoding.swift +// +// Created by cyan on 1/4/23. +// + +import Foundation + +/// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/readingFiles.html#//apple_ref/doc/uid/TP40003459-SW4 +/// +/// We *can*, but don't want to, include all supported encodings, which makes the UI super complicated, +/// Markdown prefers utf-8 as mentioned here: https://daringfireball.net/linked/2011/08/05/markdown-uti +public enum EditorTextEncoding: CaseIterable, CustomStringConvertible, Codable { + // Derived from String.Encoding + case ascii + case nonLossyASCII + case utf8 + case utf16 + case utf16BigEndian + case utf16LittleEndian + case macOSRoman + case isoLatin1 + case windowsLatin1 + + // Derived from CFStringEncodings + case gb18030 + case big5 + case japaneseEUC + case shiftJIS + case koreanEUC + + public var description: String { + switch self { + case .ascii: return "ASCII" + case .nonLossyASCII: return "Non-lossy ASCII" + case .utf8: return "Unicode (UTF-8)" + case .utf16: return "Unicode (UTF-16)" + case .utf16BigEndian: return "Unicode (UTF-16BE)" + case .utf16LittleEndian: return "Unicode (UTF-16LE)" + case .macOSRoman: return "Western (Mac OS Roman)" + case .isoLatin1: return "Western (ISO Latin 1)" + case .windowsLatin1: return "Western (Windows Latin 1)" + case .gb18030: return "Simplified Chinese (GB 18030)" + case .big5: return "Traditional Chinese (Big 5)" + case .japaneseEUC: return "Japanese (EUC)" + case .shiftJIS: return "Japanese (Shift JIS)" + case .koreanEUC: return "Korean (EUC)" + } + } + + public func encode(string: String) -> Data? { + switch self { + case .ascii: return string.data(using: .ascii) + case .nonLossyASCII: return string.data(using: .nonLossyASCII) + case .utf8: return string.data(using: .utf8) + case .utf16: return string.data(using: .utf16) + case .utf16BigEndian: return string.data(using: .utf16BigEndian) + case .utf16LittleEndian: return string.data(using: .utf16LittleEndian) + case .macOSRoman: return string.data(using: .macOSRoman) + case .isoLatin1: return string.data(using: .isoLatin1) + case .windowsLatin1: return string.data(using: .windowsCP1252) + case .gb18030: return string.data(using: .GB_18030_2000) + case .big5: return string.data(using: .big5) + case .japaneseEUC: return string.data(using: .japaneseEUC) + case .shiftJIS: return string.data(using: .shiftJIS) + case .koreanEUC: return string.data(using: .EUC_KR) + } + } + + public func decode(data: Data) -> String? { + switch self { + case .ascii: return String(data: data, encoding: .ascii) + case .nonLossyASCII: return String(data: data, encoding: .nonLossyASCII) + case .utf8: return String(data: data, encoding: .utf8) + case .utf16: return String(data: data, encoding: .utf16) + case .utf16BigEndian: return String(data: data, encoding: .utf16BigEndian) + case .utf16LittleEndian: return String(data: data, encoding: .utf16LittleEndian) + case .macOSRoman: return String(data: data, encoding: .macOSRoman) + case .isoLatin1: return String(data: data, encoding: .isoLatin1) + case .windowsLatin1: return String(data: data, encoding: .windowsCP1252) + case .gb18030: return String(data: data, encoding: .GB_18030_2000) + case .big5: return String(data: data, encoding: .big5) + case .japaneseEUC: return String(data: data, encoding: .japaneseEUC) + case .shiftJIS: return String(data: data, encoding: String.Encoding.shiftJIS) + case .koreanEUC: return String(data: data, encoding: .EUC_KR) + } + } +} + +public extension EditorTextEncoding { + /// In menus, grouping cases with a separator + static var groupingCases: Set { + Set([.nonLossyASCII, .utf16LittleEndian, .windowsLatin1, .big5, .shiftJIS]) + } +} diff --git a/MarkEditKit/Sources/Extensions/DispatchQueue+Extension.swift b/MarkEditKit/Sources/Extensions/DispatchQueue+Extension.swift new file mode 100644 index 00000000..bc8f9473 --- /dev/null +++ b/MarkEditKit/Sources/Extensions/DispatchQueue+Extension.swift @@ -0,0 +1,21 @@ +// +// DispatchQueue+Extension.swift +// +// Created by cyan on 12/13/22. +// + +import Foundation + +public extension DispatchQueue { + static func onMainThread(_ execute: @escaping () -> Void) { + if Thread.isMainThread { + execute() + } else { + DispatchQueue.main.async(execute: execute) + } + } + + static func afterDelay(seconds: TimeInterval, execute: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: execute) + } +} diff --git a/MarkEditKit/Sources/Extensions/JSRect+Extension.swift b/MarkEditKit/Sources/Extensions/JSRect+Extension.swift new file mode 100644 index 00000000..635f7e23 --- /dev/null +++ b/MarkEditKit/Sources/Extensions/JSRect+Extension.swift @@ -0,0 +1,13 @@ +// +// JSRect+Extension.swift +// +// Created by cyan on 1/7/23. +// + +import Foundation + +public extension JSRect { + var cgRect: CGRect { + CGRect(x: x, y: y, width: width, height: height) + } +} diff --git a/MarkEditKit/Sources/Extensions/LineEndings+Extension.swift b/MarkEditKit/Sources/Extensions/LineEndings+Extension.swift new file mode 100644 index 00000000..5cd1057b --- /dev/null +++ b/MarkEditKit/Sources/Extensions/LineEndings+Extension.swift @@ -0,0 +1,20 @@ +// +// LineEndings+Extension.swift +// +// Created by cyan on 1/28/23. +// + +import Foundation + +public extension LineEndings { + var characters: String { + if self == .crlf { + return "\r\n" + } else if self == .cr { + return "\r" + } else { + // LF is the preferred line endings on modern macOS + return "\n" + } + } +} diff --git a/MarkEditKit/Sources/Extensions/URL+Extension.swift b/MarkEditKit/Sources/Extensions/URL+Extension.swift new file mode 100644 index 00000000..48627468 --- /dev/null +++ b/MarkEditKit/Sources/Extensions/URL+Extension.swift @@ -0,0 +1,17 @@ +// +// URL+Extension.swift +// +// Created by cyan on 1/15/23. +// + +import Foundation + +public extension URL { + var localizedName: String { + (try? resourceValues(forKeys: Set([.localizedNameKey])))?.name ?? lastPathComponent + } + + func replacingPathExtension(_ pathExtension: String) -> URL { + deletingPathExtension().appendingPathExtension(pathExtension) + } +} diff --git a/MarkEditKit/Sources/Extensions/UserDefaults+Extension.swift b/MarkEditKit/Sources/Extensions/UserDefaults+Extension.swift new file mode 100644 index 00000000..c203b0c7 --- /dev/null +++ b/MarkEditKit/Sources/Extensions/UserDefaults+Extension.swift @@ -0,0 +1,38 @@ +// +// UserDefaults+Extension.swift +// +// Created by cyan on 12/17/22. +// + +import Foundation + +public extension UserDefaults { + static func overwriteTextCheckerOnce() { + let enabledFlag = "editor.overwrite-text-checker" + guard !standard.bool(forKey: enabledFlag) else { + return + } + + // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/mac/TextCheckerMac.mm + let featureKeys = [ + // Features we enable once until user explicitly changes the setting + "NSAllowContinuousSpellChecking", + "WebAutomaticSpellingCorrectionEnabled", + "WebContinuousSpellCheckingEnabled", + "WebGrammarCheckingEnabled", + "WebAutomaticLinkDetectionEnabled", + "WebAutomaticTextReplacementEnabled", + + // Features that respect the system settings + // "WebSmartInsertDeleteEnabled", + // "WebAutomaticQuoteSubstitutionEnabled", + // "WebAutomaticDashSubstitutionEnabled", + ] + + featureKeys.forEach { + standard.setValue(true, forKey: $0) + } + + standard.setValue(true, forKey: enabledFlag) + } +} diff --git a/MarkEditKit/Sources/Extensions/WKWebView+Extension.swift b/MarkEditKit/Sources/Extensions/WKWebView+Extension.swift new file mode 100644 index 00000000..31cae61e --- /dev/null +++ b/MarkEditKit/Sources/Extensions/WKWebView+Extension.swift @@ -0,0 +1,75 @@ +// +// WKWebView+Extension.swift +// +// Created by cyan on 12/22/22. +// + +import WebKit +import MarkEditCore + +/** + WKWebView extension to encode and decode messages. + */ +extension WKWebView { + @frozen public enum InvokeError: Error { + case unexpectedNil + case decodeError + case evaluateError(path: String, error: Error?) + } + + typealias VoidCompletion = (Result) -> Void + typealias Completion = (Result) -> Void +} + +extension WKWebView { + func invoke(path: String, message: Encodable = Message(), completion: VoidCompletion? = nil) { + invoke(path: path, message: message) { (result: Result) in + completion?(result.map { _ in () }) + } + } + + func invoke(path: String, message: Encodable = Message(), completion: Completion? = nil) { + invoke(path: path, message: message) { (result: Result) in + completion?(result.flatMap { value in + guard let value, !(value is NSNull) else { + return .failure(.unexpectedNil) + } + + // Primitive types + if let value = value as? SuccessResult { + return .success(value) + } + + do { + // JSON encoded types + let data = try JSONSerialization.data(withJSONObject: value, options: .fragmentsAllowed) + let decoded = try JSONDecoder().decode(SuccessResult.self, from: data) + return .success(decoded) + } catch { + Logger.log(.error, error.localizedDescription) + return .failure(.decodeError) + } + }) + } + } +} + +// MARK: - Private + +private extension WKWebView { + struct Message: Encodable { + // Empty message used for zero parameter functions + } + + func invoke(path: String, message: Encodable, completion: ((Result) -> Void)? = nil) { + let script = "\(path)(\(message.jsonEncoded))" + evaluateJavaScript(script) { result, error in + if let error { + Logger.log(.error, error.localizedDescription) + completion?(.failure(.evaluateError(path: path, error: error))) + } else { + completion?(.success(result)) + } + } + } +} diff --git a/MarkEditKit/Sources/Extensions/WKWebViewConfiguration+Extension.swift b/MarkEditKit/Sources/Extensions/WKWebViewConfiguration+Extension.swift new file mode 100644 index 00000000..25483655 --- /dev/null +++ b/MarkEditKit/Sources/Extensions/WKWebViewConfiguration+Extension.swift @@ -0,0 +1,31 @@ +// +// WKWebViewConfiguration+Extension.swift +// +// Created by cyan on 1/7/23. +// + +import WebKit + +public extension WKWebViewConfiguration { + static func newConfig() -> Self { + let config = Self() + if config.responds(to: sel_getUid("_drawsBackground")) { + // To mimic settable isOpaque on iOS, + // which is required for the background color and initial white flash in dark mode + config.setValue(false, forKey: "drawsBackground") + } else { + Logger.assertFail("Failed to overwrite drawsBackground in WKWebViewConfiguration") + } + +#if DEBUG + // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/API/Cocoa/WKPreferences.mm + if config.preferences.responds(to: sel_getUid("_developerExtrasEnabled")) { + config.preferences.setValue(true, forKey: "developerExtrasEnabled") + } else { + Logger.assertFail("Failed to overwrite developerExtrasEnabled in WKPreferences") + } +#endif + + return config + } +} diff --git a/MarkEditMac/Base.lproj/Main.storyboard b/MarkEditMac/Base.lproj/Main.storyboard new file mode 100755 index 00000000..9143f667 --- /dev/null +++ b/MarkEditMac/Base.lproj/Main.storyboard @@ -0,0 +1,808 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditMac/Info.entitlements b/MarkEditMac/Info.entitlements new file mode 100644 index 00000000..637aa6f1 --- /dev/null +++ b/MarkEditMac/Info.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.print + + + diff --git a/MarkEditMac/Info.plist b/MarkEditMac/Info.plist new file mode 100644 index 00000000..6ee5ba33 --- /dev/null +++ b/MarkEditMac/Info.plist @@ -0,0 +1,94 @@ + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + Markdown + LSTypeIsPackage + + CFBundleTypeIconFile + + CFBundleTypeRole + Editor + LSHandlerRank + Owner + CFBundleTypeExtensions + + md + markdown + mdown + mkdn + mkd + mdwn + + LSItemContentTypes + + net.daringfireball.markdown + + NSDocumentClass + $(PRODUCT_MODULE_NAME).EditorDocument + + + CFBundleTypeName + Text + LSTypeIsPackage + + CFBundleTypeIconFile + + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.plain-text + + NSDocumentClass + $(PRODUCT_MODULE_NAME).EditorDocument + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLIconFile + + CFBundleURLName + markedit + CFBundleURLSchemes + + markedit + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.plain-text + + UTTypeDescription + Markdown + UTTypeIdentifier + net.daringfireball.markdown + UTTypeTagSpecification + + public.filename-extension + + md + markdown + mdwn + mdown + mkd + mkdn + + + + + + diff --git a/MarkEditMac/Modules/.gitignore b/MarkEditMac/Modules/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/MarkEditMac/Modules/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitControls.xcscheme b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitControls.xcscheme new file mode 100644 index 00000000..cb568b4f --- /dev/null +++ b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitControls.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitExtensions.xcscheme b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitExtensions.xcscheme new file mode 100644 index 00000000..90fe5357 --- /dev/null +++ b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/AppKitExtensions.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/FontPicker.xcscheme b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/FontPicker.xcscheme new file mode 100644 index 00000000..14607f63 --- /dev/null +++ b/MarkEditMac/Modules/.swiftpm/xcode/xcshareddata/xcschemes/FontPicker.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkEditMac/Modules/Package.swift b/MarkEditMac/Modules/Package.swift new file mode 100644 index 00000000..8fdc2992 --- /dev/null +++ b/MarkEditMac/Modules/Package.swift @@ -0,0 +1,104 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Modules", + platforms: [ + .macOS(.v12), + ], + products: [ + .library( + name: "AppKitControls", + targets: ["AppKitControls"] + ), + .library( + name: "AppKitExtensions", + targets: ["AppKitExtensions"] + ), + .library( + name: "FontPicker", + targets: ["FontPicker"] + ), + .library( + name: "Previewer", + targets: ["Previewer"] + ), + .library( + name: "Proofing", + targets: ["Proofing"] + ), + .library( + name: "SettingsUI", + targets: ["SettingsUI"] + ), + ], + dependencies: [ + .package(path: "../MarkEditKit"), + .package(path: "../MarkEditTools"), + ], + targets: [ + .target( + name: "AppKitControls", + dependencies: ["AppKitExtensions"], + path: "Sources/AppKitControls", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + .target( + name: "AppKitExtensions", + path: "Sources/AppKitExtensions", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + .target( + name: "FontPicker", + dependencies: ["AppKitExtensions"], + path: "Sources/FontPicker", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + .target( + name: "Previewer", + dependencies: ["MarkEditKit", "AppKitExtensions"], + path: "Sources/Previewer", + resources: [ + .process("Resources"), + ], + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + .target( + name: "Proofing", + dependencies: ["MarkEditKit"], + path: "Sources/Proofing", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + .target( + name: "SettingsUI", + dependencies: ["AppKitExtensions"], + path: "Sources/SettingsUI", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + + .testTarget( + name: "ModulesTests", + dependencies: [ + "AppKitExtensions", + ], + path: "Tests", + plugins: [ + .plugin(name: "SwiftLint", package: "MarkEditTools"), + ] + ), + ] +) diff --git a/MarkEditMac/Modules/README.md b/MarkEditMac/Modules/README.md new file mode 100644 index 00000000..bb5fdff0 --- /dev/null +++ b/MarkEditMac/Modules/README.md @@ -0,0 +1,7 @@ +# MarkEditMac.Modules + +Isolated modules that serve the MarkEditMac business logic, including AppKit extensions, customized UI controls, etc. + +Each folder in this package produces a standalone target, they can be built independently. + +> Everything in this package is written for macOS only. \ No newline at end of file diff --git a/MarkEditMac/Modules/Sources/AppKitControls/BackgroundTheming.swift b/MarkEditMac/Modules/Sources/AppKitControls/BackgroundTheming.swift new file mode 100644 index 00000000..bf31f530 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/BackgroundTheming.swift @@ -0,0 +1,22 @@ +// +// BackgroundTheming.swift +// +// Created by cyan on 1/31/23. +// + +import AppKit +import AppKitExtensions + +public protocol BackgroundTheming: NSView {} + +public extension BackgroundTheming { + func setBackgroundColor(_ color: NSColor) { + layerBackgroundColor = color + needsDisplay = true + + enumerateChildren { (button: NonBezelButton) in + button.layerBackgroundColor = color + button.needsDisplay = true + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/Buttons/IconOnlyButton.swift b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/IconOnlyButton.swift new file mode 100644 index 00000000..f77cf040 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/IconOnlyButton.swift @@ -0,0 +1,28 @@ +// +// IconOnlyButton.swift +// +// Created by cyan on 12/17/22. +// + +import AppKit + +public final class IconOnlyButton: NonBezelButton { + public init(symbolName: String, accessibilityLabel: String? = nil) { + super.init() + toolTip = accessibilityLabel + + if let iconImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityLabel) { + let iconView = NSImageView(image: iconImage) + iconView.contentTintColor = .labelColor + iconView.translatesAutoresizingMaskIntoConstraints = false + addSubview(iconView) + + NSLayoutConstraint.activate([ + iconView.widthAnchor.constraint(equalToConstant: iconImage.size.width), + iconView.heightAnchor.constraint(equalToConstant: iconImage.size.height), + iconView.centerXAnchor.constraint(equalTo: centerXAnchor), + iconView.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/Buttons/NonBezelButton.swift b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/NonBezelButton.swift new file mode 100644 index 00000000..1b718bdb --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/NonBezelButton.swift @@ -0,0 +1,35 @@ +// +// NonBezelButton.swift +// +// Created by cyan on 12/17/22. +// + +import AppKit + +public class NonBezelButton: NSButton { + init() { + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func draw(_ dirtyRect: CGRect) { + super.draw(dirtyRect) + layerBackgroundColor?.setFill() + + let rectPath = NSBezierPath(rect: bounds) + rectPath.fill() + + if isHighlighted { + NSColor.plainButtonHighlighted.setFill() + rectPath.fill() + } + } + + override public func resetCursorRects() { + addCursorRect(bounds, cursor: .arrow) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/Buttons/TitleOnlyButton.swift b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/TitleOnlyButton.swift new file mode 100644 index 00000000..26c040fe --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/Buttons/TitleOnlyButton.swift @@ -0,0 +1,28 @@ +// +// TitleOnlyButton.swift +// +// Created by cyan on 12/27/22. +// + +import AppKit + +public final class TitleOnlyButton: NonBezelButton { + public let labelView = LabelView() + + public init(title: String? = nil, fontSize: Double? = nil) { + super.init() + + labelView.stringValue = title ?? "" + labelView.translatesAutoresizingMaskIntoConstraints = false + addSubview(labelView) + + if let fontSize { + labelView.font = .systemFont(ofSize: fontSize) + } + + NSLayoutConstraint.activate([ + labelView.centerXAnchor.constraint(equalTo: centerXAnchor), + labelView.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/DividerView.swift b/MarkEditMac/Modules/Sources/AppKitControls/DividerView.swift new file mode 100644 index 00000000..ee302e1c --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/DividerView.swift @@ -0,0 +1,34 @@ +// +// DividerView.swift +// +// Created by cyan on 12/17/22. +// + +import AppKit + +/** + Hairline-width divider, it requires manual layout to be correctly rendered. + */ +public final class DividerView: NSView { + public var length: Double { + hairlineWidth ? (1.0 / (window?.screen?.backingScaleFactor ?? 1)) : 1 + } + + private let color: NSColor + private let hairlineWidth: Bool + + public init(color: NSColor = .separatorColor, hairlineWidth: Bool = true) { + self.color = color + self.hairlineWidth = hairlineWidth + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func updateLayer() { + layerBackgroundColor = color + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/FocusTrackingView.swift b/MarkEditMac/Modules/Sources/AppKitControls/FocusTrackingView.swift new file mode 100644 index 00000000..638dfbe5 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/FocusTrackingView.swift @@ -0,0 +1,20 @@ +// +// FocusTrackingView.swift +// +// Created by cyan on 1/7/23. +// + +import AppKit + +/** + Tracks the focus rect to help us present popovers. + */ +public final class FocusTrackingView: NSView { + override public func hitTest(_ point: NSPoint) -> NSView? { + nil + } + + override public func isAccessibilityHidden() -> Bool { + true + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineView.swift b/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineView.swift new file mode 100644 index 00000000..a3e12989 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineView.swift @@ -0,0 +1,104 @@ +// +// GotoLineView.swift +// +// Created by cyan on 1/17/23. +// + +import AppKit + +final class GotoLineView: NSView { + private enum Constants { + static let cornerRadius: Double = 12 + static let padding: Double = 8 + } + + private let effectView: NSVisualEffectView = { + let effectView = NSVisualEffectView() + effectView.material = .popover + + return effectView + }() + + private let textField: NSTextField = { + let textField = NSTextField() + textField.font = .systemFont(ofSize: 20, weight: .light) + textField.focusRingType = .none + textField.drawsBackground = false + textField.isBezeled = false + + return textField + }() + + private let handler: (Int) -> Void + + init(frame: CGRect, placeholder: String, iconName: String, handler: @escaping (Int) -> Void) { + self.handler = handler + super.init(frame: frame) + + wantsLayer = true + layer?.cornerCurve = .continuous + layer?.cornerRadius = Constants.cornerRadius + + effectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(effectView) + + let iconView = NSImageView(image: .with(symbolName: iconName, pointSize: 24, weight: .light)) + iconView.translatesAutoresizingMaskIntoConstraints = false + addSubview(iconView) + + textField.placeholderString = placeholder + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + addSubview(textField) + + NSLayoutConstraint.activate([ + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + effectView.topAnchor.constraint(equalTo: topAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + + iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding), + iconView.centerYAnchor.constraint(equalTo: centerYAnchor), + iconView.heightAnchor.constraint(equalToConstant: iconView.frame.height), + iconView.widthAnchor.constraint(equalToConstant: iconView.frame.width), + + textField.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: Constants.padding), + textField.centerYAnchor.constraint(equalTo: centerYAnchor), + textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding), + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - NSTextFieldDelegate + +extension GotoLineView: NSTextFieldDelegate { + func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool { + if selector == #selector(insertNewline(_:)) { + performGotoLine() + return true + } + + return false + } +} + +// MARK: - Private + +private extension GotoLineView { + func performGotoLine() { + // We don't know exactly how many lines we have, unreasonable values will fail silently + guard let lineNumber = Int(textField.stringValue), lineNumber > 0 else { + NSSound.beep() + textField.currentEditor()?.selectAll(nil) + return + } + + handler(lineNumber) + window?.orderOut(self) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineWindow.swift b/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineWindow.swift new file mode 100644 index 00000000..70065eba --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/GotoLine/GotoLineWindow.swift @@ -0,0 +1,60 @@ +// +// GotoLineWindow.swift +// +// Created by cyan on 1/17/23. +// + +import AppKit + +public final class GotoLineWindow: NSWindow { + private enum Constants { + // Values are copied from Xcode + static let width: Double = 456 + static let height: Double = 48 + } + + public init( + relativeTo parentRect: CGRect, + placeholder: String, + iconName: String, + handler: @escaping (Int) -> Void + ) { + let rect = CGRect( + x: parentRect.minX + (parentRect.width - Constants.width) * 0.5, + y: parentRect.minY + parentRect.height - Constants.height - 100, + width: Constants.width, + height: Constants.height + ) + + super.init( + contentRect: rect, + styleMask: .borderless, + backing: .buffered, + defer: false + ) + + self.contentView = GotoLineView( + frame: rect, + placeholder: placeholder, + iconName: iconName, + handler: handler + ) + + self.isMovableByWindowBackground = true + self.isOpaque = false + self.hasShadow = true + self.backgroundColor = .clear + } + + override public var canBecomeKey: Bool { + true + } + + override public func resignKey() { + orderOut(self) + } + + override public func cancelOperation(_ sender: Any?) { + orderOut(self) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/LabelView.swift b/MarkEditMac/Modules/Sources/AppKitControls/LabelView.swift new file mode 100644 index 00000000..65af8f5b --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/LabelView.swift @@ -0,0 +1,21 @@ +// +// LabelView.swift +// +// Created by cyan on 12/19/22. +// + +import AppKit + +public final class LabelView: NSTextField { + init() { + super.init(frame: .zero) + backgroundColor = .clear + isBordered = false + isEditable = false + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/LabeledSearchField.swift b/MarkEditMac/Modules/Sources/AppKitControls/LabeledSearchField.swift new file mode 100644 index 00000000..9a298d54 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/LabeledSearchField.swift @@ -0,0 +1,54 @@ +// +// LabeledSearchField.swift +// +// Created by cyan on 12/19/22. +// + +import AppKit +import AppKitExtensions + +public final class LabeledSearchField: NSSearchField { + private let labelView = { + let label = LabelView() + label.font = .systemFont(ofSize: 10) + label.textColor = .secondaryLabelColor + return label + }() + + public init() { + super.init(frame: .zero) + addSubview(labelView) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func layout() { + super.layout() + + labelView.sizeToFit() + labelView.frame = CGRect( + x: (frame.width - labelView.frame.width) - 25, + y: (frame.height - labelView.frame.height) * 0.5, + width: labelView.frame.width, + height: labelView.frame.height + ) + + if let clipView { + clipView.frame = CGRect( + x: clipView.frame.minX, + y: clipView.frame.minY, + width: labelView.frame.minX - clipView.frame.minX, + height: clipView.frame.height + ) + } + } + + public func updateLabel(text: String) { + labelView.stringValue = text + labelView.isHidden = text.isEmpty + needsLayout = true + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitControls/RoundedButtonGroup.swift b/MarkEditMac/Modules/Sources/AppKitControls/RoundedButtonGroup.swift new file mode 100644 index 00000000..0a571783 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitControls/RoundedButtonGroup.swift @@ -0,0 +1,80 @@ +// +// RoundedButtonGroup.swift +// +// Created by cyan on 12/27/22. +// + +import AppKit + +/** + Rounded button group with two buttons and a divider in the middle. + */ +open class RoundedButtonGroup: NSView { + public var isEnabled: Bool = true { + didSet { + let alphaValue: Double = isEnabled ? 1.0 : 0.4 + leftButton.alphaValue = alphaValue + rightButton.alphaValue = alphaValue + } + } + + private let leftButton: NSButton + private let rightButton: NSButton + private let dividerView = DividerView(color: .plainButtonBorder, hairlineWidth: false) + + public init(leftButton: NSButton, rightButton: NSButton) { + self.leftButton = leftButton + self.rightButton = rightButton + super.init(frame: .zero) + + defer { + isEnabled = false + } + + wantsLayer = true + layer?.masksToBounds = true + layer?.borderWidth = 1 + layer?.cornerRadius = 5 + + addSubview(leftButton) + addSubview(rightButton) + addSubview(dividerView) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func layout() { + super.layout() + leftButton.frame = CGRect( + x: 0, + y: 0, + width: frame.width * 0.5, + height: frame.height + ) + + rightButton.frame = CGRect( + x: frame.width * 0.5, + y: 0, + width: frame.width * 0.5, + height: frame.height + ) + + dividerView.frame = CGRect( + x: (frame.width - dividerView.length) * 0.5, + y: 1.0, + width: dividerView.length, + height: frame.height - 2.0 + ) + } + + override public func updateLayer() { + layer?.borderColor = NSColor.plainButtonBorder.cgColor + } + + override public func hitTest(_ point: NSPoint) -> NSView? { + isEnabled ? super.hitTest(point) : nil + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSApplication+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSApplication+Extension.swift new file mode 100644 index 00000000..5f2705b3 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSApplication+Extension.swift @@ -0,0 +1,35 @@ +// +// NSApplication+Extension.swift +// +// Created by cyan on 12/13/22. +// + +import AppKit + +public extension NSApplication { + var isDarkMode: Bool { + effectiveAppearance.isDarkMode + } + + var shiftKeyIsPressed: Bool { + currentEvent?.modifierFlags.contains(.shift) == true + } + + var hasOpenPanels: Bool { + windows.contains { $0 is NSOpenPanel } + } + + func showOpenPanel() { + if let openPanel = windows.first(where: { $0 is NSOpenPanel }) { + openPanel.makeKeyAndOrderFront(self) + } else { + NSDocumentController.shared.openDocument(self) + } + } + + func closeOpenPanels() { + for openPanel in windows where openPanel is NSOpenPanel { + openPanel.close() + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDataDetector+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDataDetector+Extension.swift new file mode 100644 index 00000000..7bbe3155 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDataDetector+Extension.swift @@ -0,0 +1,15 @@ +// +// NSDataDetector+Extension.swift +// +// Created by cyan on 1/4/23. +// + +import Foundation + +public extension NSDataDetector { + static func extractURL(from string: String) -> String? { + let range = NSRange(location: 0, length: string.utf16.count) + let detector = try? Self(types: NSTextCheckingResult.CheckingType.link.rawValue) + return detector?.firstMatch(in: string, range: range)?.url?.absoluteString + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDocument+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDocument+Extension.swift new file mode 100644 index 00000000..3a094b94 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSDocument+Extension.swift @@ -0,0 +1,13 @@ +// +// NSDocument+Extension.swift +// +// Created by cyan on 1/21/23. +// + +import AppKit + +public extension NSDocument { + var folderURL: URL? { + fileURL?.deletingLastPathComponent() + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSPasteboard+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSPasteboard+Extension.swift new file mode 100644 index 00000000..ded5ea5a --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSPasteboard+Extension.swift @@ -0,0 +1,29 @@ +// +// NSPasteboard+Extension.swift +// +// Created by cyan on 1/4/23. +// + +import AppKit + +public extension NSPasteboard { + var string: String? { + string(forType: .string) + } + + var url: String? { + guard let string else { + return nil + } + + return NSDataDetector.extractURL(from: string) + } + + func overwrite(string: String?) { + clearContents() + + if let string { + setString(string, forType: .string) + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSWorkspace+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSWorkspace+Extension.swift new file mode 100644 index 00000000..934b8344 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSWorkspace+Extension.swift @@ -0,0 +1,29 @@ +// +// NSWorkspace+Extension.swift +// +// Created by cyan on 1/21/23. +// + +import AppKit + +public extension NSWorkspace { + func openTerminal() { + let identifiers = [ + "com.googlecode.iterm2", // iTerm2 + "com.eltima.cmd1", // Commander One + "com.csw.macwise", // MacWise + "net.kovidgoyal.kitty", // Kitty + "co.zeit.hyper", // Hyper + "co.byobu", // Byobu + "net.macterm.MacTerm", // MacTerm + "org.alacritty", // Alacritty + "com.emtec.zoc8", // Zoc + "org.tabby", // Tabby + "com.apple.Terminal", // Terminal + ] + + if let url = identifiers.compactMap({ urlForApplication(withBundleIdentifier: $0) }).first { + openApplication(at: url, configuration: Self.OpenConfiguration()) + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAnimationContext+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAnimationContext+Extension.swift new file mode 100644 index 00000000..6ebb0a69 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAnimationContext+Extension.swift @@ -0,0 +1,16 @@ +// +// NSAnimationContext+Extension.swift +// +// Created by cyan on 12/16/22. +// + +import AppKit + +public extension NSAnimationContext { + static func runAnimationGroup(duration: TimeInterval, changes: (NSAnimationContext) -> Void, completionHandler: (() -> Void)? = nil) { + runAnimationGroup({ context in + context.duration = duration + changes(context) + }, completionHandler: completionHandler) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAppearance+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAppearance+Extension.swift new file mode 100644 index 00000000..f245481c --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSAppearance+Extension.swift @@ -0,0 +1,37 @@ +// +// NSAppearance+Extension.swift +// +// Created by cyan on 1/24/23. +// + +import AppKit + +public extension NSAppearance { + var isDarkMode: Bool { + switch name { + case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: + return true + default: + return false + } + } + + func resolvedName(isDarkMode: Bool) -> NSAppearance.Name { + switch name { + case .aqua, .darkAqua: + // Aqua + return isDarkMode ? .darkAqua : .aqua + case .vibrantLight, .vibrantDark: + // Vibrant + return isDarkMode ? .vibrantDark : .vibrantLight + case .accessibilityHighContrastAqua, .accessibilityHighContrastDarkAqua: + // High contrast + return isDarkMode ? .accessibilityHighContrastDarkAqua : .accessibilityHighContrastAqua + case .accessibilityHighContrastVibrantLight, .accessibilityHighContrastVibrantDark: + // High contrast vibrant + return isDarkMode ? .accessibilityHighContrastVibrantDark : .accessibilityHighContrastVibrantLight + default: + return .aqua + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSColor+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSColor+Extension.swift new file mode 100644 index 00000000..1d516b05 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSColor+Extension.swift @@ -0,0 +1,50 @@ +// +// NSColor+Extension.swift +// +// Created by cyan on 12/17/22. +// + +import AppKit + +// MARK: - Semantic Colors + +public extension NSColor { + static var plainButtonBorder: NSColor { + .theme(light: NSColor(white: 0, alpha: 0.3), dark: NSColor(white: 1, alpha: 0.3)) + } + + static var plainButtonHighlighted: NSColor { + .theme(light: NSColor(white: 0, alpha: 0.1), dark: NSColor(white: 1, alpha: 0.1)) + } +} + +// MARK: - Convenience Methods + +public extension NSColor { + convenience init(hexCode: UInt32, alpha: Double = 1.0) { + let red = Double((hexCode & 0xFF0000) >> 16) / 255.0 + let green = Double((hexCode & 0x00FF00) >> 8) / 255.0 + let blue = Double(hexCode & 0x0000FF) / 255.0 + self.init(red: red, green: green, blue: blue, alpha: alpha) + } + + static func theme(light: NSColor, dark: NSColor) -> NSColor { + NSColor(name: nil) { $0.isDarkMode ? dark : light } + } + + static func theme(lightHexCode: UInt32, darkHexCode: UInt32, alpha: Double = 1.0) -> NSColor { + theme( + light: NSColor(hexCode: lightHexCode, alpha: alpha), + dark: NSColor(hexCode: darkHexCode, alpha: alpha) + ) + } + + func resolvedColor(with appearance: NSAppearance = NSApp.effectiveAppearance) -> NSColor { + var cgColor: CGColor? + appearance.performAsCurrentDrawingAppearance { + cgColor = self.cgColor + } + + return NSColor(cgColor: cgColor ?? self.cgColor) ?? self + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSControl+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSControl+Extension.swift new file mode 100644 index 00000000..6b9e02cf --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSControl+Extension.swift @@ -0,0 +1,43 @@ +// +// NSControl+Extension.swift +// +// Created by cyan on 1/3/23. +// + +import AppKit + +/** + Closure-based handlers to replace target-action. + */ +public protocol ClosureActionable: AnyObject { + var target: AnyObject? { get set } + var action: Selector? { get set } +} + +public extension ClosureActionable { + func addAction(_ action: @escaping () -> Void) { + let target = Handler(action) + objc_setAssociatedObject(self, UUID().uuidString, target, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) + + self.target = target + self.action = #selector(Handler.invoke) + } +} + +extension NSButton: ClosureActionable {} +extension NSMenuItem: ClosureActionable {} +extension NSToolbarItem: ClosureActionable {} + +// MARK: - Private + +private class Handler: NSObject { + private let action: () -> Void + + init(_ action: @escaping () -> Void) { + self.action = action + } + + @objc func invoke() { + action() + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSFont+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSFont+Extension.swift new file mode 100644 index 00000000..ad4aa908 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSFont+Extension.swift @@ -0,0 +1,33 @@ +// +// NSFont+Extension.swift +// +// Created by cyan on 1/29/23. +// + +import AppKit + +public extension NSFont { + static func monospacedSystemFont(ofSize fontSize: Double) -> NSFont { + monospacedSystemFont(ofSize: fontSize, weight: .regular) + } + + static func roundedSystemFont(ofSize fontSize: Double, weight: NSFont.Weight = .regular) -> NSFont { + .systemFont(ofSize: fontSize, weight: weight).withDesign(.rounded) + } + + static func serifSystemFont(ofSize fontSize: Double, weight: NSFont.Weight = .regular) -> NSFont { + .systemFont(ofSize: fontSize, weight: weight).withDesign(.serif) + } +} + +// MARK: - Private + +private extension NSFont { + func withDesign(_ design: NSFontDescriptor.SystemDesign) -> NSFont { + guard let descriptor = fontDescriptor.withDesign(design) else { + return self + } + + return NSFont(descriptor: descriptor, size: pointSize) ?? self + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSImage+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSImage+Extension.swift new file mode 100644 index 00000000..8a5c4da8 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSImage+Extension.swift @@ -0,0 +1,39 @@ +// +// NSImage+Extension.swift +// +// Created by cyan on 1/15/23. +// + +import AppKit + +public extension NSImage { + static func with( + symbolName: String, + pointSize: Double, + weight: NSFont.Weight = .regular, + accessibilityLabel: String? = nil + ) -> NSImage { + let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityLabel) + let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: weight) + + guard let image = image?.withSymbolConfiguration(config) else { + assertionFailure("Failed to create image with symbol \"\(symbolName)\"") + return NSImage() + } + + return image + } + + func resized(with size: CGSize) -> NSImage { + let frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) + guard let representation = bestRepresentation(for: frame, context: nil, hints: nil) else { + return self + } + + let image = NSImage(size: size, flipped: false) { _ in + representation.draw(in: frame) + } + + return image + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenu+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenu+Extension.swift new file mode 100644 index 00000000..75539bfb --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenu+Extension.swift @@ -0,0 +1,36 @@ +// +// NSMenu+Extension.swift +// +// Created by cyan on 12/26/22. +// + +import AppKit + +public extension NSMenu { + var superMenuItem: NSMenuItem? { + supermenu?.items.first { $0.submenu === self } + } + + var copiedMenu: NSMenu? { + copy() as? NSMenu + } + + @discardableResult + func addItem(withTitle string: String, action selector: Selector? = nil) -> NSMenuItem { + addItem(withTitle: string, action: selector, keyEquivalent: "") + } + + @discardableResult + func addItem(withTitle string: String, action: @escaping () -> Void) -> NSMenuItem { + let item = addItem(withTitle: string, action: nil) + item.addAction(action) + return item + } + + /// Force an update, the .update() method doesn't work reliably + func reloadItems() { + let item = NSMenuItem.separator() + addItem(item) + removeItem(item) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenuItem+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenuItem+Extension.swift new file mode 100644 index 00000000..f7abe5be --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSMenuItem+Extension.swift @@ -0,0 +1,27 @@ +// +// NSMenuItem+Extension.swift +// +// Created by cyan on 12/25/22. +// + +import AppKit + +public extension NSMenuItem { + var copiedItem: NSMenuItem? { + copy() as? NSMenuItem + } + + func setOn(_ on: Bool) { + state = on ? .on : .off + } + + func toggle() { + state.toggle() + } +} + +extension NSControl.StateValue { + mutating func toggle() { + self = self == .on ? .off : .on + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSSearchField+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSSearchField+Extension.swift new file mode 100644 index 00000000..38daf219 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSSearchField+Extension.swift @@ -0,0 +1,23 @@ +// +// NSSearchField+Extension.swift +// +// Created by cyan on 12/19/22. +// + +import AppKit + +public extension NSSearchField { + var clipView: NSView? { + // _NSKeyboardFocusClipView + subviews.first { $0.className.hasSuffix("FocusClipView") } + } + + func addToRecents(searchTerm: String) { + guard !searchTerm.isEmpty else { + return + } + + let recents = recentSearches.filter { $0 != searchTerm } + recentSearches = [searchTerm] + recents + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSTextField+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSTextField+Extension.swift new file mode 100644 index 00000000..c7247c55 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSTextField+Extension.swift @@ -0,0 +1,17 @@ +// +// NSTextField+Extension.swift +// +// Created by cyan on 12/28/22. +// + +import AppKit + +public extension NSTextField { + func startEditing(in window: NSWindow?) { + guard !isFirstResponder(in: window) else { + return + } + + window?.makeFirstResponder(self) + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSView+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSView+Extension.swift new file mode 100644 index 00000000..b8c40c76 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSView+Extension.swift @@ -0,0 +1,92 @@ +// +// NSView+Extension.swift +// +// Created by cyan on 12/16/22. +// + +import AppKit + +// MARK: - RTL + +public extension NSView { + /** + Mirror immediate subviews for RTL languages, we should generally rely on layout anchors, + but there are certain situations that need frame layout. + */ + func mirrorImmediateSubviewsIfNeeded(excludedViews: Set? = nil) { + guard NSApp.userInterfaceLayoutDirection == .rightToLeft else { + return + } + + for subview in subviews { + if excludedViews?.contains(subview) == true { + continue + } + + mirrorImmediateSubviewIfNeeded(subview) + } + } + + func mirrorImmediateSubviewIfNeeded(_ subview: NSView) { + guard NSApp.userInterfaceLayoutDirection == .rightToLeft else { + return + } + + guard subview.superview == self else { + fatalError("\(subview) is not a subview of \(self), cannot mirror its layout") + } + + subview.frame.origin.x = bounds.size.width - (subview.frame.origin.x + subview.frame.size.width) + } +} + +// MARK: - Helpers + +public extension NSView { + var layerBackgroundColor: NSColor? { + get { + guard wantsLayer, let cgColor = layer?.backgroundColor else { + return nil + } + + return NSColor(cgColor: cgColor) + } + set { + wantsLayer = true + layer?.backgroundColor = newValue?.resolvedColor(with: effectiveAppearance).cgColor + } + } + + /// Check if the view itself or its children is the first responder in a window + func isFirstResponder(in window: NSWindow?) -> Bool { + (window?.firstResponder as? NSView)?.belongs(to: self) ?? false + } + + /// Check if the view is a child of another view, or is another view + func belongs(to view: NSView) -> Bool { + var node: NSView? = self + while node != nil { + if node == view { + return true + } + node = node?.superview + } + + return false + } + + func update(_ animated: Bool = true) -> Self { + animated ? animator() : self + } + + /// Enumerate all children, recursively, self first + func enumerateChildren(where: ((T) -> Bool)? = nil, handler: (T) -> Void) { + if let view = self as? T, `where`?(view) ?? true { + handler(view) + } + + subviews.forEach { + $0.enumerateChildren(where: `where`, handler: handler) + } + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSViewController+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSViewController+Extension.swift new file mode 100644 index 00000000..1c3b16d0 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSViewController+Extension.swift @@ -0,0 +1,13 @@ +// +// NSViewController+Extension.swift +// +// Created by cyan on 1/8/23. +// + +import AppKit + +public extension NSViewController { + var popover: NSPopover? { + view.window?.value(forKey: "_popover") as? NSPopover + } +} diff --git a/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSWindow+Extension.swift b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSWindow+Extension.swift new file mode 100644 index 00000000..90e5b288 --- /dev/null +++ b/MarkEditMac/Modules/Sources/AppKitExtensions/UI/NSWindow+Extension.swift @@ -0,0 +1,59 @@ +// +// NSWindow+Extension.swift +// +// Created by cyan on 1/25/23. +// + +import AppKit + +public extension NSWindow { + var toolbarContainerView: NSView? { + toolbarEffectView?.superview + } + + var toolbarEffectView: NSVisualEffectView? { + var result: NSVisualEffectView? + rootView?.enumerateChildren { (view: NSVisualEffectView) in + // Blindly consider a full-width NSVisualEffectView the toolbar, + // a more accurate way would be relying on NSThemeFrame, which is private. + if abs(view.frame.width - frame.width) < .ulpOfOne { + result = view + } + } + + assert(result != nil, "Failed to find NSVisualEffectView in toolbar") + return result + } + + /// Change the frame size, treat the top-left corner as the anchor point + func setFrameSize(_ target: CGSize, display flag: Bool = false, animated: Bool = false) { + let size = frameRect(forContentRect: CGRect(origin: .zero, size: target)).size + let frame = CGRect(origin: frame.origin, size: size).offsetBy(dx: 0, dy: frame.height - size.height) + setFrame(frame, display: flag, animate: animated) + } + + /// Move to the center of the screen, the built-in .center() method doesn't work reliably + func moveToCenter() { + guard let visibleSize = screen?.visibleFrame.size, let fullSize = screen?.frame.size else { + return + } + + setFrameOrigin(CGPoint( + x: (visibleSize.width - frame.size.width) * 0.5, + y: (visibleSize.height - frame.size.height) * 0.5 + fullSize.height - visibleSize.height + )) + } +} + +// MARK: - Private + +private extension NSWindow { + var rootView: NSView? { + var node = contentView + while node?.superview != nil { + node = node?.superview + } + + return node + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/Extensions/NSNotification+Extension.swift b/MarkEditMac/Modules/Sources/FontPicker/Extensions/NSNotification+Extension.swift new file mode 100644 index 00000000..229ffe1b --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/Extensions/NSNotification+Extension.swift @@ -0,0 +1,17 @@ +// +// NSNotification+Extension.swift +// +// Created by cyan on 1/30/23. +// + +import Foundation + +public extension NSNotification.Name { + static let fontSizeChanged = Self("fontSizeChanged") +} + +extension NotificationCenter { + var fontSizePublisher: NotificationCenter.Publisher { + publisher(for: .fontSizeChanged) + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/FontPicker.swift b/MarkEditMac/Modules/Sources/FontPicker/FontPicker.swift new file mode 100644 index 00000000..ed23c36c --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/FontPicker.swift @@ -0,0 +1,107 @@ +// +// FontPicker.swift +// +// Created by cyan on 1/29/23. +// + +import AppKit +import SwiftUI + +public struct FontPicker: View { + public static let defaultFontSize: Double = 15 + public static let minimumFontSize: Double = 9 + public static let maximumFontSize: Double = 96 + + private let configuration: FontPickerConfiguration + private let handlers: FontPickerHandlers + + @State private var selectedFontStyle: FontStyle + @State private var selectedFontSize: Double + + public init(configuration: FontPickerConfiguration, handlers: FontPickerHandlers) { + self.configuration = configuration + self.handlers = handlers + self.selectedFontStyle = configuration.selectedFontStyle + self.selectedFontSize = configuration.selectedFontSize + } + + public var body: some View { + HStack { + Text(configuration.localizedInfo(style: selectedFontStyle, size: selectedFontSize)) + .font(Font(selectedFontStyle.fontWith(size: 12))) + .frame(width: 180, height: 19, alignment: .center) + .padding(.horizontal, 5) + .truncationMode(.middle) + .border(Color(.theme(light: .lightGray, dark: .darkGray))) + .background(Color(.theme(light: .controlBackgroundColor, dark: .windowBackgroundColor))) + + Stepper( + value: $selectedFontSize, + in: Self.minimumFontSize...Self.maximumFontSize, + label: {}, + onEditingChanged: { _ in + changeFontSize(selectedFontSize) + } + ) + + Menu { + Button(configuration.defaultFontName) { + changeFontStyle(.systemDefault) + } + Button(configuration.monoFontName) { + changeFontStyle(.systemMono) + } + Button(configuration.roundedFontName) { + changeFontStyle(.systemRounded) + } + Button(configuration.serifFontName) { + changeFontStyle(.systemSerif) + } + + Divider() + + Button(configuration.openPanelButtonTitle) { + FontManagerDelegate.shared.fontDidChange = { font in + changeFontStyle(.customFont(name: font.fontName)) + changeFontSize(font.pointSize) + } + + NSFontManager.shared.target = FontManagerDelegate.shared + NSFontPanel.shared.setPanelFont(selectedFont, isMultiple: false) + NSFontPanel.shared.orderBack(nil) + } + } label: { + Text(configuration.selectButtonTitle) + } + } + .padding(.vertical, 20) + .onReceive(NotificationCenter.default.fontSizePublisher) { + // Generally speaking, font size can also be changed by pressing ⌘ + ⌘ -, and ⌘ 0 + if let fontSize = $0.object as? Double { + selectedFontSize = fontSize + } + } + } +} + +// MARK: - Private + +private extension FontPicker { + var selectedFont: NSFont { + selectedFontStyle.fontWith(size: selectedFontSize) + } + + func changeFontStyle(_ fontStyle: FontStyle) { + selectedFontStyle = fontStyle + handlers.fontStyleDidChange(fontStyle) + } + + func changeFontSize(_ fontSize: Double) { + guard fontSize >= Self.minimumFontSize && fontSize <= Self.maximumFontSize else { + return NSSound.beep() + } + + selectedFontSize = fontSize + handlers.fontSizeDidChange(fontSize) + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/FontPickerConfiguration.swift b/MarkEditMac/Modules/Sources/FontPicker/FontPickerConfiguration.swift new file mode 100644 index 00000000..dcd106c9 --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/FontPickerConfiguration.swift @@ -0,0 +1,50 @@ +// +// FontPickerConfiguration.swift +// +// Created by cyan on 1/29/23. +// + +import AppKit + +public struct FontPickerConfiguration { + let selectedFontStyle: FontStyle + let selectedFontSize: Double + let selectButtonTitle: String + let openPanelButtonTitle: String + let defaultFontName: String + let monoFontName: String + let roundedFontName: String + let serifFontName: String + + public init(selectedFontStyle: FontStyle, selectedFontSize: Double, selectButtonTitle: String, openPanelButtonTitle: String, defaultFontName: String, monoFontName: String, roundedFontName: String, serifFontName: String) { + self.selectedFontStyle = selectedFontStyle + self.selectedFontSize = selectedFontSize + self.selectButtonTitle = selectButtonTitle + self.openPanelButtonTitle = openPanelButtonTitle + self.defaultFontName = defaultFontName + self.monoFontName = monoFontName + self.roundedFontName = roundedFontName + self.serifFontName = serifFontName + } +} + +extension FontPickerConfiguration { + func localizedInfo(style: FontStyle, size: Double) -> String { + let name = { + switch style { + case .systemDefault: + return defaultFontName + case .systemMono: + return monoFontName + case .systemRounded: + return roundedFontName + case .systemSerif: + return serifFontName + case let .customFont(name): + return name + } + }() + + return "\(name) - \(String(format: "%.1f", size))" + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/FontPickerHandlers.swift b/MarkEditMac/Modules/Sources/FontPicker/FontPickerHandlers.swift new file mode 100644 index 00000000..29e31ffb --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/FontPickerHandlers.swift @@ -0,0 +1,17 @@ +// +// FontPickerHandlers.swift +// +// Created by cyan on 1/30/23. +// + +import Foundation + +public struct FontPickerHandlers { + let fontStyleDidChange: (FontStyle) -> Void + let fontSizeDidChange: (Double) -> Void + + public init(fontStyleDidChange: @escaping (FontStyle) -> Void, fontSizeDidChange: @escaping (Double) -> Void) { + self.fontStyleDidChange = fontStyleDidChange + self.fontSizeDidChange = fontSizeDidChange + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/FontStyle.swift b/MarkEditMac/Modules/Sources/FontPicker/FontStyle.swift new file mode 100644 index 00000000..f57c7fdb --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/FontStyle.swift @@ -0,0 +1,49 @@ +// +// FontStyle.swift +// +// Created by cyan on 1/29/23. +// + +import AppKit +import AppKitExtensions + +/** + FontStyle is an abstraction of either system fonts or custom fonts. + */ +@frozen public enum FontStyle: Codable { + case systemDefault + case systemMono + case systemRounded + case systemSerif + case customFont(name: String) + + public var cssFontFamily: String { + switch self { + case .systemDefault: + return "system-ui" + case .systemMono: + return "ui-monospace" + case .systemRounded: + return "ui-rounded" + case .systemSerif: + return "ui-serif" + case let .customFont(name): + return name + } + } + + public func fontWith(size: Double, weight: NSFont.Weight = .regular) -> NSFont { + switch self { + case .systemDefault: + return .systemFont(ofSize: size, weight: weight) + case .systemMono: + return .monospacedSystemFont(ofSize: size, weight: weight) + case .systemRounded: + return .roundedSystemFont(ofSize: size, weight: weight) + case .systemSerif: + return .serifSystemFont(ofSize: size, weight: weight) + case let .customFont(name): + return NSFont(name: name, size: size) ?? .systemFont(ofSize: size, weight: weight) + } + } +} diff --git a/MarkEditMac/Modules/Sources/FontPicker/Internal/FontManagerDelegate.swift b/MarkEditMac/Modules/Sources/FontPicker/Internal/FontManagerDelegate.swift new file mode 100644 index 00000000..2b6147d5 --- /dev/null +++ b/MarkEditMac/Modules/Sources/FontPicker/Internal/FontManagerDelegate.swift @@ -0,0 +1,27 @@ +// +// FontManagerDelegate.swift +// +// Created by cyan on 1/30/23. +// + +import AppKit + +/** + Shared delegate to handle font changes sent by NSFontManager. + */ +final class FontManagerDelegate { + static let shared = FontManagerDelegate() + var fontDidChange: ((NSFont) -> Void)? + + @objc func changeFont(_ sender: NSFontManager?) { + guard let newFont = sender?.convert(.systemFont(ofSize: NSFont.systemFontSize)) else { + return + } + + fontDidChange?(newFont) + } + + // MARK: - Private + + private init() {} +} diff --git a/MarkEditMac/Modules/Sources/Previewer/Previewer.swift b/MarkEditMac/Modules/Sources/Previewer/Previewer.swift new file mode 100644 index 00000000..838de2a7 --- /dev/null +++ b/MarkEditMac/Modules/Sources/Previewer/Previewer.swift @@ -0,0 +1,131 @@ +// +// Previewer.swift +// +// Created by cyan on 1/7/23. +// + +import AppKit +import AppKitExtensions +import WebKit +import MarkEditKit + +/** + Previewer for diagrams (mermaid), math (katex) and table (gfm) etc. + */ +public final class Previewer: NSViewController { + private enum Constants { + static let popoverSize: Double = 390 + static let minimumHeight: Double = 160 + } + + private let code: String + private let type: PreviewType + + private lazy var webView = { + let controller = WKUserContentController() + controller.addUserScript(resizeObserver) + controller.add(MessageHandler(host: self), name: "bridge") + + let config: WKWebViewConfiguration = .newConfig() + config.userContentController = controller + + let webView = WKWebView(frame: .zero, configuration: config) + return webView + }() + + public init(code: String, type: PreviewType) { + self.code = code + self.type = type + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadView() { + // The initial size is minimum, it will be updated by resizeObserver + view = NSView(frame: CGRect(x: 0, y: 0, width: Constants.popoverSize, height: Constants.minimumHeight)) + } + + override public func viewDidLoad() { + super.viewDidLoad() + + struct Wrapper: Encodable { + let code: String + } + + let data = Wrapper(code: code).jsonEncoded + let html = indexHtml?.replacingOccurrences(of: "\"{{DATA}}\"", with: data) + + webView.loadHTMLString(html ?? "", baseURL: nil) + view.addSubview(webView) + } + + override public func viewDidLayout() { + super.viewDidLayout() + webView.frame = view.bounds + } + + private var indexHtml: String? { + guard let path = Bundle.module.url(forResource: type.rawValue, withExtension: "html") else { + fatalError("Missing \(type.rawValue).html to set up the editor") + } + + return try? Data(contentsOf: path).toString() + } + + /// Observe body size change and update content size accordingly + private var resizeObserver: WKUserScript { + let source = """ + const observer = new ResizeObserver(entries => { + requestAnimationFrame(() => { + const height = entries[0].target.clientHeight; + window.webkit.messageHandlers.bridge.postMessage({ height }); + if (height <= \(Constants.minimumHeight)) { + const style = document.head.appendChild(document.createElement("style")); + style.appendChild(document.createTextNode(` + html, body { + height: \(Constants.minimumHeight)px; + } + #container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0px !important; + }` + )); + } + }); + }); + observer.observe(document.body); + """ + return WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + } +} + +// MARK: - WKScriptMessageHandler + +extension Previewer { + // Break the retain cycle inside message handler, + // to keep it simple, we are not using a delegate here. + private class MessageHandler: NSObject, WKScriptMessageHandler { + private weak var host: Previewer? + + init(host: Previewer? = nil) { + self.host = host + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + host?.didReceive(message: message) + } + } + + private func didReceive(message: WKScriptMessage) { + if let body = message.body as? [String: Double], let height = body["height"], height > 0 { + popover?.contentSize = CGSize(width: view.frame.width, height: max(height, Constants.minimumHeight)) + } + } +} diff --git a/MarkEditMac/Modules/Sources/Previewer/Resources/katex.html b/MarkEditMac/Modules/Sources/Previewer/Resources/katex.html new file mode 100644 index 00000000..c66536f0 --- /dev/null +++ b/MarkEditMac/Modules/Sources/Previewer/Resources/katex.html @@ -0,0 +1,28 @@ + + + + + + katex + + + + +
+ + + diff --git a/MarkEditMac/Modules/Sources/Previewer/Resources/mermaid.html b/MarkEditMac/Modules/Sources/Previewer/Resources/mermaid.html new file mode 100644 index 00000000..c3c12e0d --- /dev/null +++ b/MarkEditMac/Modules/Sources/Previewer/Resources/mermaid.html @@ -0,0 +1,32 @@ + + + + + + mermaid + + + +
+ + + diff --git a/MarkEditMac/Modules/Sources/Previewer/Resources/table.html b/MarkEditMac/Modules/Sources/Previewer/Resources/table.html new file mode 100644 index 00000000..c38c009a --- /dev/null +++ b/MarkEditMac/Modules/Sources/Previewer/Resources/table.html @@ -0,0 +1,69 @@ + + + + + + table + + + +
+ + + + diff --git a/MarkEditMac/Modules/Sources/Proofing/Grammarly.swift b/MarkEditMac/Modules/Sources/Proofing/Grammarly.swift new file mode 100644 index 00000000..ed84b801 --- /dev/null +++ b/MarkEditMac/Modules/Sources/Proofing/Grammarly.swift @@ -0,0 +1,61 @@ +// +// Grammarly.swift +// +// Created by cyan on 1/5/23. +// + +import AppKit +import MarkEditKit + +/** + Grammarly client for proofing: https://developer.grammarly.com/. + */ +public final class Grammarly { + public static let shared = Grammarly() + + public var redirectHost: String { + "grammarly-auth" + } + + public func toggle(bridge: WebBridgeGrammarly) { + enabled.toggle() + update(bridge: bridge) + } + + public func update(bridge: WebBridgeGrammarly) { + if enabled { + bridge.connect(clientID: clientID, redirectURI: redirectURI) + } else { + bridge.disconnect() + } + } + + public func startOAuth(url: URL, bridge: WebBridgeGrammarly?) { + self.bridge = bridge + NSWorkspace.shared.open(url) + } + + public func completeOAuth(url: URL) { + bridge?.completeOAuth(url: url.absoluteString) + bridge = nil + } + + // MARK: - Private + + private var enabled: Bool = false + private weak var bridge: WebBridgeGrammarly? + + // It's OK to expose this key to an open-source project, + // we use a free plan and added OAuth support, users can log in their accounts. + // + // https://developer.grammarly.com/plans + private var clientID: String { + "client_PaEKWbhCVjvUdbgwtsVHbX" + } + + private var redirectURI: String { + "markedit://\(redirectHost)" + } + + private init() {} +} diff --git a/MarkEditMac/Modules/Sources/SettingsUI/Extensions/View+Extension.swift b/MarkEditMac/Modules/Sources/SettingsUI/Extensions/View+Extension.swift new file mode 100644 index 00000000..969a6477 --- /dev/null +++ b/MarkEditMac/Modules/Sources/SettingsUI/Extensions/View+Extension.swift @@ -0,0 +1,45 @@ +// +// View+Extension.swift +// +// Created by cyan on 1/26/23. +// + +import SwiftUI + +/** + View extension for form building. + */ +public extension View { + func formLabel(alignment: VerticalAlignment = .center, _ text: String) -> some View { + formLabel(alignment: alignment, Text(text)) + } + + func formLabel(alignment: VerticalAlignment = .center, _ content: V) -> some View { + HStack(alignment: alignment) { + content + self.frame(maxWidth: .infinity, alignment: .leading) + .alignmentGuide(.controlAlignment) { $0[.leading] } + } + .alignmentGuide(.leading) { $0[.controlAlignment] } + } + + func formMenuPicker() -> some View { + pickerStyle(.menu).frame(minWidth: 280) + } + + func formHorizontalRadio() -> some View { + pickerStyle(.radioGroup).horizontalRadioGroupLayout() + } +} + +// MARK: - Private + +private extension HorizontalAlignment { + enum ControlAlignment: AlignmentID { + static func defaultValue(in context: ViewDimensions) -> CGFloat { + return context[HorizontalAlignment.center] + } + } + + static let controlAlignment = HorizontalAlignment(ControlAlignment.self) +} diff --git a/MarkEditMac/Modules/Sources/SettingsUI/SettingsForm.swift b/MarkEditMac/Modules/Sources/SettingsUI/SettingsForm.swift new file mode 100644 index 00000000..347b84be --- /dev/null +++ b/MarkEditMac/Modules/Sources/SettingsUI/SettingsForm.swift @@ -0,0 +1,41 @@ +// +// SettingsForm.swift +// +// Created by cyan on 1/27/23. +// + +import SwiftUI + +/** + Lightweight form builder for SwiftUI. + */ +public struct SettingsForm: View { + // Generally speaking, we should avoid AnyView, + // but here we wanted to erase the type so badly. + public typealias TypedView = AnyView + + @resultBuilder + public enum Builder { + public static func buildBlock(_ sections: any View...) -> [TypedView] { + sections.map { TypedView($0) } + } + } + + private let builder: () -> [TypedView] + + public init(@Builder builder: @escaping () -> [TypedView]) { + self.builder = builder + } + + public var body: some View { + let sections = builder() + Form { + ForEach(0 ..< sections.count, id: \.self) { index in + sections[index] + VStack {}.padding(.bottom, index < sections.count - 1 ? 12 : 0) + } + } + .fixedSize() + .padding(20) + } +} diff --git a/MarkEditMac/Modules/Sources/SettingsUI/SettingsRootViewController.swift b/MarkEditMac/Modules/Sources/SettingsUI/SettingsRootViewController.swift new file mode 100644 index 00000000..ed7d2420 --- /dev/null +++ b/MarkEditMac/Modules/Sources/SettingsUI/SettingsRootViewController.swift @@ -0,0 +1,77 @@ +// +// SettingsRootViewController.swift +// +// Created by cyan on 1/26/23. +// + +import AppKit +import AppKitExtensions + +/** + Root container for settings view, multi-tab based. + */ +public final class SettingsRootViewController: NSTabViewController { + private var tabs: [SettingsTabViewController]? + private var animateChanges = false + + public static func withTabs(_ tabs: [SettingsTabViewController]) -> NSWindowController { + let contentVC = Self() + contentVC.tabs = tabs + + let window = NSPanel(contentViewController: contentVC) + window.styleMask = [.titled, .closable] + + return NSWindowController(window: window) + } + + override public func viewDidLoad() { + super.viewDidLoad() + tabStyle = .toolbar + + tabs?.forEach { + addTabViewItem($0.tabViewItem) + } + } + + override public func viewDidAppear() { + super.viewDidAppear() + view.window?.moveToCenter() + } +} + +// MARK: - NSTabViewDelegate + +extension SettingsRootViewController { + override public func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) { + super.tabView(tabView, didSelect: tabViewItem) + guard let contentVC = tabViewItem?.viewController as? SettingsTabViewController else { + return + } + + // Performing in the next run loop has a better visual effect + DispatchQueue.afterDelay(seconds: 0.02) { + self.view.window?.setFrameSize(CGSize( + width: 580, + height: contentVC.contentView.frame.size.height + ), animated: self.animateChanges) + + // Enable animations after initial selection + self.animateChanges = true + } + + // Mimic the effect of some 1st-party apps, such as Calendar.app, + // don't use isHidden, it affects the layout. + view.alphaValue = 0 + DispatchQueue.afterDelay(seconds: 0.2) { + self.view.alphaValue = 1 + } + } +} + +// MARK: - Private + +private extension DispatchQueue { + static func afterDelay(seconds: TimeInterval, execute: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: execute) + } +} diff --git a/MarkEditMac/Modules/Sources/SettingsUI/SettingsTabViewController.swift b/MarkEditMac/Modules/Sources/SettingsUI/SettingsTabViewController.swift new file mode 100644 index 00000000..ac44cd79 --- /dev/null +++ b/MarkEditMac/Modules/Sources/SettingsUI/SettingsTabViewController.swift @@ -0,0 +1,45 @@ +// +// SettingsTabViewController.swift +// +// Created by cyan on 1/28/23. +// + +import AppKit +import SwiftUI + +/** + Wrapper view controller for a settings tab in SettingsRootViewController. + */ +public final class SettingsTabViewController: NSViewController { + let tabViewItem: NSTabViewItem + let contentView: NSView + + public init(_ rootView: some View, title: String, icon: String) { + tabViewItem = NSTabViewItem() + tabViewItem.label = title + tabViewItem.image = NSImage(systemSymbolName: icon, accessibilityDescription: title) + contentView = NSHostingView(rootView: rootView) + super.init(nibName: nil, bundle: nil) + + self.title = title + self.tabViewItem.viewController = self + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadView() { + view = NSView(frame: .zero) + view.addSubview(contentView) + + // Rely on SwiftUI view size to have auto-sizing, + // the window height respects to the contentView height. + contentView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentView.topAnchor.constraint(equalTo: view.topAnchor), + ]) + } +} diff --git a/MarkEditMac/Modules/Tests/DataDetectorTests.swift b/MarkEditMac/Modules/Tests/DataDetectorTests.swift new file mode 100644 index 00000000..ab249bcd --- /dev/null +++ b/MarkEditMac/Modules/Tests/DataDetectorTests.swift @@ -0,0 +1,15 @@ +// +// DataDetectorTests.swift +// +// Created by cyan on 2/2/23. +// + +import AppKitExtensions +import XCTest + +final class DataDetectorTests: XCTestCase { + func testExtractURL() { + let url = NSDataDetector.extractURL(from: "Check it out https://markedit.app.") + XCTAssertEqual(url, "https://markedit.app") + } +} diff --git a/MarkEditMac/Modules/Tests/PasteboardTests.swift b/MarkEditMac/Modules/Tests/PasteboardTests.swift new file mode 100644 index 00000000..56953bf3 --- /dev/null +++ b/MarkEditMac/Modules/Tests/PasteboardTests.swift @@ -0,0 +1,15 @@ +// +// PasteboardTests.swift +// +// Created by cyan on 2/2/23. +// + +import AppKitExtensions +import XCTest + +final class PasteboardTests: XCTestCase { + func testOverwritePasteboard() { + NSPasteboard.general.overwrite(string: "Hello, World!") + XCTAssertEqual(NSPasteboard.general.string, "Hello, World!") + } +} diff --git a/MarkEditMac/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/MarkEditMac/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MarkEditMac/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..64dc11ee --- /dev/null +++ b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..9197b9faef759d791e6feafce2eacbd0ee89faf5 GIT binary patch literal 19581 zcmV)cK&ZcoP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91fS>~a1ONa40RR91fB*mh07#AmcK`rD07*naRCodHeFuPLMV0n>p>yt@ z?w-ud05c3>$ciu$R6tac0ulttiod&t731nEurA`Rii%-P|0?E0Sj7Y)8HX^yFf+^y z!{q7QJsn3I_-Jj}yx2jH^RHwqNTeqsN$Y0wn+xMMq7asjw z7IJGTJMJrxO^;vKfG#AEBSt_-ju1om4d(B;!?sGJpPDkqq&C28tX2^O%r4la{)D zmq;SsFGHDKfe_-ug*Le3zO4!A0y7{sCWv4Dm0dECj7oW>$U*Za0`0nYbyhCqYk z-*BJwbY*2UJ}Ac?y9{9BNIfsw7JmKEPL1rB9+3@Q+XxR`hCNU73>oV0kVx_cx%0sp zmZo(`8Jw{kwGGk|>uTh{#j^}S{X;HZE75eNM9PO{L0h?;aa_CCg2LwP)OIRcwY}mq zE2O%8mUM5~De0k&vhwHWsUV`6Qw_QX#?skLHWnLCL)B5FX~mb{uI`a!yh0+WZdv(* zH|^FJkcl#Asf#9K(O6ZYJ1W&pK7P)i|`&Wz=(&SE1{ zM*ib#_ky?%;UFFqDZ@D?%HY9=9{yqu7$E=VC!dk;|Lit^S)7ikV;sO_1POr#C!Kue zbvX?J3GwKJ#AC5Qnh~nxPy^PefC2nWq_S-e0n~&yl6#+|F)2MHaABCZa;Dx`0VwIT znwCuc8zq#VvFyG_AX`h`bjXVs(DTa%2D)#kuC6*e9xrPHoJNgDBHEe9QQBIFiXVxv zw@zo#nK*yq=1)B~@!*}Ywn~u3q7~AYSk;ZRl;XQfF$Jp8wT$q_mU!;>QeB5?#6r}9 zbX07gyo;J+S|kGHvN1x_skFT5s6Wc=0}haeswNrFbW1WdB6mFg>egtPmOBeqT`mVf zya}D53U}%7$eb}(8A3%YZWSf)M}~*x#ddHvdxk%ACd1rv=BN*&=-!- zLbRkhFB>8cKeldTWp(vJOKcu^NctL&wdf>1m5E4SZ@=7l{U8c?R#4EH9?7If<>Q~5 zEfp0QTsU|5f%7oHlI@J<6~Qrw&4q9>3tZy43YovyDzxdEOIIw7X64S`Zj{ge*My`i z+GQd=E@jd$sbs$_S==M9Jot!!Az1_#4CRJ_fSDDICnV400iQrg<)(l`&b(V1+UCo6 zwpLQv9@Nq?iKT~Nl&pN|3vU8C_~y`!iTZg8tXR{Vm)JTuCf#fL@?d_1H;W5y+#wCK z=Spv)47M9U*D)Z`)TkV_cv#+X@zL_+dMGl9%TwU!rOwcF9M6OFHo1KKy&N9uMO^fKsLc@0VB>pzqIUFb*tR)r9a4m zebAiAkhr1mXcgweiATlAOFC6LE_n(d-hoj&Zi|)!Kzv0IU2*S^j(UVmna-LdFc-`b7vhaz2=&HS>n@va$mr(I{pMBo>Kc-~k`?2_qt~Qx51L@L-O38biu7 z{LDmZV)(2h?wQp-Z$F6+w#xX#kbHl|BDR3wIm&5L)B@?$=pdRvUEZk2MnRlQpktfn zs7Rye<}%qb#LK7;hGlZ*;lHb^u0bOjm?4RYZi$!0*!f^SA>S4D13L2)jYQDw3XQX; zxG~PkRpMX5!M`zAs7705H9R#)iJHfUI1P5mA_1 zq^cm#Cq+q@mG@lpn6&SEs0?q-piXw0N z^Z{-bP4v=#S}&2>GD*cUl8OwWmW@aj1C{nR%&8bX`D!?)fo}oKMxxC2!hQe8>!hr4 zwhWKfV47_NlauI+CPrjzWUJiuC~Ln}l--&8DbZ|dI2@^{rdH#v6!LuLJ|g4eW3v46 zjSz~qHC`*3(LRaefh2|w@jaJS$@%9jSG<5VJb8saOBNhSZB%dC$8h^ zA52Kgl2^&l&K|+!hy3}U+hxw|!_W!AsEX$l&tu0-m_`B55=B5_B3rIqTxkQ?trrc+ zk_{h~yWcS`JHB$L%$+mawRfbeP>+rID!}6Ol{0k9N?SuhPCVui zIqcAba{~Q-BVgt-!ib~Sj9FsggNe0!O$R|}1rjr^Pc^b@U{E$}?2rpCyvV`x({pvk z!Id^gpA(<205JlgD##I2iBRhx{K6C8li&Yojnt#EDNP{J;SncLG|T|$wHPQGVbASe z2iVm7GBaeSL>lcsIX)uCzV=jfkVwfa_@oM|khtzy9mLZU$v;%5(47yeO)|I`g8Ky-W-XOjZZ~CmAUA z#)EIaAe)VSucoqDsw!sS<$pv*Cw9thPaWTajBuK&s4H?aE{gZ}2#lV%VGu3fw8ge8mjzbmokw-wa~ zoPth`-CvQ4WN`BfO=xiQrOVg}bQa;u$6?k2cO5>r3f0>lL^9GTPJO1<~U!79nJ=K6X zCd>MQ`KnSZ9NaQm3T?M^XE1t9<4KuLs@6f;a9}mmrZ@o`q9SE7i+3X2Cr7=eOwPM(88aX9c*|L5toY7%{_`jRLhh7cuA~8Ozy0T@p-}MR z$dn-o!g+CmFzgLse!8Oz4ME((bPr}}=0g@U|L%9*EqC0Oz;d?PQa^XD4DYCvbR07x z(Lu>pqFF?GC6Vrxs+Od@`y-2`s*>XrlN0d2^x}*Dj1b0dySq|NAadNXuliVs&b;vP zT&pUAjMJX{X&MC}e^OdD|8pq7CI`AZ$Ksk6S1Ctc_6Xe7wWdNzIsb1s=O($bi z5VxFDo);@rHAE+ zBNpPWU|BJ_Ii5}rLkKg^K*<^a?4ZrC>IN2cgnZOhi^8L@TvQQui#vQrqJ4()BO;vh z$Z`xz;a+nQ*1lG^N(E-T26mvdv6gc@gGr{yFgkn~0SnEzL}fg&L07}vbi-WC4R}@G z_yyd-zfgZm2>g-;DAI<)eCM<#6G$|nVIKiNm|+ixF#ZZ^2B&$pZ|{{e|6!d}R?n2` zwmRwBI)d3Y6hj)F9rBPxdoPQ}F$*__4iEVK8XPwc3#z@ zEX|dZX_+LhTE-C(&p=Q@jo^|Td}wG;mc4VO#A@;67O9kGWPaPWUWuXPv#B8z;5c5h z41;S_;#GL_fw!^AF}(R2-!9+#;#|D9FXqNFq~wSw z-0@gtdw|t1F{#9hGt}bdk5Cum0{&w0;P&x4xL|S0_`6! zJVp~E+Tm4|4a;x7`5M`0{=A^wn=JLMv>9>o6z+!=kawBH0IsO%?bf*=7lfPj%@}?> zlYfyPK`aPj3?Mioe7kex%IvBGpG z#*3#?6LQ0yRT#iyrU`jyNG@#St}0SNXFFdB>ie2yE%K)| z7}Oxc!|4h%gN%Im_c#{{y8ek#`6T~r@;>*&K2$-dDTjKp#omo5&*OTMq zbFa-)fBF_~aU=5EKdzSRKYI(t@a-u7s3a#?bI1TrGZ_R+UmdO@SOyI?B?d}a$^hV$ zPL#*-JYc-@1YspMZb9X`J_raZ&mn5q6v?U2sjSBy{JGRsrsbXoUXTNiJVo|fG!G?@ z9zS%BhsME!YrAL2XT?E?ryxspc3{&LKyaWq2#p6 z5k_2=B%Q@EO$FGFZChl1+qkURuv7m0ObB8(2CbnMln}2l9C&cVyi(fq{@A}A> zF>4!_u3@fUqw;@QhqWqft3_8*mok8KbaZTrr@QZ6Fn`{O#nekAVlrzEW?yJstA-jT zHtJnqb?x*XdhEN>h#~v;f4)jCx#}O~*dyj+pNWYspeHS>t)o}j8rM*8$&1r~1ke?R z11Q$Z9*7X%lkMh8HUVHJ7Vdw<>3{g+eP0e3lR1BM&S&*i*)GlW(DYzR>ydkYEUhh- za{r?nvCy$zPCW7W+$Nil59lq!dDY(Jm`sa1c}DN#scL$0cnBl&2f*8apS4ZJu*+ly zAkOf~uYb#XBNc56*FLwN)8SCGfRDlmFZe;JuC8tw7>moJE4Rzj>$~LRpSn(}t2k#z zM>STVgzna1|SyO&!(lzjswH? zI{Xeg`>mH<+|@JiT)@I*Bu}v{ZjnjZxP3@YJnd4s^Zsu6BwlK_v^0Svum0w0Z@|o@ zhmk3zLxar2k3RNGWOzA#bMPx)w{G1Zz=iETu!zUsdpoEpPk7vv2(!;3e+_HXPdvd4D$~kp&ADRP0~%>Bp+eW**cwbGD53 zm&rILSh8c|S3yhO4&7tx?mGWZuL1EB_W$a48!Gp^AQGvN=H@m@jZ`7G<9K5iC%s6=@r*s|FuhNYHE(hd&I@DShO8eXjKu+>SM4+9L2+vnEDn+Ys3IXALaon za1p%e#3KYK*=94CmsFo&{20cQb!L2cWO(rGSNwY4+KM??6I3O&O-<YXMiF>1L*6 zWh$0_2Kag|4g8XBItE}y%0`CzZ^d~2+~H^Ml-ib-+)y>_W)RRLTYyd&A8zP090c{b zfp*D*AuXc2tgbnWo6G0X_D@5^bqqLjM0A*KS`SvwMNsDFL zM=X|ZnPwV)jE67~YhH0dlKvQ1l&swGi*p)ik3W2#;|OneSIPj9Pk!=~`|XZC2_&!7G&e4TE+-@n zGYuFsO6O2rZgV)A9-JQV`5T|b{k=?Lb>-M8gw-NA^&p8?=V@fTtO9*Os#iv{eRBHw zv$1F*c$sC)k2vhm?;>2X7L+nT^dJA>o&OmyDY#mxDLB-=c!n&K#AVoJN=fzN86pP^ z@F0gB-@je=XS^tGgaP8xI13q!w`w!s8iI)|wn}D)VT5Lxw1q0N}=@uM7 zQ}R5XQU-`OH8vg>R#7Tv8Mo@IFuOpQlNVIqP&g3+w;R2WJ6ctojj*_OM7YhSege?1&7GZ<0&jQ!76vghZ?2*o{Aqa5XZXLJh!JKRYO+ zseT+Of~HXx6>Pe9YX&@BwJ*wIJ;B^lowg=1^h+6_JkOFuS0#C|AA=S)pNO6#IL{d# zeX|}B44y;YwQERrcH+%3)&tbforA>^P3ZCQK9ajcadZuiKf$t@VLbH>%GnpjbjzYs zGVr{b(A3a)c#d;^sMJt6zH(h}fLW-k)(VjsLrK}N&h10+^ODbhuVNhWec~feO1z>$ z%9|UcXH&0~$2jbs(3N9ptPPyN0%@)&nZUVCt&5su)kdx_aLN@(O-&@j1KJ9bN^4>;HdURl?G zW8Qd#1RmD@MhHlM)~IV_sO zCV6a=$B}G|kD(R}%d*32rJB3=j6YTLGEd1^M`-J67|nB~4WMYnGFS-9<5v*za5bM6 z*GQXApp@NQsZP+TMq&#h z%&O&AFJ?xQaj`Jt`qI>3ir~@_Hu}(~PH1sx```J=T8!DMC4+I>*a+4Hm+{pe7GvO8 z2CkaL0z%XRtR_LvpG;vvB}Rnx^<2ZDLKQbqs4&xoafb@8lmX<9`?uw=ii8fpO3t+p zY%h|scc4)m?Dwso^-E>dENp(j>alIO)!}G>REoRtQ#27qh9h_;K=qGRN^)eU9Js7T z{_sRUmd@$1k9#H970+(8``}!QP1=NfjL?v{3#|UN)E@YTphQXTSMA-{6#~e}N)5*W z;kNh2cRnq7ClW3PTvvRE~m#R1`!4Gl6hie>RAZT9}W&84$Rw0SP2!G&?@@2RtE-?H?=4FdL1NO7UD!Q6|ggZI^xbTP$mER8BOCqjO%QD`|kEmvT9a z?`Td}>ag_i>N1&s0JgDNCq(P#g@Nvo_+cK!^Y%|ahg&|@1ee$0HkQOmID^{Wx$ig2 zYR`uqbb8UUDwH}7DVyk!{?1N$YB(b||MMJaKxY{=Gw7tHT_E3AeUk7L`vA)ceF}0M z)%Rg8%w#o)!=q`K;vP+`lg&(L_A4kjlQ4uECY=p7B; zcKzp_>vRal!->n{$Sgk1kirl*l4hI7_`j-N_YNe7aOf!a5MVoe4AbE^y=OncIp_K` z7>G1QOab#vG4l<+yI;Bn5Z5wcbSBR-<)>Tb3Cl}2;=t#i=kvF3#Qhw5MX*k|s-X$H zMeq(2ao+y2=u1t#FP3J)%~xC|LZIS0#4sJFI*capILq4_-;yS=9|+&Zs6@|?*IL{wBq==T zu*XN+=boYYGsfg|A3Z=0Sh85R#%ec1enG$)+smNJvUJD~b#l5CcqI*>r)wFqF`@5V z3`^xl`Z-QF=Ve;)J*5l6w51G?ZEwcLQOiSKh60YmdZJBBZj%@;Y?l^-5ES*gCc)^dJWmaVntO!9Y3_D!IDexyTfMw3hgb|w@Q_|q( zD;#v@E1UVymsaP)bFV1gZ9m`S5EpLm7( zTUx$fY8;R=n+hoqY&KCDmPQg6){;lqPe+tbf90vIGKifb&u#3HB?m0V^7*PWB1TzPpjb)G!bl ziF#yZWHzJ-0#5g$X~gjO=24nXKD~pDkBxJ3BTVt>N*N&6 zxI;1o$WYrUftCQ@SvtqqO9PM(K)X6tN?+fgY~0c-Z@=PdZzYH0Wh9hgnKYk_E$qt= zD|9XW4wIB@NKux$fJq zJnHawsa3T@%cmb6jLFQxjg$GjHO9HW!Frs*kMlx){kzBI-#&MX9IybZKJ@8_YO!=# zQjSF~^4vxB>$K zOmpdc>CxNWi5|aEc6JTwA!jpMnsYBRlr0!2J|f1;_!s)Bi<56*N;N#yz|#ep-`Uai z1i-sAsmP_20m!9m^X6?2gfu3Ef~~}Sm}bUh!es;>h?8|s+$NnJz4GL$9dg`Br|5|g z`5mbeIx#Nn=tJ2WKw5|j1eHyI0nI0}GEl173zl7iK*>$cr3^sgkp+tnzbq#J5DGQH zltJgq)pQ3#I=>J!qp4B`helBnI3ePUvoHsUR!2OeS0?Uayv$eTVZeD_7#7SQrPi6pWG1k@9^-SB` zgH8w)3-7~BHifASe$-2K!(=w*{vW*mSF)pfOjfVoDsQ>qZNY)m0RoBP99MN5u;3y?PB zS8}sdz+`F$pmgrNe-@0-gdvW*?}3MZ3JlCN zgp(aaXHA*RO(VU#r}z0wuX^v5*ff3cH^2S8pMWiIdwa}0#qUWBaePO7jvct{j3+em z$BBB(peme2Oq}So*IxU%4I4Hj&piF~LoU7i@=F2h#P6kWtkV4PI~wfWxi5$>33;ny ze9|h9^q16WX2Fpe_jiPs-*nSWKUlePI450y8i#l)5?fGyHz@dd4J)(rR0qmX?t|We2<7PBm7k? zV1*oe%>my&DjumnD;}>tBbqH+($q8q=L=&KK`Ne3jCF0sBy(pblgixr^r_qpwb$d{ z7ds66t61fKu@DwGe{p;)<&4T$!>6K=ssp1DY=gsYkora(pj=*ywZm91IL`CMu~#rX zjB}>P7e%wxi?;0g<&~i2L)czVFTY#?V7$+{LYGWdVTU~4r*Xd&p6iE1*q1+-BWsPXC9yZW zqj&hJ-&V$&r7Q9H&sV%~-GktT#bun6(s2O1>ASyl1t5R^Vz=FP+sxySKjz}>mWN(j z7MXZu-N9$_MEsoN?4V7x>XfKUGI$QNc*yX~H>|?A!3%xEGvCq+;|4GE4bOZ_M;u!= z^<>4L{@2s;gD-PlAcZBdHCP6Ql@T}srnapf_jerpf$x6DqjjhdSU`yRbiPtbW6eCj z;LR^6v7z^muK&V)>)&}JB7E2Vq6#1MrMHVQaW9<~_#JoLG4G@kkNs?IO)aNpLzlqP zaC{&=jE4cg&<$8(At1+ z24M#X4q7e8%2EATPO=|eK30#T0&r=33L7Re*f=qmeEtV0{4XN=SXijDy?g?AQSPN0 z1F+DeC!Tm>)h~bkqc36_{4(votWb&-qz|*O`OH$OFw^LYr5A%0;wtf9o}QaQMn)3y zv1=ZZwJS$)A8*3iNZbL?rAP2yq<&trOblRKU`Hhmf~iM=V{%(JiLfK&*A~jqJH+rJ znIr$C?0fW9IrW15XME&iuO7Sq{`)x-{-O(?rm*KqhWFORjyvwS+To%8-~aLM-wbKt zD<{?oIKNOVPM?*7X%_bV0I1i$9ep4^knh6usI%tDg;@@k7iI~qp35NDuG%SQF8`T4 zwJmDyc!VU_YN1yPXpTdW{Q|VnAUU+a) z-u90(KuUq*{c9 zikKS8E9lA8wEFzlqBl9Flf}&^oMA0ojS#>9VECE73J5NGeB1cA{N%f9<(i8gK%uta zEocok_~MK!d>E{L-&VB#3YpkhE*0h6?1wHNGBO<5vG)NTL1-C_x0@jep6G^8KI$Q zhp;ukN`9DWbc7W;Gc@5ojUP6eVlbsw;sScW!Za&9S0RlLOEN4>1PpG4Z1lt-%;4%f zAnw&%0mdzG*S`CA=%GrE`?oSWQj0CkxHY4&vK29zv3LP+%4Mvl90l8e-N2|6IQ)vIe{mql!p`6@oH3mJ zmPmJD2^Bu31)PfV7P1RHX_?;jeXMvIaJ$J8{_r2#S zZQW%Rn8w96*Xp)rX_!4n`Zn*vq)uzFD-@4@nJ8L5@9{C7{sgo0+)o^RHIm$piJnf} z3UDqMw%xq?w6whXHFLxLg275MUw){l>Jr3|p3TNT`H7EQjyT^9OxaCm5J;0%%xrHz zPD_?L6*DLcNA=ZRbE&reY{9262+n_p*@x_c@Ra+yba4Tikc5DMK$5{u+6HWv` zPk|jeJOm;#j=>)HL&wleVS04!JQpF2ebQ;n0W3aHhC|XIT$$j6M@o7-_zq(# zPIK%b`tf36gZVzmeO(+oLn)SK58IdK3{kZe`Sb{fFd!;!0ArBf#Qm>ZH^}X` zCvc1lw{+u9h|^1JvG09s00(;x<54Y+uha7nqdCs#@l&xp_fpGEm$G6S96bh3#0sW+ zi3=OP7G3}02Q846=4L!apbP_opnx?DJ*cYb=cl{8q|!K1Xqsby=~e)o<%@?@I#Y(0 z2`t4x4z37@oAR0%B7`vwSZ0imX40YfVNLf)wSaI@--iR>S4;~N#q{=Jp$tc7afh41 zb3P0IYv0-+zqq|as%z(h5em2>BGpaJGSrFFo+cVm_&C=K&-M6*9>n9396dk;A4HGo zC?4YgbpL$z$GL}i=AY055wm#BTVI!!lTV$GZL>HafscD8pZQ`~9ef5u{SnqA2S+p( zL+uM51r-b7J-X>s05Zi9-MHD&sc>J80*b&5*>D|Rz#w2mkR@|+gkd^01S019Fou=J z_z|Bl0JU%t(Kz7<6vCM3(-W62Pd*C@2esjw$Txg$t^Dc1UVP!YCAha&$D|sEw)SuB zMpeL6F9v`-1ct`}aAY5_=bJzr_lSW&7Pmbd{0Sd3e)x+;s&Hmf4ZfM4loL;i>4?9) zygZofp{`-U2WkbnBCK$xQ&)=<2V5iia*xm{yXjN_GL7JwlrOJ60jh-;XAtS8V3X}<50Lgs~OOjj6v{7g0Vo$NwbXl$3DbO?hFXZLuL_j~{4Sy{V2 zh4Wp|!{Xh2v?eaqIO?c-a|a3%%fB(Dn?gZH@Wa7R$P(`180crxSj2&mKR3#8t{?Xe z#Bn$l;2F#ZM6v3puCZ2Le_Fdv_UJiqp>4ekggP>VbS^C~|A3+H1?f}2PP+m?Lcwz; zm4Wk>p~9w$!KrizQv$>dVD$9y(^kkd$J=PVXs)F?pvj?cN2F*HFDA#rhGTG`gt8dK z+8Yesb`1>5l^P15l~CuZ+Z_`IJ3pU3-Z_mAayS)6zp zM?cRmRC64V#xYQMMWmI0_j?}w%H!c!Db6~x1)GR)CZ3!BrzUDk)mby(;+o=JTwa*p z7vZK`0nnxS0-X{xo`EgheA*Z-DJTOT0vezOd>oO{G9wmNdPG=Bl-?BHOtWIX;hWFQPR~ud0_e;U3q=i0fegcAg8D9Qt(z!x&ZozukdG#U z(0GQ&VLGCak1_IajhRCYUh}}KULgHf|9v1PWlfbKgO!r~{7{M#w z2o{4RC$NbPy;GFqXbhz;yLcW({Y`iWfT?2MGkm=@>@w!VgG@MFOLzGq-Zb5`D*zMjVd<8gqCXvohV3^L+KMbP-N2?zl z9hGZ8^SC^+8lREClx`F!c&AYzEAS1M^73lgz8T#xxmPaw@CtmH0Q=a{8sk{N!AYB{1}xEL;csH$V>>KX>9DJ2tLKuu zEGmw6TA0h{;lBLL16~elfoH*o;{cvFl1W1jdW95DKyPUnm3Llzpx*n-@XW6>er6w4 z+3D)3uVWs|li5V$g?SC~HP!3%D}V_%pN4n(hWemo1cd}IFKlXoK+BM{1%(;%bs3{! z)R}Mi=97oVmq9b8hI!9-ybR4%7B1Tw$79%6ezab`aYqkk_U56$;wXG{*(l1Y#&UG` zRno!HK6->S^1`)wA%C#tEMN+QJdWV?o)0=6i^=x_+2km?eVqP|O2HmxA~}p(!>D}l zUCZ={yefQaTn|h!8!;c6EdEPhN$LB!NYVbP?u@K%{b9r#>nk>+JC+Z)hkX=l%0a+0=o;!v>5D z=218;%aiMCQTS~6J2!0&4sX^0A7tZ^j>mj*AqNXJi*BARn4^2%7)dKtcF;UM|2Sqxh6Kv$)$|JsQ%wY=%_o+H&Cj!Qz-Fq~=~sY2 zdZ2|FmcA-Mu{5%3#t58R-1!Rd!b~gX$OAvta-UvVd9V0PUi5N4qUt$!96{= z_XEaW0R=9YW8`MX~0;th`<3w&9C_CymLHMJ=-5BEIIW$0H=az6!@YI^7rz z9P?oLK+v-6!-yE>O(VQa+fBOyZ13iv#$2eG0dh)(5SI}bFT){Q5p=z~y1AXr9B>rP z8G~#s*9KPvhaa+Z@)@Tyb(B5;navpEEFDMrPp|8i_kDdE>S;AvK4-Sk`gIC6jqO;vPY+iP0z3bSO%a{Ud8a>VI~a+zHb{wr{Shu0p_%F z(Md7qO2uj=l~Rx?ALKUCk3rN#Oj?zXl^pmv@;skn^2v({cKHVbGn)BI4+hYQ?D-CV z#k0Ng{%`L@tB>QY9SWZdF;QS_-SvFbt8I{-FT8+n`Je@ZhOcs&#zN+(KZCh@?Ez3> zB3QQ|IF?@vGo@I#2#hwFrkj2RP#L&bCe$+2u>eLz3`YZ|nWv-`qTyM~wMt;| zkkrvyM$I>R;)QxRBE-2|dw6(AuKvPGbl`Y@k9Bwv99NvpU{(%jwbl5J3#NB_JGRJZ zra}uEO;;I2dAJqk?Ag1d`8E${yYp0k^!@tdK#ya+#2&)jz0;#%}0pK(16oeDZVO-(h3D(+pVJ5hOo3-gSy!8Czs7NBo*K5S~? z=~sX#Zkid)ar%{jiqfgVuE3dJf-NmfX8=yqJvuW`2xkCJ=`BovBEW;M{1u58H~@UT z0I@AvKYE};KTE-TdJF@$6eeYAFxV@vYsPBc0T~!Zi$=xBaC((g%5EY z=;<>)mV|MZudzBM=bTU`Cm%LnmvJ>?TDPvQPVe*VIf4luRV>hrK~v3%YGg z&vNpX1zb^EC*}3+c&ty#&b|Q@EP4jKs%0|Y-0`WNU$RDRL1n;mJ_d8_8J4z=%j*uv z$eh+De8{N@FLE$Jgin0*PQ<0Z8-?D3$3(pMFYQ=Lz5%V&v}C=sV(`ger6o?5GTjOw zuR6GiU-@&dgrQJDi&J5RFfX21heE(<`THD*uVB%6X#rreD-BR+Jl}Ahf!Hd*o8AA; zougQciP5~Sm&~Huk4sJCY}^W>vU$rkJnmsHK1cCnMSU>3=WZRo;AyPP%DIP*%4r8z z3QzlOYHrkb_x!F4cj|;Ic!K5{C~tWx^qCr}i-6NHua{IDmlkl#(O6t@a4~aOwiVzb z?KwQ{3V>FS>j5-_MT8V)jPX(~Omn$0+7N%YTq4Ez(VEUF9m0x^3w2`z|2(C1ZcDYi z(1it25)5_S&vWDS74tWSk#n_%h~()%9)48r4=o_sd*vR=hfi# zZ-veg@L2(C;lLQbu;38&!=X+ATF|P&+;1eNK7G1SeG9k*#q(gA7vL)|2hYP%ZhLi^ zb_Iae81~Xp5kDsgt3gz=qRC2Tm(X~mbF4xb04|@3Zs`PCIFtnp3;Vu;F^B_fd~`^T zo3%;mYhyUNyI0y*KO@a!TV&3n6XZ>&o*-@bDs5xaLHZ?|GPf=-cw{rZR10#n0D?4w zmwzi9^9@r;a0x4d%K&wy3*pXgh4gtcjRtf^uS^_IA6MafzD&0Q=v;?38}e3zGhoUq zke8ebah&p&q9lOPcjSRjnDK)jW|S~Gq#G;aZAMcvM~^hVw;#sWdbh5Q-U-*hqgs$Nb$qd8^0aLBWO zm!hZ;N2iJq3db;=M?+Kv`*?~p<;!#{fZX-x9(?MtU|Hal#fdD$#7P`a0qY;>nFiJL zQFX6A4(-o&{Z5Wv9Fwk&9vQ&l{rq;#^PBo)`8ij~6Puz^jdvSS?9Kn*;LvMX?U{0R zrdZu>#I66Q7Ar1ltCrcB-usMU79Qu?Uoqh#CNlwq!_Y>rR!1`Z~O|v-)2k-Vbt<( zJe8gi;ssuiPlk=2>2R7y=LdisjPX4C%;v& zA(vfwjn3L}6NZ+P%Jz7ge9NQ8T}e?zRoBx$r1t|zH{FbezQGLDkDEN-SFs?p{D-`h znd4{n9NsK+`OTHDWm+ILKfDHIinF~nbLH-fW|UrAoa?d`4lOdo8* z2!#YJj1@CyNg;nE=E1zU+QZRkUKu7biIoB3&Np28`8Ckv&$JwA$OE9zk)7(3<2yc% zuaj~jvnWAGnHQLD1xR&vc0Z*YR6z(YTDVlf6w8yBAIZZhz&AtYTY`-7)113-K*z^D zeyM5AEZkI8g?*vfsH&G!blX-_+cc4a4)Rn0gtZk?x&H0ml;!cb`zW`Me-NF ze(;Y{Gw+J25)|S!4q=`huTnQ{D)K=m=7X7}D-{k>^U{NrK1nR$dO{|;HektB93O4# zlb*hWJhNuI^be)wg7Ytwj-d+Kg%v?jybcZu*9p^tM%edFjv5^iYtgEg;W{MK)%SO! zFM>-@$RFWi^o}gV^Jxmgci*$}UVt#2IdwN_bAA z4rcoKFhf;iI6T74IOa3s42Q{1Lo|k4O(j{HxY> z$odV~$bltZ|MbC+VBpt=J$-z8hx#7!cLFRP$^zWi$KZv&BQFH2elD*;niZmtX)xcn zbSK~V8;T`|KJgHN_rssu_ThiJ_7is@whKQjY?!*6ZVy0_Z2A4~e$NN*xbU)T&cGRY zZh0?~)Nt@=RBFe#2H`Vch9!*YVH)FfK8=eTj`~Ci=z{xxN_KADC=>Wba_fv5>FwPm zTXzh|bL%@W>o+EsU-howgw1LcK3)oi{FQYv`8obXQT#mLgMmD740u0pLHK!y}R zK|MeG;m;o`tC@A|+I7$WJ`}6saWOUMTOk@SOhbTa3J-w^LK)K=;0*(I+({mrs);eue_JqzEbCwn^OtpSY&G9g37@M48$ zzLRgdsjnGu^v270SlrPYOAlLR7z^{s*F-6Ry7HJ4-f&7sXZI>i3KeC5KRWYW*a}wD3ND3dT6j4eyYJum zlh0xA{(#)~$QpTS%~t8%mBiAnaapk6Qq1?U9J0X$U81=QokrwNgA zN*KKd(V(IFt=NRPa?FwJ2FU4Cg1)D(f8(3pa`Bl97aw&2j;37;UVQeScFQ*%3Wmf6 zp0dwL2i^j15YAqaj=|!$=k5LFC|Z{d`!M^%Z*t5n~-bXdoh+_P)kM__kN(Br7>eCt}+_d#96 zad*xbj%s`4iPdt!MVI1siSu@_N@p~I);|)J>;L6?nLlrizT4N0mmv?oz)FNwyh}4x z{ucCjpS5-C_Gf-|*WI^Y^AFel7Ao-7Dev(-pU=8_YWSiZbNiQY_gOP70n2 zhvPJ*X5qZqP1pRNE0!I&q(b z{9zpu3!euz{r=uR%Tcdfu7zKVleI=hM&!J6&y`!g5EOo0U2X0~Pgsye3brr}%I)R~ zal$w-!?5G+69=zmv3r~+4dJH6$;pWcY(`Fw!5MY$`@1^3dpg#ye__ivzVp4O zSFKvhf@VWvLG#BjCw8ohr^J`kQS-lsqa6FO7vSS3D~J6UC!9aS^L4W@@fvTq;f9;v z{N^_=$M>n%AAb1Z`(JRu1=-D;H-B~O)~(+`dhk&m1pW@3UtkQ|kGD?#*mH6$Wk1FV zH$0yn;FsRL6t{q>Xk{59E;Gp*y*G~f?!w^b(N(KfE$->*nSaeY-f{Exojbn^IPZ-t zeCr|+eA)ePvjBUD+LKQ{`EARWFaH9<2jbU;A8#{Ed)ZCX0@k%olk9JXPJLO>T*kw& sb)oDwUv@8B;AIQEY=M_8Ff9xGKmB7gbQ2A`S^xk507*qoM6N<$f@E{>=l}o! literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..204a97414f7a07219267b07842942a3f4d47fe0c GIT binary patch literal 61190 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 Eg4|t4`2YX_ literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..1d37c14aafd19840300a8b1a341885dc7916cb38 GIT binary patch literal 899 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 Ef`^Z)<= literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..a190238278ccdf9ade8f9d0f5122d8af36c0d0f2 GIT binary patch literal 10444 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 HQ`G+fUg}dQ literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..df56620df3aa79cd1557ae25bea65df1fce26404 GIT binary patch literal 26253 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 literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..704c26413c9af3d87de742e0364a540a9d72d0d5 GIT binary patch literal 2334 zcmV+(3E}pMP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f${h)G02R9FeES9^?9XBGe5nLBr8 zXLi{avrxdY+huu_T}#qx1FhRq6iAWQScQs(YP1_s3?)&ju~Lm`+W3e5V^s_$ZJLxq zjjgQ}k&w0&x>=x2ftJFSWqEYj$IL!wcjtBIe*VsP@0}gMsEMA;x!>!2=lst3&i5GL z8Zx9kkQh>c!gn9cy2C0zs+zv}wkl)>m6lN1q{?P92wT=Q3iSNsuo4|xwj5)bRDt-k z0OD0a92f~BUKxZT^ObU77^05Ggs5I!#dcb4YVB4-$ty1hw1i_*itOZB=CSmvm(jYZ z2i29aFS#0WLrQu4P}??DNGFswMRQua(@n9$q=dl8B|2fn=MyH!{B&X6wr#B4bP;Nz3Kfw&_Uw)$5MXYFD#}d_!zoaa zC?snirF8S{(1|f@|6KyvKpD(v277)VML?VN!Ym4sqs&LgF^usXR50Tz;y0VkqV3sB z7()bGmv!L5`&*H7EF?zKu06_VT~2N&ZzyHvl3jG6(H-Q-kpsIit8pnB8s@r61*Gqq zRKxLdo$${+|H5NB8+1kx8r>Kf?8kw(PN9D8Y~8+gTrVwPYXnp#6eZtE<_wtY)~)-( z-o1Mdvd$zZl3mhw)YSPIhdqC%ggP(6djk1b)aA^0=Z&6mTqc8V zD!cATr>jIpBmz`8JeI?`l!?6Mz~%v2T{VpZ;+h*G_vG~UyH=o;QGk=Vk8#K5P7G!O zNLvLgUVIsr74nRTk|F}uG<@CX2`KD;cL-Lkg~@U5NLqj5d$T;dQ%JQ7i4g|TkC_Ku zR6YID2e^`}!=l;|?D!%rhFZ9hynDx+pcTX`2}^hA*~RJ;tsi1Im%{3mt%#OM%s5_R zK#WYqKosa$2owdUVB_`I-@$!rR&cUJT}WahzEj4~7WFE~O#Ra``lE2M>pd*J`(Btq z*M^=b)`ciOo=Oc!RYqn1y1l-32DGp1Ev#D2%SS`@tQHYEc=^?RzY+yPMssuXO8455 ztE*pS$1baHePe?vOkPncEmzspl_po)M<)*-=puDOw`iGEd zel0g}7FHd1y-rZ#^h;Ot`nKQim9h(VKG}nVuMHv;h`|ihVtV7HD}&Z=+tKc>jEMRX zFcOKrHPdUVUi0-70r(w004arJrc`|mCH4NE3^uPnig0)~B3a&HMi6z4ec1X`6m@lR zB!@@7JY&YEUJ(smj=_pXtY_HadUjn_F^jZ8FPmgs17TZAug%?RU7PL(g~Y?8Cw1<2(@}1RX?xCnsMnhy)e3ekY7MGb&u~wf?L1 z_z^&2i0F#ASe=jv@Raocxno{KqRy(Fx3S@uz33mwvl-^I#^9sLMt}}hgz{*+e*hKL zi!hv$?N9y}P80#?Yt2(Ep62p=*j9O0o$s`^?+V#=9*^$2fZoY89BUe~e8mS08@x7g zcYO~YSrtV~a|?Gu*j+`LE96JPZC!N^aK+>;%#k8eX?UoL;TKK39`kve>AuLTaxT+3 zjfWQYV8zW5EL=DrwY9hM=Cj^ki3L zp!WiPu;d+dkM`j6tJk1B)_`D;E6=y*m>jZ`Iajdw^&An0N-PxtRK<);C}hUmL^Hf4 zU=m(uTp^pupf~X*4!=K+A3gpR#9}cmrrpya_N?kUQJ0n+vJ1AngEaX7)bSG^cg$3g#=NHXyS|%O2!Sk@D-{hiq7mklHh0c?QaPHg(2vy9-^5u8wwHEm)044GD z=DFkUxAWZjjs?xjzRuP~@>S1kIThv6)$-IY9$&j^+0Az}wtRM8G*&i^Cm8n_j12#% zrE(Gc^)Gv{{(-fK#`KlUDW&9vJRr|mJ3Bd%PF?z>r)%H-H;!!o@y}kR{PXGV_07*qoM6N<$ Eg7E%bApigX literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0fb70a4850ccb5fc6baa3d4fc9a188aa22bc8f32 GIT binary patch literal 6202 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

`cK^XWL1sD8`5(jkHyF#PH+fgV(A>DW8AH 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 Hqs09mQyQsI literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/Contents.json b/MarkEditMac/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MarkEditMac/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MarkEditMac/Resources/en.lproj/Localizable.strings b/MarkEditMac/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..ed60b89c47319bc2c37b5775c777dddd6a16d56b GIT binary patch literal 4 LcmezWkBb2S2Mz)V literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/zh-Hans.lproj/Localizable.strings b/MarkEditMac/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..29dfbdde --- /dev/null +++ b/MarkEditMac/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,291 @@ +/* Use 1 tab as the indent unit */ +"1 tab" = "1 个制表符"; + +/* Use 2 spaces as the indent unit */ +"2 spaces" = "2 个空格"; + +/* Use 2 tabs as the indent unit */ +"2 tabs" = "2 个制表符"; + +/* Use 4 spaces as the indent unit */ +"4 spaces" = "4 个空格"; + +/* Option to show active line indicator */ +"Active line indicator" = "当前行指示器"; + +/* Button title, perform actions to all items */ +"All" = "全部"; + +/* Appearance for the app */ +"Appearance:" = "外观:"; + +/* Automatic window tabbing mode */ +"Automatic" = "自动"; + +/* Toolbar item to toggle bold */ +"Bold" = "粗体"; + +/* Toggle case sensitive search */ +"Case Sensitive" = "大小写敏感"; + +/* Line endings used on Classic Mac OS */ +"Classic Mac OS (CR)" = "Classic Mac OS (CR)"; + +/* Menu item: clear recents */ +"Clear Recents" = "清除最近"; + +/* Column name for table creation */ +"Column" = "列"; + +/* Compact mode for window toolbar */ +"Compact" = "紧凑"; + +/* Phrase used in CodeMirror to indicate control character */ +"Control Character" = "控制字符"; + +/* Toolbar item to copy pandoc command */ +"Copy Pandoc Command" = "拷贝 Pandoc 指令"; + +/* Always use dark mode for the app */ +"Dark" = "深色"; + +/* Dark theme for the editor */ +"Dark Theme:" = "深色主题:"; + +/* Line endings for creating new files */ +"Default Line Endings:" = "默认行尾格式:"; + +/* Text encoding for opening and saving files */ +"Default Text Encoding:" = "默认文本编码:"; + +/* Explanation for focus mode */ +"Dim inactive lines" = "淡化非当前行"; + +/* Disallowed window tabbing mode */ +"Disallowed" = "禁用"; + +/* Button title, confirm an action */ +"Done" = "完成"; + +/* Editor behavior like focus mode and typewriter mode */ +"Edit Behavior:" = "编辑行为:"; + +/* Window title for editor settings */ +"Editor" = "编辑器"; + +/* Find mode in search menu */ +"Find" = "查找"; + +/* Menu item: use selection to find */ +"Find Selection" = "查找选中内容"; + +/* Phrase used in CodeMirror fold a line */ +"Fold Line" = "折叠行"; + +/* Phrase used in CodeMirror to indicated folded code */ +"Folded Code" = "折叠的代码"; + +/* Phrase used in CodeMirror to indicate folded lines */ +"Folded Lines" = "折叠的行"; + +/* Label for font settings */ +"Font:" = "字体:"; + +/* Window title for general settings */ +"General" = "通用"; + +/* Placeholder text for goto line window */ +"Go to Line" = "跳转到行"; + +/* Toolbar item to toggle heading levels */ +"Headers" = "标题"; + +/* Hidden mode for window toolbar */ +"Hidden" = "隐藏"; + +/* Toolbar item to insert horizontal rule */ +"Horizontal Rule" = "水平分割线"; + +/* Toolbar item to insert code */ +"Insert Code" = "插入代码"; + +/* Toolbar item to insert image */ +"Insert Image" = "插入图片"; + +/* Toolbar item to insert link */ +"Insert Link" = "插入链接"; + +/* Press tab key to insert 2 spaces */ +"Inserts 2 spaces" = "插入 2 个空格"; + +/* Press tab key to insert 4 spaces */ +"Inserts 4 spaces" = "插入 4 个空格"; + +/* Default tab key behavior */ +"Inserts tab character" = "插入制表符"; + +/* Option to show invisible characters */ +"Invisible characters" = "不可见字符"; + +/* Toolbar item to toggle italic */ +"Italic" = "斜体"; + +/* Item name for table creation */ +"Item" = "项目"; + +/* Explanation for typewriter mode */ +"Keep caret in the middle" = "保持光标居中"; + +/* Always use light mode for the app */ +"Light" = "浅色"; + +/* Light theme for the editor */ +"Light Theme:" = "浅色主题:"; + +/* Label for line height option */ +"Line Height:" = "行高:"; + +/* Option to show line numbers */ +"Line numbers" = "行号"; + +/* Label for line wrapping option */ +"Line Wrapping:" = "自动折行:"; + +/* Toggle literal search */ +"Literal Search" = "字面查找"; + +/* Line endings used on macOS and Unix */ +"macOS / Unix (LF)" = "macOS / Unix (LF)"; + +/* Menu item: create a new document */ +"New Document" = "新建文档"; + +/* Behavior when creating new windows */ +"New Window Behavior:" = "新窗口行为:"; + +/* Button title, move to the next item */ +"Next" = "下一个"; + +/* Normal line spacing */ +"Normal" = "正常"; + +/* Menu item: open an existing document */ +"Open Document" = "打开文档"; + +/* Menu item for selecting custom fonts */ +"Open Font Panel..." = "打开字体面板…"; + +/* Label for indent unit settings */ +"Prefer Indent Using:" = "缩进偏好:"; + +/* Preferred window tabbing mode */ +"Preferred" = "首选"; + +/* Button title for code preview */ +"preview" = "预览"; + +/* Button title, move to the previous item */ +"Previous" = "上一个"; + +/* Toolbar item to toggle blockquote */ +"Quote" = "引用"; + +/* Menu item: recent searches */ +"Recent Searches" = "最近的搜索"; + +/* Label for the option to reduce window transparency */ +"Reduce Transparency:" = "降低透明度:"; + +/* Toggle regular expression for search */ +"Regular Expression" = "正则表达式"; + +/* Relaxed line spacing */ +"Relaxed" = "宽松"; + +/* Explanation for the option to reduce window transparency */ +"Remove the toolbar blur" = "移除工具栏模糊效果"; + +/* Replace mode in search menu */ +"Replace" = "替换"; + +/* Menu item: select all occurrences */ +"Select All Occurrences" = "选择所有相同项"; + +/* Menu label for selecting fonts */ +"Select..." = "选择…"; + +/* Option to show selection status */ +"Selection status" = "选择状态"; + +/* Toolbar item to share the document */ +"Share this document" = "分享此文档"; + +/* Label for display options */ +"Show:" = "显示:"; + +/* Toolbar item to toggle strikethrough */ +"Strikethrough" = "删除线"; + +/* Follow the system appearance */ +"System" = "系统"; + +/* System default font name */ +"System Default" = "系统默认"; + +/* System mono font name */ +"System Mono" = "系统等宽"; + +/* System rounded font name */ +"System Rounded" = "系统圆润"; + +/* System serif font name */ +"System Serif" = "系统衬线"; + +/* Label for tab key behavior settings */ +"Tab Key:" = "制表键:"; + +/* Label for window tabbing mode settings */ +"Tabbing Mode:" = "标签模式:"; + +/* Toolbar item to insert table */ +"Table" = "表格"; + +/* Toolbar item to show table of contents */ +"Table of Contents" = "内容目录"; + +/* Toolbar item to use text format menu */ +"Text Format" = "文本格式"; + +/* Tight line spacing */ +"Tight" = "紧凑"; + +/* Default title used for link insertion */ +"title" = "标题"; + +/* Toolbar item to toggle bullet list */ +"Toggle List" = "切换列表"; + +/* Label for window toolbar mode */ +"Toolbar Mode:" = "工具栏模式:"; + +/* Phrase used in CodeMirror to unfold a piece of text */ +"Unfold" = "展开"; + +/* Phrase used in CodeMirror to unfold a line */ +"Unfold Line" = "展开行"; + +/* Phrase used in CodeMirror to indicate unfolded lines */ +"Unfolded Lines" = "展开的行"; + +/* Toggle whole word search */ +"Whole Word" = "整词查找"; + +/* Window title for window settings */ +"Window" = "窗口"; + +/* Line endings used on Windows */ +"Windows (CRLF)" = "Windows (CRLF)"; + +/* Explanation for line wrapping option */ +"Wrap lines to editor width" = "按编辑器的宽度换行"; + diff --git a/MarkEditMac/Resources/zh-Hant.lproj/Localizable.strings b/MarkEditMac/Resources/zh-Hant.lproj/Localizable.strings new file mode 100644 index 00000000..90ddcb5d --- /dev/null +++ b/MarkEditMac/Resources/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,291 @@ +/* Use 1 tab as the indent unit */ +"1 tab" = "1 個制表符"; + +/* Use 2 spaces as the indent unit */ +"2 spaces" = "2 個空格"; + +/* Use 2 tabs as the indent unit */ +"2 tabs" = "2 個制表符"; + +/* Use 4 spaces as the indent unit */ +"4 spaces" = "4 個空格"; + +/* Option to show active line indicator */ +"Active line indicator" = "當前行指示器"; + +/* Button title, perform actions to all items */ +"All" = "全部"; + +/* Appearance for the app */ +"Appearance:" = "外觀:"; + +/* Automatic window tabbing mode */ +"Automatic" = "自動"; + +/* Toolbar item to toggle bold */ +"Bold" = "粗體"; + +/* Toggle case sensitive search */ +"Case Sensitive" = "大小寫敏感"; + +/* Line endings used on Classic Mac OS */ +"Classic Mac OS (CR)" = "Classic Mac OS (CR)"; + +/* Menu item: clear recents */ +"Clear Recents" = "清除最近"; + +/* Column name for table creation */ +"Column" = "列"; + +/* Compact mode for window toolbar */ +"Compact" = "緊湊"; + +/* Phrase used in CodeMirror to indicate control character */ +"Control Character" = "控制字元"; + +/* Toolbar item to copy pandoc command */ +"Copy Pandoc Command" = "拷貝 Pandoc 指令"; + +/* Always use dark mode for the app */ +"Dark" = "深色"; + +/* Dark theme for the editor */ +"Dark Theme:" = "深色主題:"; + +/* Line endings for creating new files */ +"Default Line Endings:" = "預設行尾格式:"; + +/* Text encoding for opening and saving files */ +"Default Text Encoding:" = "預設文字編碼:"; + +/* Explanation for focus mode */ +"Dim inactive lines" = "淡化非當前行"; + +/* Disallowed window tabbing mode */ +"Disallowed" = "禁止"; + +/* Button title, confirm an action */ +"Done" = "完成"; + +/* Editor behavior like focus mode and typewriter mode */ +"Edit Behavior:" = "編輯行為:"; + +/* Window title for editor settings */ +"Editor" = "編輯器"; + +/* Find mode in search menu */ +"Find" = "尋找"; + +/* Menu item: use selection to find */ +"Find Selection" = "尋找選中內容"; + +/* Phrase used in CodeMirror fold a line */ +"Fold Line" = "折疊行"; + +/* Phrase used in CodeMirror to indicated folded code */ +"Folded Code" = "折疊的程式碼"; + +/* Phrase used in CodeMirror to indicate folded lines */ +"Folded Lines" = "折疊的行"; + +/* Label for font settings */ +"Font:" = "字體:"; + +/* Window title for general settings */ +"General" = "一般"; + +/* Placeholder text for goto line window */ +"Go to Line" = "跳轉到行"; + +/* Toolbar item to toggle heading levels */ +"Headers" = "標題"; + +/* Hidden mode for window toolbar */ +"Hidden" = "隱藏"; + +/* Toolbar item to insert horizontal rule */ +"Horizontal Rule" = "水平分割線"; + +/* Toolbar item to insert code */ +"Insert Code" = "插入程式碼"; + +/* Toolbar item to insert image */ +"Insert Image" = "插入圖片"; + +/* Toolbar item to insert link */ +"Insert Link" = "插入連結"; + +/* Press tab key to insert 2 spaces */ +"Inserts 2 spaces" = "插入 2 個空格"; + +/* Press tab key to insert 4 spaces */ +"Inserts 4 spaces" = "插入 4 個空格"; + +/* Default tab key behavior */ +"Inserts tab character" = "插入制表符"; + +/* Option to show invisible characters */ +"Invisible characters" = "不可見字元"; + +/* Toolbar item to toggle italic */ +"Italic" = "斜體"; + +/* Item name for table creation */ +"Item" = "項目"; + +/* Explanation for typewriter mode */ +"Keep caret in the middle" = "保持游標居中"; + +/* Always use light mode for the app */ +"Light" = "浅色"; + +/* Light theme for the editor */ +"Light Theme:" = "淺色主題:"; + +/* Label for line height option */ +"Line Height:" = "行高:"; + +/* Option to show line numbers */ +"Line numbers" = "行號"; + +/* Label for line wrapping option */ +"Line Wrapping:" = "自動折行:"; + +/* Toggle literal search */ +"Literal Search" = "字面尋找"; + +/* Line endings used on macOS and Unix */ +"macOS / Unix (LF)" = "macOS / Unix (LF)"; + +/* Menu item: create a new document */ +"New Document" = "新增文件"; + +/* Behavior when creating new windows */ +"New Window Behavior:" = "新視窗行為:"; + +/* Button title, move to the next item */ +"Next" = "下一個"; + +/* Normal line spacing */ +"Normal" = "正常"; + +/* Menu item: open an existing document */ +"Open Document" = "打開文件"; + +/* Menu item for selecting custom fonts */ +"Open Font Panel..." = "打開字體面板…"; + +/* Label for indent unit settings */ +"Prefer Indent Using:" = "縮排偏好:"; + +/* Preferred window tabbing mode */ +"Preferred" = "首選"; + +/* Button title for code preview */ +"preview" = "預覽"; + +/* Button title, move to the previous item */ +"Previous" = "上一個"; + +/* Toolbar item to toggle blockquote */ +"Quote" = "引用"; + +/* Menu item: recent searches */ +"Recent Searches" = "最近的搜尋"; + +/* Label for the option to reduce window transparency */ +"Reduce Transparency:" = "減少透明度:"; + +/* Toggle regular expression for search */ +"Regular Expression" = "正規表示法"; + +/* Relaxed line spacing */ +"Relaxed" = "寬鬆"; + +/* Explanation for the option to reduce window transparency */ +"Remove the toolbar blur" = "移除工具列模糊效果"; + +/* Replace mode in search menu */ +"Replace" = "取代"; + +/* Menu item: select all occurrences */ +"Select All Occurrences" = "選擇所有相同項"; + +/* Menu label for selecting fonts */ +"Select..." = "選擇…"; + +/* Option to show selection status */ +"Selection status" = "選擇狀態"; + +/* Toolbar item to share the document */ +"Share this document" = "分享此文件"; + +/* Label for display options */ +"Show:" = "顯示:"; + +/* Toolbar item to toggle strikethrough */ +"Strikethrough" = "刪除線"; + +/* Follow the system appearance */ +"System" = "系統"; + +/* System default font name */ +"System Default" = "系統預設"; + +/* System mono font name */ +"System Mono" = "系統等寬"; + +/* System rounded font name */ +"System Rounded" = "系統圓潤"; + +/* System serif font name */ +"System Serif" = "系統襯線"; + +/* Label for tab key behavior settings */ +"Tab Key:" = "製表鍵:"; + +/* Label for window tabbing mode settings */ +"Tabbing Mode:" = "標籤模式:"; + +/* Toolbar item to insert table */ +"Table" = "表格"; + +/* Toolbar item to show table of contents */ +"Table of Contents" = "內容目錄"; + +/* Toolbar item to use text format menu */ +"Text Format" = "文字格式"; + +/* Tight line spacing */ +"Tight" = "緊湊"; + +/* Default title used for link insertion */ +"title" = "標題"; + +/* Toolbar item to toggle bullet list */ +"Toggle List" = "切換列表"; + +/* Label for window toolbar mode */ +"Toolbar Mode:" = "工具列模式:"; + +/* Phrase used in CodeMirror to unfold a piece of text */ +"Unfold" = "展開"; + +/* Phrase used in CodeMirror to unfold a line */ +"Unfold Line" = "展開行"; + +/* Phrase used in CodeMirror to indicate unfolded lines */ +"Unfolded Lines" = "展開的行"; + +/* Toggle whole word search */ +"Whole Word" = "整詞尋找"; + +/* Window title for window settings */ +"Window" = "視窗"; + +/* Line endings used on Windows */ +"Windows (CRLF)" = "Windows (CRLF)"; + +/* Explanation for line wrapping option */ +"Wrap lines to editor width" = "按編輯器的寬度換行"; + diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Config.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Config.swift new file mode 100644 index 00000000..a6fdcac5 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Config.swift @@ -0,0 +1,72 @@ +// +// EditorViewController+Config.swift +// MarkEditMac +// +// Created by cyan on 1/28/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + func setTheme(_ theme: AppTheme) { + updateWindowColors(theme) + bridge.config.setTheme(name: theme.editorTheme) + + // It's possible to select a light theme for dark mode, + // override the window appearance to keep consistent. + view.window?.appearance = theme.resolvedAppearance + } + + func setFontFamily(_ fontFamily: String) { + bridge.config.setFontFamily(fontFamily: fontFamily) + } + + func setFontSize(_ fontSize: Double) { + bridge.config.setFontSize(fontSize: fontSize) + } + + func setShowLineNumbers(enabled: Bool) { + bridge.config.setShowLineNumbers(enabled: enabled) + } + + func setShowActiveLineIndicator(enabled: Bool) { + bridge.config.setShowActiveLineIndicator(enabled: enabled) + } + + func setShowInvisibles(enabled: Bool) { + bridge.config.setShowInvisibles(enabled: enabled) + } + + func setShowSelectionStatus(enabled: Bool) { + statusView.isHidden = !enabled + } + + func setTypewriterMode(enabled: Bool) { + bridge.config.setTypewriterMode(enabled: enabled) + } + + func setFocusMode(enabled: Bool) { + bridge.config.setFocusMode(enabled: enabled) + } + + func setLineWrapping(enabled: Bool) { + bridge.config.setLineWrapping(enabled: enabled) + } + + func setLineHeight(_ lineHeight: Double) { + bridge.config.setLineHeight(lineHeight: lineHeight) + } + + func setDefaultLineBreak(_ lineBreak: String?) { + bridge.config.setDefaultLineBreak(lineBreak: lineBreak) + } + + func setTabKeyBehavior(_ behavior: TabKeyBehavior) { + bridge.config.setTabKeyBehavior(behavior: behavior) + } + + func setIndentUnit(_ unit: IndentUnit) { + bridge.config.setIndentUnit(unit: unit.characters) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Delegate.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Delegate.swift new file mode 100644 index 00000000..06288998 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Delegate.swift @@ -0,0 +1,121 @@ +// +// EditorViewController+Delegate.swift +// MarkEditMac +// +// Created by cyan on 12/27/22. +// + +import AppKit +import WebKit +import MarkEditKit +import Proofing + +// MARK: - WKUIDelegate + +extension EditorViewController: WKUIDelegate { + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + guard let url = navigationAction.request.url else { + return nil + } + + // Instead of creating a new WebView, opening the link in the system default browser + if url.absoluteString.contains("grammarly") { + Grammarly.shared.startOAuth(url: url, bridge: bridge.grammarly) + } else { + NSWorkspace.shared.open(url) + } + + return nil + } +} + +// MARK: - EditorWebViewMenuDelegate + +extension EditorViewController: EditorWebViewMenuDelegate { + func editorWebView(_ sender: EditorWebView, didSelect menuAction: EditorWebViewMenuAction) { + switch menuAction { + case .findSelection: + findSelection(self) + case .selectAllOccurrences: + selectAllOccurrences() + } + } +} + +// MARK: - EditorModuleCoreDelegate + +extension EditorViewController: EditorModuleCoreDelegate { + func editorModuleCoreWindowDidLoad(_ sender: EditorModuleCore) { + hasFinishedLoading = true + resetEditor() + } + + func editorModuleCoreTextDidChange(_ sender: EditorModuleCore) { + document?.updateChangeCount(.changeDone) + + if findPanel.mode != .hidden { + Task { + if let count = try? await bridge.search.numberOfMatches() { + updateTextFinderPanels(numberOfItems: count) + } + } + } + } + + func editorModuleCore(_ sender: EditorModuleCore, selectionDidChange lineColumn: LineColumnInfo) { + statusView.updateLineColumn(lineColumn) + layoutStatusView() + } +} + +// MARK: - EditorModulePreviewDelegate + +extension EditorViewController: EditorModulePreviewDelegate { + func editorModulePreview(_ sender: NativeModulePreview, show code: String, type: PreviewType, rect: CGRect) { + showPreview(code: code, type: type, rect: rect) + } +} + +// MARK: - EditorFindPanelDelegate + +extension EditorViewController: EditorFindPanelDelegate { + func editorFindPanel(_ sender: EditorFindPanel, modeDidChange mode: EditorFindMode) { + updateTextFinderMode(mode) + } + + func editorFindPanel(_ sender: EditorFindPanel, searchTermDidChange searchTerm: String) { + updateTextFinderQuery() + } + + func editorFindPanelDidChangeOptions(_ sender: EditorFindPanel) { + updateTextFinderQuery() + } + + func editorFindPanelDidPressTabKey(_ sender: EditorFindPanel) { + replacePanel.textField.startEditing(in: view.window) + } + + func editorFindPanelDidClickNext(_ sender: EditorFindPanel) { + findNextInTextFinder() + } + + func editorFindPanelDidClickPrevious(_ sender: EditorFindPanel) { + findPreviousInTextFinder() + } +} + +// MARK: - EditorReplacePanelDelegate + +extension EditorViewController: EditorReplacePanelDelegate { + func editorReplacePanel(_ sender: EditorReplacePanel, replacementDidChange replacement: String) { + updateTextFinderQuery() + } + + func editorReplacePanelDidClickReplaceNext(_ sender: EditorReplacePanel) { + replaceNextInTextFinder() + } + + func editorReplacePanelDidClickReplaceAll(_ sender: EditorReplacePanel) { + replaceAllInTextFinder() + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Encoding.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Encoding.swift new file mode 100644 index 00000000..bdf81bdd --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Encoding.swift @@ -0,0 +1,24 @@ +// +// EditorViewController+Encoding.swift +// MarkEditMac +// +// Created by cyan on 1/3/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + @objc func reopenWithEncoding(_ sender: NSMenuItem) { + guard let encoding = sender.representedObject as? EditorTextEncoding else { + return + } + + guard let data = document?.fileData, let string = encoding.decode(data: data) else { + return + } + + document?.stringValue = string + resetEditor() + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+GotoLine.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+GotoLine.swift new file mode 100644 index 00000000..3b055825 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+GotoLine.swift @@ -0,0 +1,30 @@ +// +// EditorViewController+GotoLine.swift +// MarkEditMac +// +// Created by cyan on 1/17/23. +// + +import AppKit +import AppKitControls +import MarkEditKit + +extension EditorViewController { + func showGotoLineWindow(_ sender: Any?) { + guard let parentRect = view.window?.frame else { + Logger.assertFail("Failed to retrieve window.frame to proceed") + return + } + + let window = GotoLineWindow( + relativeTo: parentRect, + placeholder: Localized.Document.gotoLine, + iconName: Icons.arrowUturnBackwardCircle + ) { [weak self] lineNumber in + self?.bridge.selection.gotoLine(lineNumber: lineNumber) + } + + window.appearance = view.effectiveAppearance + window.makeKeyAndOrderFront(sender) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+HyperLink.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+HyperLink.swift new file mode 100644 index 00000000..0338c934 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+HyperLink.swift @@ -0,0 +1,30 @@ +// +// EditorViewController+HyperLink.swift +// MarkEditMac +// +// Created by cyan on 1/4/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + func insertHyperLink(prefix: String?) { + Task { + guard let text = try? await bridge.selection.getText() else { + return + } + + let prefersURL = text == NSDataDetector.extractURL(from: text) + let defaultTitle = Localized.Editor.defaultLinkTitle + let title = (text.isEmpty || text.components(separatedBy: .newlines).count > 1) ? defaultTitle : text + + // Try our best to guess from selection and clipboard + bridge.format.insertHyperLink( + title: prefersURL ? defaultTitle : title, + url: prefersURL ? text : (NSPasteboard.general.url ?? "https://"), + prefix: prefix + ) + } + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+LineEndings.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+LineEndings.swift new file mode 100644 index 00000000..c35cc587 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+LineEndings.swift @@ -0,0 +1,24 @@ +// +// EditorViewController+LineEndings.swift +// MarkEditMac +// +// Created by cyan on 1/28/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + @IBAction func setLineEndings(_ sender: Any?) { + guard let item = sender as? NSMenuItem else { + return Logger.assertFail("Invalid sender") + } + + guard let lineEndings = LineEndings(rawValue: item.tag) else { + return Logger.assertFail("Invalid lineEndings: \(item.tag)") + } + + document?.updateChangeCount(.changeDone) + bridge.lineEndings.setLineEndings(lineEndings: lineEndings) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Menu.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Menu.swift new file mode 100644 index 00000000..0e2e6244 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Menu.swift @@ -0,0 +1,284 @@ +// +// EditorViewController+Menu.swift +// MarkEditMac +// +// Created by cyan on 12/15/22. +// + +import AppKit +import MarkEditKit +import FontPicker +import Proofing + +// MARK: - NSMenuDelegate + +extension EditorViewController: NSMenuDelegate { + func menuNeedsUpdate(_ menu: NSMenu) { + updateToolbarItemMenus(menu) + } +} + +// MARK: - NSMenuItemValidation + +extension EditorViewController: NSMenuItemValidation { + /// Actions that require the existence of a file + private static let fileActions = [ + #selector(copyFilePath(_:)), + #selector(copyFolderPath(_:)), + #selector(copyPandocCommand(_:)), + #selector(revealInFinder(_:)), + ] + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if let action = menuItem.action, Self.fileActions.contains(action) { + return document?.fileURL != nil + } + + return true + } +} + +// MARK: - Formatting + +extension EditorViewController { + + // MARK: - Headers + + @IBAction func toggleH1(_ sender: Any?) { + bridge.format.toggleHeading(level: 1) + } + + @IBAction func toggleH2(_ sender: Any?) { + bridge.format.toggleHeading(level: 2) + } + + @IBAction func toggleH3(_ sender: Any?) { + bridge.format.toggleHeading(level: 3) + } + + @IBAction func toggleH4(_ sender: Any?) { + bridge.format.toggleHeading(level: 4) + } + + @IBAction func toggleH5(_ sender: Any?) { + bridge.format.toggleHeading(level: 5) + } + + @IBAction func toggleH6(_ sender: Any?) { + bridge.format.toggleHeading(level: 6) + } + + // MARK: - Text Styles + + @IBAction func toggleBold(_ sender: Any?) { + bridge.format.toggleBold() + } + + @IBAction func toggleItalic(_ sender: Any?) { + bridge.format.toggleItalic() + } + + @IBAction func toggleStrikethrough(_ sender: Any?) { + bridge.format.toggleStrikethrough() + } + + // MARK: - Hyper Link + + @IBAction func insertLink(_ sender: Any?) { + insertHyperLink(prefix: nil) + } + + @IBAction func insertImage(_ sender: Any?) { + insertHyperLink(prefix: "!") + } + + // MARK: - List + + @IBAction func toggleBullet(_ sender: Any?) { + bridge.format.toggleBullet() + } + + @IBAction func toggleNumbering(_ sender: Any?) { + bridge.format.toggleNumbering() + } + + @IBAction func toggleTodo(_ sender: Any?) { + bridge.format.toggleTodo() + } + + // MARK: - Others + + @IBAction func toggleBlockquote(_ sender: Any?) { + bridge.format.toggleBlockquote() + } + + @IBAction func toggleInlineCode(_ sender: Any?) { + bridge.format.toggleInlineCode() + } + + @IBAction func toggleInlineMath(_ sender: Any?) { + bridge.format.toggleInlineMath() + } + + @IBAction func insertCodeBlock(_ sender: Any?) { + bridge.format.insertCodeBlock() + } + + @IBAction func insertMathBlock(_ sender: Any?) { + bridge.format.insertMathBlock() + } + + @IBAction func insertHorizontalRule(_ sender: Any?) { + bridge.format.insertHorizontalRule() + } + + @IBAction func insertTable(_ sender: Any?) { + bridge.format.insertTable( + columnName: Localized.Editor.tableColumnName, + itemName: Localized.Editor.tableItemName + ) + } +} + +// MARK: - Text Find + +extension EditorViewController { + @IBAction func startFind(_ sender: Any?) { + updateTextFinderMode(.find) + } + + @IBAction func startReplace(_ sender: Any?) { + updateTextFinderMode(.replace) + } + + @IBAction func findSelection(_ sender: Any?) { + findSelectionInTextFinder() + } + + @IBAction func findNextMatch(_ sender: Any?) { + findNextInTextFinder() + } + + @IBAction func findPreviousMatch(_ sender: Any?) { + findPreviousInTextFinder() + } + + @IBAction func scrollToSelection(_ sender: Any?) { + bridge.selection.scrollToSelection() + } +} + +// MARK: - Document + +private extension EditorViewController { + @IBAction func createNewTab(_ sender: Any?) { + // The easiest way to always create tab regardless of the tabbing mode, + // just temporarily overwrite the mode to preferred and switch back later. + let tabbingMode = AppPreferences.Window.tabbingMode + AppPreferences.Window.tabbingMode = .preferred + + NSDocumentController.shared.newDocument(sender) + AppPreferences.Window.tabbingMode = tabbingMode + } + + @IBAction func revealInFinder(_ sender: Any?) { + guard let fileURL = document?.fileURL else { return } + NSWorkspace.shared.activateFileViewerSelecting([fileURL]) + } + + @IBAction func copyFilePath(_ sender: Any?) { + guard let fileURL = document?.fileURL else { return } + NSPasteboard.general.overwrite(string: fileURL.path) + } + + @IBAction func copyFolderPath(_ sender: Any?) { + guard let folderURL = document?.folderURL else { return } + NSPasteboard.general.overwrite(string: folderURL.path) + } + + @IBAction func copyPandocCommand(_ sender: Any?) { + guard let fileURL = document?.fileURL, let format = (sender as? NSMenuItem)?.identifier?.rawValue else { + Logger.log(.error, "Failed to copy pandoc command") + return + } + + copyPandocCommand(url: fileURL, format: format) + } + + @IBAction func learnPandoc(_ sender: Any?) { + if let url = URL(string: "https://github.com/MarkEdit-app/MarkEdit/wiki/Manual#pandoc") { + NSWorkspace.shared.open(url) + } + } +} + +// MARK: - Edit + +private extension EditorViewController { + @IBAction func undo(_ sender: Any?) { + bridge.history.undo() + } + + @IBAction func redo(_ sender: Any?) { + bridge.history.redo() + } + + @IBAction func gotoLine(_ sender: Any?) { + showGotoLineWindow(sender) + } + + @IBAction func makeFontBigger(_ sender: Any?) { + let fontSize = AppPreferences.Editor.fontSize + if fontSize < FontPicker.maximumFontSize { + AppPreferences.Editor.fontSize = fontSize + 1 + notifyFontSizeChanged() + } else { + NSSound.beep() + } + } + + @IBAction func makeFontSmaller(_ sender: Any?) { + let fontSize = AppPreferences.Editor.fontSize + if fontSize > FontPicker.minimumFontSize { + AppPreferences.Editor.fontSize = fontSize - 1 + notifyFontSizeChanged() + } else { + NSSound.beep() + } + } + + @IBAction func resetFontSize(_ sender: Any?) { + AppPreferences.Editor.fontSize = FontPicker.defaultFontSize + notifyFontSizeChanged() + } + + @IBAction func performEditCommand(_ sender: Any?) { + guard let identifier = (sender as? NSMenuItem)?.identifier?.rawValue else { + Logger.log(.error, "Missing identifier to performCommand") + return + } + + guard let command = EditCommand(rawValue: identifier) else { + Logger.log(.error, "Missing command to performCommand") + return + } + + bridge.format.performEditCommand(command: command) + } + + @IBAction func toggleGrammarly(_ sender: Any?) { + (sender as? NSMenuItem)?.toggle() + Grammarly.shared.toggle(bridge: bridge.grammarly) + } +} + +// MARK: - Private + +private extension EditorViewController { + func notifyFontSizeChanged() { + NotificationCenter.default.post( + name: .fontSizeChanged, + object: AppPreferences.Editor.fontSize + ) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Pandoc.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Pandoc.swift new file mode 100644 index 00000000..76803b30 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Pandoc.swift @@ -0,0 +1,33 @@ +// +// EditorViewController+Pandoc.swift +// MarkEditMac +// +// Created by cyan on 1/21/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + func copyPandocCommand(url: URL, format: String) { + // https://pandoc.org/ + let command = [ + "pandoc", + url.escapedFilePath, + "-f gfm -t \(format)", + "-s -o \(url.replacingPathExtension(format).escapedFilePath)", + "&& open \(url.deletingLastPathComponent().escapedFilePath)", + ].joined(separator: " ") + + NSPasteboard.general.overwrite(string: command) + NSWorkspace.shared.openTerminal() + } +} + +// MARK: - Private + +private extension URL { + var escapedFilePath: String { + path.replacingOccurrences(of: " ", with: "\\ ") + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Preview.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Preview.swift new file mode 100644 index 00000000..b57dbfe7 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Preview.swift @@ -0,0 +1,19 @@ +// +// EditorViewController+Preview.swift +// MarkEditMac +// +// Created by cyan on 1/7/23. +// + +import AppKit +import Previewer +import MarkEditKit + +extension EditorViewController { + func showPreview(code: String, type: PreviewType, rect: CGRect) { + let popover = NSPopover() + popover.behavior = .transient + popover.contentViewController = Previewer(code: code, type: type) + presentPopover(popover, rect: rect) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+TextFinder.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+TextFinder.swift new file mode 100644 index 00000000..3c8cea2d --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+TextFinder.swift @@ -0,0 +1,145 @@ +// +// EditorViewController+TextFinder.swift +// MarkEditMac +// +// Created by cyan on 12/18/22. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + func updateTextFinderMode(_ mode: EditorFindMode, searchTerm: String? = nil) { + if mode != .hidden { + // Move the focus to find panel, with a delay to make the focus ring animation more natural + DispatchQueue.afterDelay(seconds: 0.15) { + self.findPanel.searchField.startEditing(in: self.view.window) + } + } + + if let searchTerm { + findPanel.searchField.stringValue = searchTerm + if searchTerm.isEmpty { + findPanel.updateResult(numberOfItems: 0, emptyInput: true) + } + } + + guard findPanel.mode != mode else { + return + } + + hasUnfinishedAnimations = true + bridge.search.setState(enabled: mode != .hidden) + + findPanel.mode = mode + findPanel.resetMenu() + + // Move the focus back to editor + if mode == .hidden { + view.window?.makeFirstResponder(webView) + } + + // Unhide the replace panel, see below for details about this UI trick + if mode == .replace { + replacePanel.isHidden = false + } + + // Animate layout changes + NSAnimationContext.runAnimationGroup( + duration: 0.2 + ) { _ in + findPanel.animator().alphaValue = mode == .hidden ? 0 : 1 + replacePanel.animator().alphaValue = mode == .replace ? 1 : 0 + layoutPanels(animated: true) + layoutWebView(animated: true) + } completionHandler: { + self.hasUnfinishedAnimations = false + + // Must set isHidden because it is behind the find panel, + // alpha = 0 still tracks mouse, which makes the cursor "i-beam" for the magnifier + if mode != .replace { + self.replacePanel.isHidden = true + } + } + } + + func updateTextFinderQuery() { + let searchTerm = findPanel.searchField.stringValue + let replacement = replacePanel.textField.stringValue + + let options = SearchOptions( + search: searchTerm, + caseSensitive: AppPreferences.Search.caseSensitive, + literal: AppPreferences.Search.literalSearch, + regexp: AppPreferences.Search.regularExpression, + wholeWord: AppPreferences.Search.wholeWord, + replace: replacement + ) + + findPanel.searchField.addToRecents(searchTerm: searchTerm) + findPanel.resetMenu() + + Task { + if let count = try? await bridge.search.updateQuery(options: options) { + updateTextFinderPanels(numberOfItems: count) + } + } + } + + func updateTextFinderPanels(numberOfItems: Int) { + let searchTerm = findPanel.searchField.stringValue + findPanel.updateResult(numberOfItems: numberOfItems, emptyInput: searchTerm.isEmpty) + replacePanel.updateResult(numberOfItems: numberOfItems) + } + + func findSelectionInTextFinder() { + updateTextFinderMode(.find) + + Task { + guard let text = try? await bridge.selection.getText() else { + return + } + + findPanel.searchField.stringValue = text + DispatchQueue.afterDelay(seconds: 0.2) { // 0.2 is the animation duration of panel + self.updateTextFinderQuery() + } + } + } + + func findNextInTextFinder() { + prepareFinderNavigation() + bridge.search.findNext() + } + + func findPreviousInTextFinder() { + prepareFinderNavigation() + bridge.search.findPrevious() + } + + func replaceNextInTextFinder() { + bridge.search.replaceNext() + } + + func replaceAllInTextFinder() { + bridge.search.replaceAll() + } + + func selectAllOccurrences() { + bridge.search.selectAllOccurrences() + } +} + +// MARK: - Private + +private extension EditorViewController { + func prepareFinderNavigation() { + if findPanel.numberOfItems == 1 { + NSSound.beep() + } + + if findPanel.mode == .hidden { + updateTextFinderMode(.find) + } + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Toolbar.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Toolbar.swift new file mode 100644 index 00000000..a8bb418b --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+Toolbar.swift @@ -0,0 +1,227 @@ +// +// EditorViewController+Toolbar.swift +// MarkEditMac +// +// Created by cyan on 1/13/23. +// + +import AppKit +import MarkEditKit + +extension EditorViewController { + private enum Constants { + static let tableOfContentsMenuIdentifier = "tableOfContentsMenu" + static let normalizedButtonSize: Double = 15 // "bold" icon looks bigger than expected, fix it + } + + func updateToolbarItemMenus(_ menu: NSMenu) { + if menu.identifier?.rawValue == Constants.tableOfContentsMenuIdentifier { + updateTableOfContentsMenu(menu) + } + } +} + +// MARK: - NSToolbarDelegate + +extension EditorViewController: NSToolbarDelegate { + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + let item: NSToolbarItem? = { + switch itemIdentifier { + case .tableOfContents: return tableOfContentsItem + case .formatHeaders: return formatHeadersItem + case .toggleBold: return toggleBoldItem + case .toggleItalic: return toggleItalicItem + case .toggleStrikethrough: return toggleStrikethroughItem + case .insertLink: return insertLinkItem + case .insertImage: return insertImageItem + case .toggleList: return toggleListItem + case .toggleBlockquote: return toggleBlockquoteItem + case .horizontalRule: return horizontalRuleItem + case .insertTable: return insertTableItem + case .insertCode: return insertCodeItem + case .textFormat: return textFormatItem + case .shareDocument: return shareDocumentItem + case .copyPandocCommand: return copyPandocCommandItem + default: return nil + } + }() + + if let item, item.toolTip == nil { + if let shortcutHint = item.shortcutHint { + item.toolTip = "\(item.label) (\(shortcutHint))" + } else { + item.toolTip = item.label + } + } + + item?.isBordered = true + return item + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + NSToolbarItem.Identifier.defaultItems + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + NSToolbarItem.Identifier.allItems + } +} + +// MARK: - NSToolbarItemValidation + +extension EditorViewController: NSToolbarItemValidation { + func validateToolbarItem(_ item: NSToolbarItem) -> Bool { + true + } +} + +// MARK: - NSSharingServicePickerToolbarItemDelegate + +extension EditorViewController: NSSharingServicePickerToolbarItemDelegate { + func items(for pickerToolbarItem: NSSharingServicePickerToolbarItem) -> [Any] { + guard let document else { + return [] + } + + return [document] + } +} + +// MARK: - Private + +private extension EditorViewController { + var tableOfContentsItem: NSToolbarItem { + let menu = NSMenu() + menu.delegate = self + menu.identifier = NSUserInterfaceItemIdentifier(Constants.tableOfContentsMenuIdentifier) + + let label = NSMenuItem(title: Localized.Toolbar.tableOfContents, action: nil, keyEquivalent: "") + label.isEnabled = false + + menu.items = [label, .separator()] + menu.autoenablesItems = false + + return NSToolbarItem.with(identifier: .tableOfContents, menu: menu) + } + + var formatHeadersItem: NSToolbarItem { + .with(identifier: .formatHeaders, menu: NSApp.appDelegate?.formatHeadersMenu?.copiedMenu) + } + + var toggleBoldItem: NSToolbarItem { + .with(identifier: .toggleBold, iconSize: Constants.normalizedButtonSize) { [weak self] in + self?.toggleBold(nil) + } + } + + var toggleItalicItem: NSToolbarItem { + .with(identifier: .toggleItalic, iconSize: Constants.normalizedButtonSize) { [weak self] in + self?.toggleItalic(nil) + } + } + + var toggleStrikethroughItem: NSToolbarItem { + .with(identifier: .toggleStrikethrough, iconSize: Constants.normalizedButtonSize) { [weak self] in + self?.toggleStrikethrough(nil) + } + } + + var insertLinkItem: NSToolbarItem { + .with(identifier: .insertLink) { [weak self] in + self?.insertLink(nil) + } + } + + var insertImageItem: NSToolbarItem { + .with(identifier: .insertImage) { [weak self] in + self?.insertImage(nil) + } + } + + var toggleListItem: NSToolbarItem { + let menu = NSMenu() + menu.items = [ + NSApp.appDelegate?.formatBulletItem, + NSApp.appDelegate?.formatNumberingItem, + NSApp.appDelegate?.formatTodoItem, + ].compactMap { $0?.copiedItem } + + return NSToolbarItem.with(identifier: .toggleList, menu: menu) + } + + var toggleBlockquoteItem: NSToolbarItem { + .with(identifier: .toggleBlockquote) { [weak self] in + self?.toggleBlockquote(nil) + } + } + + var horizontalRuleItem: NSToolbarItem { + .with(identifier: .horizontalRule) { [weak self] in + self?.insertHorizontalRule(nil) + } + } + + var insertTableItem: NSToolbarItem { + .with(identifier: .insertTable) { [weak self] in + self?.insertTable(nil) + } + } + + var insertCodeItem: NSToolbarItem { + let menu = NSMenu() + menu.items = [ + NSApp.appDelegate?.formatCodeItem, + NSApp.appDelegate?.formatCodeBlockItem, + NSApp.appDelegate?.formatMathItem, + NSApp.appDelegate?.formatMathBlockItem, + ].compactMap { $0?.copiedItem } + + return NSToolbarItem.with(identifier: .insertCode, menu: menu) + } + + var textFormatItem: NSToolbarItem { + .with(identifier: .textFormat, menu: NSApp.appDelegate?.textFormatMenu?.copiedMenu) + } + + var shareDocumentItem: NSToolbarItem { + let item = NSSharingServicePickerToolbarItem(itemIdentifier: .shareDocument) + item.toolTip = Localized.Toolbar.shareDocument + item.image = NSImage(systemSymbolName: Icons.squareAndArrowUp, accessibilityDescription: Localized.Toolbar.shareDocument) + item.delegate = self + return item + } + + var copyPandocCommandItem: NSToolbarItem { + .with(identifier: .copyPandocCommand, menu: NSApp.appDelegate?.copyPandocCommandMenu?.copiedMenu) + } + + func updateTableOfContentsMenu(_ menu: NSMenu) { + // Remove existing items, the first two are placeholders that we want to keep + for (index, item) in menu.items.enumerated() where index > 1 { + menu.removeItem(item) + } + + Task { + let tableOfContents = try? await self.bridge.toc.getTableOfContents() + tableOfContents?.forEach { info in + let title = info.title.components(separatedBy: .newlines).first ?? info.title + let item = menu.addItem(withTitle: title, action: #selector(self.gotoHeader(_:))) + item.attributedTitle = NSAttributedString(string: title, attributes: [ + .font: NSFont.systemFont(ofSize: 15.0 - min(3, Double(info.level)), weight: .medium), + ]) + + item.representedObject = info + menu.addItem(.separator()) + } + } + } + + @objc func gotoHeader(_ sender: NSMenuItem) { + guard let headingInfo = sender.representedObject as? HeadingInfo else { + Logger.assertFail("Failed to get HeadingInfo from sender: \(sender)") + return + } + + bridge.toc.gotoHeader(headingInfo: headingInfo) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController+UI.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+UI.swift new file mode 100644 index 00000000..d7370692 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController+UI.swift @@ -0,0 +1,122 @@ +// +// EditorViewController+UI.swift +// MarkEditMac +// +// Created by cyan on 12/27/22. +// + +import AppKit + +extension EditorViewController { + func setUp() { + let wrapper = NSView(frame: CGRect(x: 0, y: 0, width: 720, height: 480)) + self.view = wrapper + + wrapper.addSubview(replacePanel) // ReplacePanel must go before FindPanel + wrapper.addSubview(findPanel) + wrapper.addSubview(panelDivider) + wrapper.addSubview(webView) + wrapper.addSubview(statusView) + + layoutPanels() + layoutWebView() + layoutStatusView() + + // Trigger an additional layout loop to correct view (find panels) positions + safeAreaObservation = view.observe(\.safeAreaInsets) { view, _ in + view.needsLayout = true + } + } + + func configureToolbar() { + let toolbar = NSToolbar(identifier: "EditorToolbar") + toolbar.displayMode = .iconOnly + toolbar.delegate = self + toolbar.allowsUserCustomization = true + toolbar.autosavesConfiguration = true + + view.window?.toolbar = toolbar + view.window?.toolbar?.validateVisibleItems() + + view.window?.acceptsMouseMovedEvents = true + view.window?.appearance = AppTheme.current.resolvedAppearance + + updateWindowColors(AppTheme.current) + } + + func updateWindowColors(_ theme: AppTheme) { + let backgroundColor = theme.windowBackground + view.window?.backgroundColor = backgroundColor + view.window?.toolbarContainerView?.layerBackgroundColor = backgroundColor + + statusView.setBackgroundColor(backgroundColor) + findPanel.setBackgroundColor(backgroundColor) + replacePanel.setBackgroundColor(backgroundColor) + } + + func layoutPanels(animated: Bool = false) { + findPanel.update(animated).frame = CGRect( + x: 0, + y: view.bounds.height - view.safeAreaInsets.top - (findPanel.mode == .hidden ? 0 : findPanel.frame.height), + width: view.bounds.width, + height: findPanel.frame.height + ) + + replacePanel.update(animated).frame = CGRect( + x: findPanel.frame.minX, + y: findPanel.frame.minY - (findPanel.mode == .replace ? replacePanel.frame.height : 0), + width: findPanel.frame.width, + height: findPanel.frame.height - findPanel.searchField.frame.minY + ) + + replacePanel.layoutInfo = (findPanel.searchField.frame, findPanel.findButtons.frame.height) + panelDivider.update(animated).frame = CGRect(x: 0, y: (findPanel.mode == .replace ? replacePanel : findPanel).frame.minY, width: view.frame.width, height: panelDivider.length) + } + + func layoutWebView(animated: Bool = false) { + webView.update(animated).frame = CGRect( + x: 0, + y: 0, + width: view.bounds.width, + height: panelDivider.frame.minY + ) + } + + func layoutStatusView() { + statusView.frame = CGRect( + x: view.bounds.width - statusView.frame.width - 6, + y: 8, // Vertical margins are intentionally larger to visually look the same + width: statusView.frame.width, + height: statusView.frame.height + ) + + view.mirrorImmediateSubviewIfNeeded(statusView) + } + + func handleMouseMoved(_ event: NSEvent) { + guard NSCursor.current != NSCursor.arrow else { + return + } + + // WKWebView contentEditable keeps showing i-beam, fix that + let location = event.locationInWindow.y + if location > view.frame.height - view.safeAreaInsets.top && location < view.frame.height { + NSCursor.arrow.push() + } + } + + func presentPopover(_ popover: NSPopover, rect: CGRect) { + if focusTrackingView.superview == nil { + webView.addSubview(focusTrackingView) + } + + focusTrackingView.frame = CGRect( + x: rect.minX, + y: rect.minY, + width: max(1, rect.width), // It can be zero, which is invalid + height: rect.height + ) + + popover.show(relativeTo: rect, of: focusTrackingView, preferredEdge: .maxX) + } +} diff --git a/MarkEditMac/Sources/Editor/Controllers/EditorViewController.swift b/MarkEditMac/Sources/Editor/Controllers/EditorViewController.swift new file mode 100644 index 00000000..d8c203cc --- /dev/null +++ b/MarkEditMac/Sources/Editor/Controllers/EditorViewController.swift @@ -0,0 +1,162 @@ +// +// EditorViewController.swift +// MarkEditMac +// +// Created by cyan on 12/12/22. + +import AppKit +import AppKitControls +import WebKit +import MarkEditCore +import MarkEditKit +import Proofing + +final class EditorViewController: NSViewController { + var hasFinishedLoading = false + var hasUnfinishedAnimations = false + var safeAreaObservation: NSKeyValueObservation? + + var editorText: String? { + get async { + guard hasFinishedLoading else { + return nil + } + + return try? await bridge.core.getEditorText() + } + } + + lazy var bridge = WebModuleBridge( + webView: webView + ) + + var document: EditorDocument? { + representedObject as? EditorDocument + } + + private(set) lazy var findPanel = { + let panel = EditorFindPanel() + panel.delegate = self + return panel + }() + + private(set) lazy var replacePanel = { + let panel = EditorReplacePanel() + panel.delegate = self + return panel + }() + + private(set) lazy var panelDivider = { + DividerView() + }() + + private(set) lazy var statusView = { + let view = EditorStatusView { [weak self] in + self?.showGotoLineWindow(nil) + } + + view.isHidden = !AppPreferences.Editor.showSelectionStatus + return view + }() + + private(set) lazy var focusTrackingView = { + FocusTrackingView() + }() + + private(set) lazy var webView: WKWebView = { + let modules = NativeModules(modules: [ + EditorModuleCore(delegate: self), + EditorModulePreview(delegate: self), + ]) + + let handler = EditorMessageHandler(modules: modules) { [weak self] in + self?.webView + } + + let controller = WKUserContentController() + controller.add(handler, name: "bridge") + + let config: WKWebViewConfiguration = .newConfig() + config.processPool = EditorReusePool.shared.processPool + config.userContentController = controller + + let webView = EditorWebView(frame: .zero, configuration: config) + webView.uiDelegate = self + webView.menuDelegate = self + + // Non-nil baseURL is required by web services like Grammarly + let baseURL = URL(string: "http://localhost") + webView.loadHTMLString(AppPreferences.editorConfig.toHtml, baseURL: baseURL) + return webView + }() + + init() { + super.init(nibName: nil, bundle: nil) + _ = self.webView // Pre-load + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + setUp() + } + + override func viewWillAppear() { + super.viewWillAppear() + configureToolbar() + } + + override func viewDidLayout() { + super.viewDidLayout() + guard !hasUnfinishedAnimations else { + return + } + + layoutPanels() + layoutWebView() + layoutStatusView() + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + handleMouseMoved(event) + } + + override var representedObject: Any? { + didSet { + resetEditor() + } + } +} + +// MARK: - Exposed Methods + +extension EditorViewController { + func clearEditor() { + bridge.core.clearEditor() + updateTextFinderMode(.hidden, searchTerm: "") + } + + func resetEditor() { + guard hasFinishedLoading, let text = document?.stringValue else { + return + } + + bridge.core.resetEditor(text: text) { _ in + self.bridge.textChecker.update(options: TextCheckerOptions( + spellcheck: true, + autocorrect: true, + autocomplete: true, + autocapitalize: false + )) + Grammarly.shared.update(bridge: self.bridge.grammarly) + } + } + + func markEditorDirty(_ isDirty: Bool) { + bridge.core.markEditorDirty(isDirty: isDirty) + } +} diff --git a/MarkEditMac/Sources/Editor/EditorWindow.swift b/MarkEditMac/Sources/Editor/EditorWindow.swift new file mode 100644 index 00000000..3a453c38 --- /dev/null +++ b/MarkEditMac/Sources/Editor/EditorWindow.swift @@ -0,0 +1,57 @@ +// +// EditorWindow.swift +// MarkEditMac +// +// Created by cyan on 1/12/23. +// + +import AppKit + +final class EditorWindow: NSWindow { + var toolbarMode: ToolbarMode? { + didSet { + toolbarStyle = toolbarMode == .compact ? .unifiedCompact : .unified + super.toolbar = toolbarMode == .hidden ? nil : cachedToolbar + } + } + + // swiftlint:disable:next discouraged_optional_boolean + var reduceTransparency: Bool? { + didSet { + layoutIfNeeded() + } + } + + override var toolbar: NSToolbar? { + get { + super.toolbar + } + set { + cachedToolbar = newValue + super.toolbar = toolbarMode == .hidden ? nil : newValue + } + } + + private var cachedToolbar: NSToolbar? + + override func awakeFromNib() { + super.awakeFromNib() + toolbar = NSToolbar() // Required for multi-tab layout + toolbarMode = AppPreferences.Window.toolbarMode + tabbingMode = AppPreferences.Window.tabbingMode + reduceTransparency = AppPreferences.Window.reduceTransparency + } + + override func layoutIfNeeded() { + super.layoutIfNeeded() + + // Slightly change the toolbar effect to match editor better + if let view = toolbarEffectView { + view.isHidden = reduceTransparency == true + view.alphaValue = effectiveAppearance.isDarkMode ? 0.3 : 0.7 + + // Blend the color of contents behind the window + view.blendingMode = .behindWindow + } + } +} diff --git a/MarkEditMac/Sources/Editor/EditorWindowController.swift b/MarkEditMac/Sources/Editor/EditorWindowController.swift new file mode 100644 index 00000000..26afd89f --- /dev/null +++ b/MarkEditMac/Sources/Editor/EditorWindowController.swift @@ -0,0 +1,28 @@ +// +// EditorWindowController.swift +// MarkEditMac +// +// Created by cyan on 12/12/22. + +import AppKit + +final class EditorWindowController: NSWindowController, NSWindowDelegate { + required init?(coder: NSCoder) { + super.init(coder: coder) + shouldCascadeWindows = true + } + + override func windowDidLoad() { + super.windowDidLoad() + window?.minSize = CGSize(width: 240, height: 0) + window?.backgroundColor = .controlBackgroundColor + } + + func windowDidBecomeMain(_ notification: Notification) { + NSApplication.shared.closeOpenPanels() + } + + func windowWillClose(_ notification: Notification) { + (contentViewController as? EditorViewController)?.clearEditor() + } +} diff --git a/MarkEditMac/Sources/Editor/Models/EditorDocument.swift b/MarkEditMac/Sources/Editor/Models/EditorDocument.swift new file mode 100644 index 00000000..fb0b703d --- /dev/null +++ b/MarkEditMac/Sources/Editor/Models/EditorDocument.swift @@ -0,0 +1,186 @@ +// +// EditorDocument.swift +// MarkEditMac +// +// Created by cyan on 12/12/22. + +import AppKit +import MarkEditKit + +/** + Main document used to deal with markdown files. + + https://developer.apple.com/documentation/appkit/nsdocument + */ +final class EditorDocument: NSDocument { + var fileData: Data? + var stringValue = "" + + var canUndo: Bool { + get async { + (try? await hostViewController?.bridge.history.canUndo()) ?? false + } + } + + var canRedo: Bool { + get async { + (try? await hostViewController?.bridge.history.canRedo()) ?? false + } + } + + var lineEndings: LineEndings? { + get async { + try? await hostViewController?.bridge.lineEndings.getLineEndings() + } + } + + private weak var hostViewController: EditorViewController? + + override func makeWindowControllers() { + let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) + let sceneIdentifier = NSStoryboard.SceneIdentifier("EditorWindowController") + + guard let windowController = storyboard.instantiateController(withIdentifier: sceneIdentifier) as? NSWindowController else { + return + } + + // Note hostViewController is a weak reference, it must be strongly retained first + let contentVC = EditorReusePool.shared.dequeueViewController() + windowController.contentViewController = contentVC + + hostViewController = contentVC + hostViewController?.representedObject = self + + let hasOpenPanels = NSApplication.shared.hasOpenPanels + NSApplication.shared.closeOpenPanels() + + // Return early, window creation in version browsing mode cannot be asynchronous + guard !NSDocumentController.shared.documents.contains(where: { $0.isBrowsingVersions }) else { + return addWindowController(windowController) + } + + // Return early, the line number glitch happens only when there're open panels + guard hasOpenPanels else { + return addWindowController(windowController) + } + + // Dirty trick, show the window later to wait CodeMirror finishes its initial layout, + // the line number height is not initially correct because of the window animation, + // we skip showing the window in makeWindowControllers and wait the final layout. + DispatchQueue.afterDelay(seconds: 0.02) { + self.hostViewController?.view.needsLayout = true + self.addWindowController(windowController) + self.showWindows() + } + } +} + +// MARK: - Overridden + +extension EditorDocument { + override class var autosavesInPlace: Bool { + true + } + + override func canAsynchronouslyWrite(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) -> Bool { + true + } + + override class func canConcurrentlyReadDocuments(ofType type: String) -> Bool { + true + } + + // MARK: - Reading and Writing + + override func read(from data: Data, ofType typeName: String) throws { + let encoding = AppPreferences.General.defaultTextEncoding + let newValue = encoding.decode(data: data) ?? data.toString() ?? "" + guard stringValue != newValue else { + return + } + + fileData = data + stringValue = newValue + hostViewController?.representedObject = self + } + + // We don't have a sync way to get the text, override save and autosave to do an async approach. + // + // Note that, by only overriding the "saveToURL" method can bring hang issues. + override func save(_ sender: Any?) { + Task { + await saveAsynchronously { + super.save(sender) + } + } + } + + override func autosave(withImplicitCancellability implicitlyCancellable: Bool) async throws { + await saveAsynchronously { + Task { + try await super.autosave(withImplicitCancellability: implicitlyCancellable) + } + } + } + + override func data(ofType typeName: String) throws -> Data { + let encoding = AppPreferences.General.defaultTextEncoding + return encoding.encode(string: stringValue) ?? stringValue.toData() ?? Data() + } + + override func presentedItemDidChange() { + guard let fileURL, let fileType else { + return + } + + // Only under certain conditions we need this flow, + // e.g., editing in VS Code won't trigger the regular data(ofType...) reload + DispatchQueue.onMainThread { + do { + if let modificationDate = try FileManager.default.attributesOfItem(atPath: fileURL.path)[.modificationDate] as? Date, modificationDate > (self.fileModificationDate ?? .distantPast) { + try self.revert(toContentsOf: fileURL, ofType: fileType) + } + } catch { + Logger.log(.error, error.localizedDescription) + } + } + } +} + +// MARK: - Printing + +extension EditorDocument { + @IBAction override func printDocument(_ sender: Any?) { + guard let window = hostViewController?.view.window else { + return + } + + // Ideally we should be able to print WKWebView, + // but it doesn't work very well even on Ventura. + // + // For now let's just print plain text, + // we don't expect printing to be used a lot. + let textView = NSTextView(frame: CGRect(origin: .zero, size: printInfo.paperSize)) + textView.string = stringValue + + let operation = NSPrintOperation(view: textView) + operation.runModal(for: window, delegate: nil, didRun: nil, contextInfo: nil) + } +} + +// MARK: - Private + +private extension EditorDocument { + func saveAsynchronously(saveAction: () -> Void) async { + guard let editorText = await hostViewController?.editorText else { + return + } + + stringValue = editorText + saveAction() + + // The editor is no longer dirty because changes are saved + hostViewController?.markEditorDirty(false) + unblockUserInteraction() + } +} diff --git a/MarkEditMac/Sources/Editor/Models/EditorReusePool.swift b/MarkEditMac/Sources/Editor/Models/EditorReusePool.swift new file mode 100644 index 00000000..da4c07b7 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Models/EditorReusePool.swift @@ -0,0 +1,50 @@ +// +// EditorReusePool.swift +// MarkEditMac +// +// Created by cyan on 12/15/22. +// + +import AppKit +import WebKit + +/** + Reuse pool for editors to keep WebViews in memory. + */ +final class EditorReusePool { + static let shared = EditorReusePool() + let processPool = WKProcessPool() + + func warmUp() { + // The theory here is that loading resources from WKWebViews is expensive, + // we make a pool that always keeps two instances in memory, + // if users open more than two editors, it's expected to be slower. + controllerPool.append(contentsOf: [ + EditorViewController(), + EditorViewController(), + ]) + } + + func dequeueViewController() -> EditorViewController { + if let reusable = controllerPool.first(where: { $0.view.window == nil }) { + return reusable + } + + return EditorViewController() + } + + /// All editors, whether with or without a visible window + func viewControllers() -> [EditorViewController] { + controllerPool + { + let windows = NSApplication.shared.windows.compactMap { $0 as? EditorWindow } + let controllers = windows.compactMap { $0.contentViewController as? EditorViewController } + return controllers.filter { !controllerPool.contains($0) } + }() + } + + // MARK: - Private + + private var controllerPool = [EditorViewController]() + + private init() {} +} diff --git a/MarkEditMac/Sources/Editor/Models/EditorToolbarItems.swift b/MarkEditMac/Sources/Editor/Models/EditorToolbarItems.swift new file mode 100644 index 00000000..0f564248 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Models/EditorToolbarItems.swift @@ -0,0 +1,155 @@ +// +// EditorToolbarItems.swift +// MarkEditMac +// +// Created by cyan on 1/13/23. +// + +import AppKit +import MarkEditKit + +extension NSToolbarItem { + static func with(identifier: NSToolbarItem.Identifier, menu: NSMenu?) -> NSMenuToolbarItem { + let item = NSMenuToolbarItem(itemIdentifier: identifier) + item.label = identifier.itemLabel + item.image = NSImage(systemSymbolName: identifier.itemIcon, accessibilityDescription: item.label) + + if let menu { + item.menu = menu + } else { + Logger.log(.error, "Missing menu for NSMenuToolbarItem") + } + + return item + } + + static func with(identifier: NSToolbarItem.Identifier, iconSize: Double? = nil, action: @escaping () -> Void) -> NSToolbarItem { + let item = NSToolbarItem(itemIdentifier: identifier) + item.label = identifier.itemLabel + + if let iconSize { + item.image = .with( + symbolName: identifier.itemIcon, + pointSize: iconSize, + accessibilityLabel: item.label + ) + } else { + item.image = NSImage(systemSymbolName: identifier.itemIcon, accessibilityDescription: item.label) + } + + item.addAction(action) + return item + } + + /// Used in toolTip as a hint, values should match mainMenu + var shortcutHint: String? { + switch itemIdentifier { + case .toggleBold: return "⌘ B" + case .toggleItalic: return "⌘ I" + case .toggleStrikethrough: return "⌃ ⌘ S" + case .insertLink: return "⌘ K" + case .insertImage: return "⌃ ⌘ K" + default: return nil + } + } +} + +extension NSToolbarItem.Identifier { + static let tableOfContents = newItem("tableOfContents") + static let formatHeaders = newItem("formatHeaders") + static let toggleBold = newItem("toggleBold") + static let toggleItalic = newItem("toggleItalic") + static let toggleStrikethrough = newItem("toggleStrikethrough") + static let insertLink = newItem("insertLink") + static let insertImage = newItem("insertImage") + static let toggleList = newItem("toggleList") + static let toggleBlockquote = newItem("toggleBlockquote") + static let horizontalRule = newItem("horizontalRule") + static let insertTable = newItem("insertTable") + static let insertCode = newItem("insertCode") + static let textFormat = newItem("textFormat") + static let shareDocument = newItem("shareDocument") + static let copyPandocCommand = newItem("copyPandocCommand") + + static var defaultItems: [NSToolbarItem.Identifier] { + [ + .tableOfContents, + .formatHeaders, + .toggleBold, + .toggleItalic, + .toggleList, + ] + } + + static var allItems: [NSToolbarItem.Identifier] { + [ + .tableOfContents, + .formatHeaders, + .toggleBold, + .toggleItalic, + .toggleStrikethrough, + .insertLink, + .insertImage, + .toggleList, + .toggleBlockquote, + .horizontalRule, + .insertTable, + .insertCode, + .textFormat, + .shareDocument, + .copyPandocCommand, + .space, + .flexibleSpace, + ] + } +} + +// MARK: - Private + +private extension NSToolbarItem.Identifier { + static func newItem(_ identifier: String) -> Self { + Self("app.markedit.editor.\(identifier)") + } + + var itemLabel: String { + switch self { + case .tableOfContents: return Localized.Toolbar.tableOfContents + case .formatHeaders: return Localized.Toolbar.formatHeaders + case .toggleBold: return Localized.Toolbar.toggleBold + case .toggleItalic: return Localized.Toolbar.toggleItalic + case .toggleStrikethrough: return Localized.Toolbar.toggleStrikethrough + case .insertLink: return Localized.Toolbar.insertLink + case .insertImage: return Localized.Toolbar.insertImage + case .toggleList: return Localized.Toolbar.toggleList + case .toggleBlockquote: return Localized.Toolbar.toggleBlockquote + case .horizontalRule: return Localized.Toolbar.horizontalRule + case .insertTable: return Localized.Toolbar.insertTable + case .insertCode: return Localized.Toolbar.insertCode + case .textFormat: return Localized.Toolbar.textFormat + case .shareDocument: return Localized.Toolbar.shareDocument + case .copyPandocCommand: return Localized.Toolbar.copyPandocCommand + default: fatalError("Unexpected toolbar item identifier: \(self)") + } + } + + var itemIcon: String { + switch self { + case .tableOfContents: return Icons.listBulletRectangle + case .formatHeaders: return Icons.number + case .toggleBold: return Icons.bold + case .toggleItalic: return Icons.italic + case .toggleStrikethrough: return Icons.strikethrough + case .insertLink: return Icons.link + case .insertImage: return Icons.photo + case .toggleList: return Icons.listBullet + case .toggleBlockquote: return Icons.textQuote + case .horizontalRule: return Icons.squareSplit1x2 + case .insertTable: return Icons.tablecells + case .insertCode: return Icons.curlybracesSquare + case .textFormat: return Icons.textformat + case .shareDocument: return Icons.squareAndArrowUp + case .copyPandocCommand: return Icons.terminal + default: fatalError("Unexpected toolbar item identifier: \(self)") + } + } +} diff --git a/MarkEditMac/Sources/Editor/Views/EditorPanelView.swift b/MarkEditMac/Sources/Editor/Views/EditorPanelView.swift new file mode 100644 index 00000000..9dd36248 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Views/EditorPanelView.swift @@ -0,0 +1,24 @@ +// +// EditorPanelView.swift +// MarkEditMac +// +// Created by cyan on 12/27/22. +// + +import AppKit +import AppKitControls + +class EditorPanelView: NSView, BackgroundTheming { + init() { + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func resetCursorRects() { + addCursorRect(bounds, cursor: .arrow) + } +} diff --git a/MarkEditMac/Sources/Editor/Views/EditorStatusView.swift b/MarkEditMac/Sources/Editor/Views/EditorStatusView.swift new file mode 100644 index 00000000..b019f42a --- /dev/null +++ b/MarkEditMac/Sources/Editor/Views/EditorStatusView.swift @@ -0,0 +1,77 @@ +// +// EditorStatusView.swift +// MarkEditMac +// +// Created by cyan on 1/16/23. +// + +import AppKit +import AppKitControls +import MarkEditKit + +/** + To indicate the current line, column and length of selection. + */ +final class EditorStatusView: NSView, BackgroundTheming { + private let button = TitleOnlyButton(fontSize: 11) + + init(handler: @escaping () -> Void) { + super.init(frame: .zero) + self.toolTip = Localized.Document.gotoLine + + button.addAction(handler) + addSubview(button) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + button.frame = bounds + } + + override func updateLayer() { + layer?.borderWidth = 1 + layer?.cornerRadius = 3 + layer?.cornerCurve = .continuous + layer?.borderColor = NSColor.plainButtonBorder.cgColor + } + + func updateLineColumn(_ info: LineColumnInfo) { + let title = { + // Don't localize the labels + let lineColumn = "Ln \(info.line), Col \(info.column)" + if info.length > 0 { + return "\(lineColumn) (\(info.length))" + } else { + return lineColumn + } + }() + + let label = button.labelView + label.stringValue = title + label.sizeToFit() + + self.frame = label.bounds.insetBy(dx: -4, dy: -2) + self.needsLayout = true + } +} + +// MARK: - Accessibility + +extension EditorStatusView { + override func isAccessibilityElement() -> Bool { + true + } + + override func accessibilityRole() -> NSAccessibility.Role? { + .button + } + + override func accessibilityLabel() -> String? { + button.labelView.stringValue + } +} diff --git a/MarkEditMac/Sources/Editor/Views/EditorWebView.swift b/MarkEditMac/Sources/Editor/Views/EditorWebView.swift new file mode 100644 index 00000000..b846ba37 --- /dev/null +++ b/MarkEditMac/Sources/Editor/Views/EditorWebView.swift @@ -0,0 +1,63 @@ +// +// EditorWebView.swift +// MarkEditMac +// +// Created by cyan on 12/16/22. +// + +import WebKit +import MarkEditKit + +enum EditorWebViewMenuAction { + case findSelection + case selectAllOccurrences +} + +protocol EditorWebViewMenuDelegate: AnyObject { + func editorWebView(_ sender: EditorWebView, didSelect menuAction: EditorWebViewMenuAction) +} + +/** + Lightweight wrapper for WKWebView used in editors. + */ +final class EditorWebView: WKWebView { + weak var menuDelegate: EditorWebViewMenuDelegate? + + override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/Shared/API/c/WKContextMenuItem.cpp + menu.items = menu.items.filter { item in + // Disable Font and Paragraph Direction + if item.submenu?.items.contains(where: { $0.tag == 41 || $0.tag == 52 }) == true { + return false + } + + return true + } + + menu.addItem(.separator()) + menu.addItem(withTitle: Localized.Search.findSelection, action: #selector(findSelection(_:))) + menu.addItem(withTitle: Localized.Search.selectAllOccurrences, action: #selector(selectAllOccurrences(_:))) + + menu.addItem({ + let item = NSMenuItem() + item.title = Localized.Toolbar.textFormat + item.submenu = NSApp.appDelegate?.textFormatMenu?.copiedMenu + return item + }()) + + menu.addItem(.separator()) + super.willOpenMenu(menu, with: event) + } +} + +// MARK: - Private + +private extension EditorWebView { + @objc func findSelection(_ sender: NSMenuItem) { + menuDelegate?.editorWebView(self, didSelect: .findSelection) + } + + @objc func selectAllOccurrences(_ sender: NSMenuItem) { + menuDelegate?.editorWebView(self, didSelect: .selectAllOccurrences) + } +} diff --git a/MarkEditMac/Sources/Extensions/NSApplication+Extension.swift b/MarkEditMac/Sources/Extensions/NSApplication+Extension.swift new file mode 100644 index 00000000..b1362568 --- /dev/null +++ b/MarkEditMac/Sources/Extensions/NSApplication+Extension.swift @@ -0,0 +1,20 @@ +// +// NSApplication+Extension.swift +// MarkEditMac +// +// Created by cyan on 12/13/22. +// + +import AppKit +import MarkEditKit + +extension NSApplication { + var appDelegate: AppDelegate? { + guard let delegate = delegate as? AppDelegate else { + Logger.assert(delegate != nil, "Expected to get AppDelegate") + return nil + } + + return delegate + } +} diff --git a/MarkEditMac/Sources/Main/AppPreferences.swift b/MarkEditMac/Sources/Main/AppPreferences.swift new file mode 100644 index 00000000..37ccc5bb --- /dev/null +++ b/MarkEditMac/Sources/Main/AppPreferences.swift @@ -0,0 +1,301 @@ +// +// AppPreferences.swift +// MarkEditMac +// +// Created by cyan on 12/25/22. +// + +import AppKit +import MarkEditCore +import MarkEditKit +import FontPicker + +/** + UserDefaults wrapper with handy getters and setters. + */ +enum AppPreferences { + enum General { + @Storage(key: "general.appearance", defaultValue: .system) + static var appearance: Appearance + + @Storage(key: "general.new-window-behavior", defaultValue: .openDocument) + static var newWindowBehavior: NewWindowBehavior + + @Storage(key: "general.default-text-encoding", defaultValue: .utf8) + static var defaultTextEncoding: EditorTextEncoding + + @Storage(key: "general.default-line-endings", defaultValue: .lf) + static var defaultLineEndings: LineEndings { + didSet { + performUpdates { $0.setDefaultLineBreak(defaultLineEndings.characters) } + } + } + } + + enum Editor { + @Storage(key: "editor.light-theme", defaultValue: AppTheme.GitHubLight.editorTheme) + static var lightTheme: String { + didSet { + AppTheme.current.updateAppearance() + } + } + + @Storage(key: "editor.dark-theme", defaultValue: AppTheme.GitHubDark.editorTheme) + static var darkTheme: String { + didSet { + AppTheme.current.updateAppearance() + } + } + + @Storage(key: "editor.font-style", defaultValue: .systemMono) + static var fontStyle: FontStyle { + didSet { + performUpdates { $0.setFontFamily(fontStyle.cssFontFamily) } + } + } + + @Storage(key: "editor.font-size", defaultValue: FontPicker.defaultFontSize) + static var fontSize: Double { + didSet { + performUpdates { $0.setFontSize(fontSize) } + } + } + + @Storage(key: "editor.show-line-numbers", defaultValue: true) + static var showLineNumbers: Bool { + didSet { + performUpdates { $0.setShowLineNumbers(enabled: showLineNumbers) } + } + } + + @Storage(key: "editor.show-active-line-indicator", defaultValue: true) + static var showActiveLineIndicator: Bool { + didSet { + performUpdates { $0.setShowActiveLineIndicator(enabled: showActiveLineIndicator) } + } + } + + @Storage(key: "editor.show-invisibles", defaultValue: true) + static var showInvisibles: Bool { + didSet { + performUpdates { $0.setShowInvisibles(enabled: showInvisibles) } + } + } + + @Storage(key: "editor.show-selection-status", defaultValue: true) + static var showSelectionStatus: Bool { + didSet { + performUpdates { $0.setShowSelectionStatus(enabled: showSelectionStatus) } + } + } + + @Storage(key: "editor.typewriter-mode", defaultValue: false) + static var typewriterMode: Bool { + didSet { + performUpdates { $0.setTypewriterMode(enabled: typewriterMode) } + } + } + + @Storage(key: "editor.focus-mode", defaultValue: false) + static var focusMode: Bool { + didSet { + performUpdates { $0.setFocusMode(enabled: focusMode) } + } + } + + @Storage(key: "editor.line-wrapping", defaultValue: true) + static var lineWrapping: Bool { + didSet { + performUpdates { $0.setLineWrapping(enabled: lineWrapping) } + } + } + + @Storage(key: "editor.line-height", defaultValue: .normal) + static var lineHeight: LineHeight { + didSet { + performUpdates { $0.setLineHeight(lineHeight.multiplier) } + } + } + + @Storage(key: "editor.tab-key-behavior", defaultValue: .insertTab) + static var tabKeyBehavior: TabKeyBehavior { + didSet { + performUpdates { $0.setTabKeyBehavior(tabKeyBehavior) } + } + } + + @Storage(key: "editor.indent-unit", defaultValue: .twoSpaces) + static var indentUnit: IndentUnit { + didSet { + performUpdates { $0.setIndentUnit(indentUnit) } + } + } + } + + enum Search { + @Storage(key: "search.case-sensitive", defaultValue: false) + static var caseSensitive: Bool + + @Storage(key: "search.whole-word", defaultValue: false) + static var wholeWord: Bool + + @Storage(key: "search.literal-search", defaultValue: false) + static var literalSearch: Bool + + @Storage(key: "search.regular-expression", defaultValue: false) + static var regularExpression: Bool + } + + enum Window { + @Storage(key: "window.toolbar-mode", defaultValue: .normal) + static var toolbarMode: ToolbarMode { + didSet { + performUpdates { ($0.view.window as? EditorWindow)?.toolbarMode = toolbarMode } + } + } + + @Storage(key: "window.tabbing-mode", defaultValue: .automatic) + static var tabbingMode: NSWindow.TabbingMode { + didSet { + performUpdates { $0.view.window?.tabbingMode = tabbingMode } + } + } + + @Storage(key: "window.reduce-transparency", defaultValue: false) + static var reduceTransparency: Bool { + didSet { + performUpdates { ($0.view.window as? EditorWindow)?.reduceTransparency = reduceTransparency } + } + } + } +} + +extension AppPreferences { + static var editorConfig: EditorConfig { + EditorConfig( + text: "", + theme: AppTheme.current.editorTheme, + fontFamily: Editor.fontStyle.cssFontFamily, + fontSize: Editor.fontSize, + showLineNumbers: Editor.showLineNumbers, + showActiveLineIndicator: Editor.showActiveLineIndicator, + showInvisibles: Editor.showInvisibles, + typewriterMode: Editor.typewriterMode, + focusMode: Editor.focusMode, + lineWrapping: Editor.lineWrapping, + lineHeight: Editor.lineHeight.multiplier, + defaultLineBreak: General.defaultLineEndings.characters, + tabKeyBehavior: Editor.tabKeyBehavior.rawValue, + indentUnit: Editor.indentUnit.characters, + localizable: EditorLocalizable.main + ) + } +} + +// MARK: - Types + +enum Appearance: Codable { + case system + case light + case dark + + func resolved(with appearance: NSAppearance = NSApp.effectiveAppearance) -> NSAppearance? { + switch self { + case .system: + return nil + case .light: + return NSAppearance(named: appearance.resolvedName(isDarkMode: false)) + case .dark: + return NSAppearance(named: appearance.resolvedName(isDarkMode: true)) + } + } +} + +enum IndentUnit: Codable { + case twoSpaces + case fourSpaces + case oneTab + case twoTabs + + var characters: String { + switch self { + case .twoSpaces: + return " " + case .fourSpaces: + return " " + case .oneTab: + return "\t" + case .twoTabs: + return "\t\t" + } + } +} + +enum LineHeight: Codable { + case tight + case normal + case relaxed + + var multiplier: Double { + switch self { + case .tight: + return 1.2 + case .normal: + return 1.5 + case .relaxed: + return 1.8 + } + } +} + +enum NewWindowBehavior: Codable { + case openDocument + case newDocument +} + +enum ToolbarMode: Codable { + case normal + case compact + case hidden +} + +extension NSWindow.TabbingMode: Codable {} + +// MARK: - Private + +private extension AppPreferences { + static func performUpdates(action: (EditorViewController) -> Void) { + EditorReusePool.shared.viewControllers().forEach { action($0) } + } +} + +@propertyWrapper +struct Storage { + private let key: String + private let defaultValue: T + + init(key: String, defaultValue: T) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: T { + get { + guard let data = UserDefaults.standard.object(forKey: key) as? Data else { + return defaultValue + } + + let value = try? Coders.decoder.decode(T.self, from: data) + return value ?? defaultValue + } + set { + let data = try? Coders.encoder.encode(newValue) + UserDefaults.standard.set(data, forKey: key) + } + } +} + +private enum Coders { + static let encoder = JSONEncoder() + static let decoder = JSONDecoder() +} diff --git a/MarkEditMac/Sources/Main/AppResources.swift b/MarkEditMac/Sources/Main/AppResources.swift new file mode 100644 index 00000000..4ed9a84f --- /dev/null +++ b/MarkEditMac/Sources/Main/AppResources.swift @@ -0,0 +1,178 @@ +// +// AppResources.swift +// MarkEditMac +// +// Created by cyan on 12/31/22. +// + +import Foundation +import MarkEditCore + +// To make localization work, always use String(localized:comment:) directly and add to this file. +// +// Besides, we use xcloc files to do the translation work: +// https://developer.apple.com/documentation/xcode/exporting-localizations +enum Localized { + enum General { + static let done = String(localized: "Done", comment: "Button title, confirm an action") + static let previous = String(localized: "Previous", comment: "Button title, move to the previous item") + static let next = String(localized: "Next", comment: "Button title, move to the next item") + static let all = String(localized: "All", comment: "Button title, perform actions to all items") + } + + enum Editor { + static let controlCharacter = String(localized: "Control Character", comment: "Phrase used in CodeMirror to indicate control character") + static let foldedLines = String(localized: "Folded Lines", comment: "Phrase used in CodeMirror to indicate folded lines") + static let unfoldedLines = String(localized: "Unfolded Lines", comment: "Phrase used in CodeMirror to indicate unfolded lines") + static let foldedCode = String(localized: "Folded Code", comment: "Phrase used in CodeMirror to indicated folded code") + static let unfold = String(localized: "Unfold", comment: "Phrase used in CodeMirror to unfold a piece of text") + static let foldLine = String(localized: "Fold Line", comment: "Phrase used in CodeMirror fold a line") + static let unfoldLine = String(localized: "Unfold Line", comment: "Phrase used in CodeMirror to unfold a line") + static let defaultLinkTitle = String(localized: "title", comment: "Default title used for link insertion") + static let previewButtonTitle = String(localized: "preview", comment: "Button title for code preview") + static let tableColumnName = String(localized: "Column", comment: "Column name for table creation") + static let tableItemName = String(localized: "Item", comment: "Item name for table creation") + } + + enum Toolbar { + static let tableOfContents = String(localized: "Table of Contents", comment: "Toolbar item to show table of contents") + static let formatHeaders = String(localized: "Headers", comment: "Toolbar item to toggle heading levels") + static let toggleBold = String(localized: "Bold", comment: "Toolbar item to toggle bold") + static let toggleItalic = String(localized: "Italic", comment: "Toolbar item to toggle italic") + static let toggleStrikethrough = String(localized: "Strikethrough", comment: "Toolbar item to toggle strikethrough") + static let insertLink = String(localized: "Insert Link", comment: "Toolbar item to insert link") + static let insertImage = String(localized: "Insert Image", comment: "Toolbar item to insert image") + static let toggleList = String(localized: "Toggle List", comment: "Toolbar item to toggle bullet list") + static let toggleBlockquote = String(localized: "Quote", comment: "Toolbar item to toggle blockquote") + static let horizontalRule = String(localized: "Horizontal Rule", comment: "Toolbar item to insert horizontal rule") + static let insertTable = String(localized: "Table", comment: "Toolbar item to insert table") + static let insertCode = String(localized: "Insert Code", comment: "Toolbar item to insert code") + static let textFormat = String(localized: "Text Format", comment: "Toolbar item to use text format menu") + static let shareDocument = String(localized: "Share this document", comment: "Toolbar item to share the document") + static let copyPandocCommand = String(localized: "Copy Pandoc Command", comment: "Toolbar item to copy pandoc command") + } + + enum Search { + static let find = String(localized: "Find", comment: "Find mode in search menu") + static let replace = String(localized: "Replace", comment: "Replace mode in search menu") + static let caseSensitive = String(localized: "Case Sensitive", comment: "Toggle case sensitive search") + static let wholeWord = String(localized: "Whole Word", comment: "Toggle whole word search") + static let literalSearch = String(localized: "Literal Search", comment: "Toggle literal search") + static let regularExpression = String(localized: "Regular Expression", comment: "Toggle regular expression for search") + static let recentSearches = String(localized: "Recent Searches", comment: "Menu item: recent searches") + static let clearRecents = String(localized: "Clear Recents", comment: "Menu item: clear recents") + static let findSelection = String(localized: "Find Selection", comment: "Menu item: use selection to find") + static let selectAllOccurrences = String(localized: "Select All Occurrences", comment: "Menu item: select all occurrences") + } + + enum Document { + static let openDocument = String(localized: "Open Document", comment: "Menu item: open an existing document") + static let newDocument = String(localized: "New Document", comment: "Menu item: create a new document") + static let gotoLine = String(localized: "Go to Line", comment: "Placeholder text for goto line window") + } + + enum Settings { + // Editor + static let editor = String(localized: "Editor", comment: "Window title for editor settings") + static let font = String(localized: "Font:", comment: "Label for font settings") + static let selectFont = String(localized: "Select...", comment: "Menu label for selecting fonts") + static let systemDefault = String(localized: "System Default", comment: "System default font name") + static let systemMono = String(localized: "System Mono", comment: "System mono font name") + static let systemRounded = String(localized: "System Rounded", comment: "System rounded font name") + static let systemSerif = String(localized: "System Serif", comment: "System serif font name") + static let openFontPanel = String(localized: "Open Font Panel...", comment: "Menu item for selecting custom fonts") + static let lightTheme = String(localized: "Light Theme:", comment: "Light theme for the editor") + static let darkTheme = String(localized: "Dark Theme:", comment: "Dark theme for the editor") + static let displayOptions = String(localized: "Show:", comment: "Label for display options") + static let lineNumbers = String(localized: "Line numbers", comment: "Option to show line numbers") + static let activeLineIndicator = String(localized: "Active line indicator", comment: "Option to show active line indicator") + static let invisibleCharacters = String(localized: "Invisible characters", comment: "Option to show invisible characters") + static let selectionStatus = String(localized: "Selection status", comment: "Option to show selection status") + static let editBehavior = String(localized: "Edit Behavior:", comment: "Editor behavior like focus mode and typewriter mode") + static let typewriterModeTitle = String(localized: "Keep caret in the middle", comment: "Explanation for typewriter mode") + static let focusModeTitle = String(localized: "Dim inactive lines", comment: "Explanation for focus mode") + static let lineWrappingLabel = String(localized: "Line Wrapping:", comment: "Label for line wrapping option") + static let lineWrappingDescription = String(localized: "Wrap lines to editor width", comment: "Explanation for line wrapping option") + static let lineHeight = String(localized: "Line Height:", comment: "Label for line height option") + static let tightHeight = String(localized: "Tight", comment: "Tight line spacing") + static let normalHeight = String(localized: "Normal", comment: "Normal line spacing") + static let relaxedHeight = String(localized: "Relaxed", comment: "Relaxed line spacing") + static let tabKeyBehavior = String(localized: "Tab Key:", comment: "Label for tab key behavior settings") + static let insertsTab = String(localized: "Inserts tab character", comment: "Default tab key behavior") + static let insertsTwoSpaces = String(localized: "Inserts 2 spaces", comment: "Press tab key to insert 2 spaces") + static let insertsFourSpaces = String(localized: "Inserts 4 spaces", comment: "Press tab key to insert 4 spaces") + static let indentUnit = String(localized: "Prefer Indent Using:", comment: "Label for indent unit settings") + static let twoSpaces = String(localized: "2 spaces", comment: "Use 2 spaces as the indent unit") + static let fourSpaces = String(localized: "4 spaces", comment: "Use 4 spaces as the indent unit") + static let oneTab = String(localized: "1 tab", comment: "Use 1 tab as the indent unit") + static let twoTabs = String(localized: "2 tabs", comment: "Use 2 tabs as the indent unit") + + // General + static let general = String(localized: "General", comment: "Window title for general settings") + static let appearance = String(localized: "Appearance:", comment: "Appearance for the app") + static let system = String(localized: "System", comment: "Follow the system appearance") + static let light = String(localized: "Light", comment: "Always use light mode for the app") + static let dark = String(localized: "Dark", comment: "Always use dark mode for the app") + static let newWindowBehavior = String(localized: "New Window Behavior:", comment: "Behavior when creating new windows") + static let defaultTextEncoding = String(localized: "Default Text Encoding:", comment: "Text encoding for opening and saving files") + static let defaultLineEndings = String(localized: "Default Line Endings:", comment: "Line endings for creating new files") + static let macOSLineEndings = String(localized: "macOS / Unix (LF)", comment: "Line endings used on macOS and Unix") + static let windowsLineEndings = String(localized: "Windows (CRLF)", comment: "Line endings used on Windows") + static let classicMacLineEndings = String(localized: "Classic Mac OS (CR)", comment: "Line endings used on Classic Mac OS") + + // Window + static let window = String(localized: "Window", comment: "Window title for window settings") + static let toolbarMode = String(localized: "Toolbar Mode:", comment: "Label for window toolbar mode") + static let normalMode = String(localized: "Normal", comment: "Normal mode for window toolbar") + static let compactMode = String(localized: "Compact", comment: "Compact mode for window toolbar") + static let hiddenMode = String(localized: "Hidden", comment: "Hidden mode for window toolbar") + static let tabbingMode = String(localized: "Tabbing Mode:", comment: "Label for window tabbing mode settings") + static let automatic = String(localized: "Automatic", comment: "Automatic window tabbing mode") + static let preferred = String(localized: "Preferred", comment: "Preferred window tabbing mode") + static let disallowed = String(localized: "Disallowed", comment: "Disallowed window tabbing mode") + static let reduceTransparencyLabel = String(localized: "Reduce Transparency:", comment: "Label for the option to reduce window transparency") + static let reduceTransparencyDescription = String(localized: "Remove the toolbar blur", comment: "Explanation for the option to reduce window transparency") + } +} + +// Icon set used in the app: https://developer.apple.com/sf-symbols/ +// +// Note: double check availability and deployment target before adding new icons +enum Icons { + static let arrowUturnBackwardCircle = "arrow.uturn.backward.circle" + static let bold = "bold" + static let characterCursorIbeam = "character.cursor.ibeam" + static let chevronLeft = "chevron.left" + static let chevronRight = "chevron.right" + static let curlybracesSquare = "curlybraces.square" + static let gearshape = "gearshape" + static let italic = "italic" + static let link = "link" + static let listBullet = "list.bullet" + static let listBulletRectangle = "list.bullet.rectangle" + static let macwindow = "macwindow" + static let number = "number" + static let photo = "photo" + static let squareAndArrowUp = "square.and.arrow.up" + static let squareSplit1x2 = "square.split.1x2" + static let strikethrough = "strikethrough" + static let tablecells = "tablecells" + static let terminal = "terminal" + static let textQuote = "text.quote" + static let textformat = "textformat" +} + +extension EditorLocalizable { + static var main: Self { + EditorLocalizable( + controlCharacter: Localized.Editor.controlCharacter, + foldedLines: Localized.Editor.foldedLines, + unfoldedLines: Localized.Editor.unfoldedLines, + foldedCode: Localized.Editor.foldedCode, + unfold: Localized.Editor.unfold, + foldLine: Localized.Editor.foldLine, + unfoldLine: Localized.Editor.unfoldLine, + previewButtonTitle: Localized.Editor.previewButtonTitle + ) + } +} diff --git a/MarkEditMac/Sources/Main/AppTheme.swift b/MarkEditMac/Sources/Main/AppTheme.swift new file mode 100644 index 00000000..e30f6b1b --- /dev/null +++ b/MarkEditMac/Sources/Main/AppTheme.swift @@ -0,0 +1,168 @@ +// +// AppTheme.swift +// MarkEditMac +// +// Created by cyan on 12/17/22. +// + +import AppKit + +struct AppTheme { + let isDark: Bool + let editorTheme: String + let windowBackground: NSColor // Pre-define colors to style the window for initial launch + + static var current: AppTheme { + NSApplication.shared.isDarkMode ? darkTheme : lightTheme + } + + static func withName(_ name: String) -> AppTheme { + allCases.first { $0.editorTheme == name } ?? GitHubLight + } + + /// Get a "resolved" appearance name based on the current effective appearance + var resolvedAppearance: NSAppearance? { + NSAppearance(named: NSApp.effectiveAppearance.resolvedName(isDarkMode: isDark)) + } + + /// Trigger theme update for all editors + func updateAppearance() { + EditorReusePool.shared.viewControllers().forEach { + $0.setTheme(self) + } + } +} + +// MARK: - Themes + +extension AppTheme: CaseIterable, Hashable, CustomStringConvertible { + static var allCases: [AppTheme] { + [ + GitHubLight, GitHubDark, + XcodeLight, XcodeDark, + Dracula, + Cobalt, + WinterIsComingLight, WinterIsComingDark, + MinimalLight, MinimalDark, + ] + } + + static var GitHubLight: Self { + Self( + isDark: false, + editorTheme: "github-light", + windowBackground: NSColor(hexCode: 0xffffff) + ) + } + + static var GitHubDark: Self { + Self( + isDark: true, + editorTheme: "github-dark", + windowBackground: NSColor(hexCode: 0x0d1116) + ) + } + + static var XcodeLight: Self { + Self( + isDark: false, + editorTheme: "xcode-light", + windowBackground: NSColor(hexCode: 0xffffff) + ) + } + + static var XcodeDark: Self { + Self( + isDark: true, + editorTheme: "xcode-dark", + windowBackground: NSColor(hexCode: 0x1f1f24) + ) + } + + static var Dracula: Self { + Self( + isDark: true, + editorTheme: "dracula", + windowBackground: NSColor(hexCode: 0x282a36) + ) + } + + static var Cobalt: Self { + Self( + isDark: true, + editorTheme: "cobalt", + windowBackground: NSColor(hexCode: 0x193549) + ) + } + + static var WinterIsComingLight: Self { + Self( + isDark: false, + editorTheme: "winter-is-coming-light", + windowBackground: NSColor(hexCode: 0xffffff) + ) + } + + static var WinterIsComingDark: Self { + Self( + isDark: true, + editorTheme: "winter-is-coming-dark", + windowBackground: NSColor(hexCode: 0x282822) + ) + } + + static var MinimalLight: Self { + Self( + isDark: false, + editorTheme: "minimal-light", + windowBackground: NSColor(hexCode: 0xffffff) + ) + } + + static var MinimalDark: Self { + Self( + isDark: true, + editorTheme: "minimal-dark", + windowBackground: NSColor(hexCode: 0x000000) + ) + } + + var description: String { + switch self { + case Self.GitHubLight: + return "GitHub (Light)" + case Self.GitHubDark: + return "GitHub (Dark)" + case Self.XcodeLight: + return "Xcode (Light)" + case Self.XcodeDark: + return "Xcode (Dark)" + case Self.Dracula: + return "Dracula" + case Self.Cobalt: + return "Cobalt" + case Self.WinterIsComingLight: + return "Winter is Coming (Light)" + case Self.WinterIsComingDark: + return "Winter is Coming (Dark)" + case Self.MinimalLight: + return "Minimal (Light)" + case Self.MinimalDark: + return "Minimal (Dark)" + default: + fatalError("Invalid theme was found") + } + } +} + +// MARK: - Private + +private extension AppTheme { + static var lightTheme: Self { + withName(AppPreferences.Editor.lightTheme) + } + + static var darkTheme: Self { + withName(AppPreferences.Editor.darkTheme) + } +} diff --git a/MarkEditMac/Sources/Main/Application/AppDelegate+Document.swift b/MarkEditMac/Sources/Main/Application/AppDelegate+Document.swift new file mode 100644 index 00000000..d8234e27 --- /dev/null +++ b/MarkEditMac/Sources/Main/Application/AppDelegate+Document.swift @@ -0,0 +1,38 @@ +// +// AppDelegate+Document.swift +// MarkEditMac +// +// Created by cyan on 1/15/23. +// + +import AppKit + +extension AppDelegate { + func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { + switch AppPreferences.General.newWindowBehavior { + case .openDocument: + sender.showOpenPanel() + return false + case .newDocument: + return true + } + } + + func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { + let menu = NSMenu() + + // Only show the secondary option based on the preference + switch AppPreferences.General.newWindowBehavior { + case .openDocument: + menu.addItem(withTitle: Localized.Document.newDocument) { + NSDocumentController.shared.newDocument(nil) + } + case .newDocument: + menu.addItem(withTitle: Localized.Document.openDocument) { + NSApplication.shared.showOpenPanel() + } + } + + return menu + } +} diff --git a/MarkEditMac/Sources/Main/Application/AppDelegate+Menu.swift b/MarkEditMac/Sources/Main/Application/AppDelegate+Menu.swift new file mode 100644 index 00000000..6da567ac --- /dev/null +++ b/MarkEditMac/Sources/Main/Application/AppDelegate+Menu.swift @@ -0,0 +1,109 @@ +// +// AppDelegate+Menu.swift +// MarkEditMac +// +// Created by cyan on 1/15/23. +// + +import AppKit +import MarkEditKit + +extension AppDelegate: NSMenuDelegate { + func menuNeedsUpdate(_ menu: NSMenu) { + switch menu { + case mainFileMenu: + let noDoc = activeDocument?.fileURL == nil + openFileInMenu?.superMenuItem?.isHidden = noDoc + reopenFileMenu?.superMenuItem?.isHidden = noDoc + lineEndingsMenu?.superMenuItem?.isHidden = noDoc + case mainEditMenu: + reconfigureMainEditMenu(document: activeDocument) + case openFileInMenu: + reconfigureOpenFileInMenu(document: activeDocument) + case reopenFileMenu: + reconfigureReopenFileMenu(document: activeDocument) + case lineEndingsMenu: + reconfigureLineEndingsMenu(document: activeDocument) + default: + break + } + } +} + +// MARK: - Private + +private extension AppDelegate { + var activeDocument: EditorDocument? { + (NSApp.mainWindow?.contentViewController as? EditorViewController)?.document + } + + func reconfigureOpenFileInMenu(document: EditorDocument?) { + openFileInMenu?.removeAllItems() + + // Disabled or not able to find the document, just leave the menu empty + guard let fileURL = document?.fileURL else { + return + } + + // Basically, we wouldn't expect to see "MarkEdit.app" + let appURLs = NSWorkspace.shared.urlsForApplications(toOpen: fileURL).filter { + $0.lastPathComponent != Bundle.main.bundleURL.lastPathComponent + } + + appURLs.forEach { appURL in + let item = openFileInMenu?.addItem(withTitle: appURL.localizedName) { + NSWorkspace.shared.open( + [fileURL], + withApplicationAt: appURL, + configuration: NSWorkspace.OpenConfiguration(), + completionHandler: nil + ) + } + + let icon = NSWorkspace.shared.icon(forFile: appURL.path) + item?.image = icon.resized(with: CGSize(width: 16, height: 16)) + } + } + + func reconfigureReopenFileMenu(document: EditorDocument?) { + reopenFileMenu?.removeAllItems() + + // Disabled or not able to find the document, just leave the menu empty + guard document?.fileURL != nil else { + return + } + + for encoding in EditorTextEncoding.allCases { + let item = reopenFileMenu?.addItem(withTitle: encoding.description, action: #selector(EditorViewController.reopenWithEncoding(_:))) + item?.representedObject = encoding + + if EditorTextEncoding.groupingCases.contains(encoding) { + reopenFileMenu?.addItem(.separator()) + } + } + } + + func reconfigureLineEndingsMenu(document: EditorDocument?) { + Task { @MainActor in + guard let lineEndings = await document?.lineEndings else { + return + } + + lineEndingsLFItem?.setOn(lineEndings == .lf) + lineEndingsCRLFItem?.setOn(lineEndings == .crlf) + lineEndingsCRItem?.setOn(lineEndings == .cr) + lineEndingsMenu?.reloadItems() + } + } + + func reconfigureMainEditMenu(document: EditorDocument?) { + Task { @MainActor in + guard let document else { + return + } + + editUndoItem?.isEnabled = await document.canUndo + editRedoItem?.isEnabled = await document.canRedo + } + } +} diff --git a/MarkEditMac/Sources/Main/Application/AppDelegate.swift b/MarkEditMac/Sources/Main/Application/AppDelegate.swift new file mode 100644 index 00000000..cd13e0a6 --- /dev/null +++ b/MarkEditMac/Sources/Main/Application/AppDelegate.swift @@ -0,0 +1,79 @@ +// +// AppDelegate.swift +// MarkEditMac +// +// Created by cyan on 12/12/22. + +import AppKit +import AppKitExtensions +import Proofing +import SettingsUI + +@NSApplicationMain +final class AppDelegate: NSObject, NSApplicationDelegate { + @IBOutlet weak var mainFileMenu: NSMenu? + @IBOutlet weak var mainEditMenu: NSMenu? + + @IBOutlet weak var openFileInMenu: NSMenu? + @IBOutlet weak var reopenFileMenu: NSMenu? + @IBOutlet weak var lineEndingsMenu: NSMenu? + @IBOutlet weak var textFormatMenu: NSMenu? + @IBOutlet weak var formatHeadersMenu: NSMenu? + @IBOutlet weak var copyPandocCommandMenu: NSMenu? + + @IBOutlet weak var formatBulletItem: NSMenuItem? + @IBOutlet weak var formatNumberingItem: NSMenuItem? + @IBOutlet weak var formatTodoItem: NSMenuItem? + @IBOutlet weak var formatCodeItem: NSMenuItem? + @IBOutlet weak var formatCodeBlockItem: NSMenuItem? + @IBOutlet weak var formatMathItem: NSMenuItem? + @IBOutlet weak var formatMathBlockItem: NSMenuItem? + + @IBOutlet weak var lineEndingsLFItem: NSMenuItem? + @IBOutlet weak var lineEndingsCRLFItem: NSMenuItem? + @IBOutlet weak var lineEndingsCRItem: NSMenuItem? + + @IBOutlet weak var editUndoItem: NSMenuItem? + @IBOutlet weak var editRedoItem: NSMenuItem? + + private var appearanceObservation: NSKeyValueObservation? + private var settingsWindowController: NSWindowController? + + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.appearance = AppPreferences.General.appearance.resolved() + appearanceObservation = NSApp.observe(\.effectiveAppearance) { _, _ in + AppTheme.current.updateAppearance() + } + + UserDefaults.overwriteTextCheckerOnce() + EditorReusePool.shared.warmUp() + + // Initialize this earlier instead of making it lazy, + // the window size relies on the SwiftUI content view size, it takes time. + settingsWindowController = SettingsRootViewController.withTabs([ + .editor, + .general, + .window, + ]) + } + + func application(_ application: NSApplication, open urls: [URL]) { + if let url = urls.first(where: { $0.host == Grammarly.shared.redirectHost }) { + Grammarly.shared.completeOAuth(url: url) + } + } +} + +// MARK: - Private + +private extension AppDelegate { + @IBAction func showPreferences(_ sender: Any?) { + settingsWindowController?.showWindow(self) + } + + @IBAction func showHelp(_ sender: Any?) { + if let url = URL(string: "https://github.com/MarkEdit-app/MarkEdit/wiki") { + NSWorkspace.shared.open(url) + } + } +} diff --git a/MarkEditMac/Sources/Panels/Find/EditorFindButtons.swift b/MarkEditMac/Sources/Panels/Find/EditorFindButtons.swift new file mode 100644 index 00000000..c8a4991c --- /dev/null +++ b/MarkEditMac/Sources/Panels/Find/EditorFindButtons.swift @@ -0,0 +1,22 @@ +// +// EditorFindButtons.swift +// MarkEditMac +// +// Created by cyan on 12/17/22. +// + +import AppKit +import AppKitControls + +final class EditorFindButtons: RoundedButtonGroup { + init(leftAction: @escaping (() -> Void), rightAction: @escaping (() -> Void)) { + let leftButton = IconOnlyButton(symbolName: Icons.chevronLeft, accessibilityLabel: Localized.General.previous) + leftButton.addAction(leftAction) + + let rightButton = IconOnlyButton(symbolName: Icons.chevronRight, accessibilityLabel: Localized.General.next) + rightButton.addAction(rightAction) + + super.init(leftButton: leftButton, rightButton: rightButton) + self.frame = CGRect(x: 0, y: 0, width: 72, height: 0) + } +} diff --git a/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Delegate.swift b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Delegate.swift new file mode 100644 index 00000000..159b426d --- /dev/null +++ b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Delegate.swift @@ -0,0 +1,31 @@ +// +// EditorFindPanel+Delegate.swift +// MarkEditMac +// +// Created by cyan on 12/25/22. +// + +import AppKit + +// MARK: - NSSearchFieldDelegate + +extension EditorFindPanel: NSSearchFieldDelegate { + func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool { + if selector == #selector(insertTab(_:)) && mode == .replace { + // Focus on the replace panel + delegate?.editorFindPanelDidPressTabKey(self) + return true + } else if selector == #selector(insertNewline(_:)) { + // Navigate between search results + if NSApplication.shared.shiftKeyIsPressed { + delegate?.editorFindPanelDidClickPrevious(self) + } else { + delegate?.editorFindPanelDidClickNext(self) + } + + return true + } + + return false + } +} diff --git a/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Menu.swift b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Menu.swift new file mode 100644 index 00000000..2a2b535b --- /dev/null +++ b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+Menu.swift @@ -0,0 +1,84 @@ +// +// EditorFindPanel+Menu.swift +// MarkEditMac +// +// Created by cyan on 12/25/22. +// + +import AppKit + +extension EditorFindPanel { + /// Reset the search menu, generally after search mode changed + func resetMenu() { + let menu = NSMenu() + menu.addItem(withTitle: Localized.Search.find, action: #selector(enableFindMode(_:))).setOn(mode != .replace) + menu.addItem(withTitle: Localized.Search.replace, action: #selector(enableReplaceMode(_:))).setOn(mode == .replace) + menu.addItem(.separator()) + + let caseItem = menu.addItem(withTitle: Localized.Search.caseSensitive, action: #selector(toggleCaseSensitive(_:))) + caseItem.setOn(AppPreferences.Search.caseSensitive) + + let wholeWordItem = menu.addItem(withTitle: Localized.Search.wholeWord, action: #selector(toggleWholeWord(_:))) + wholeWordItem.setOn(AppPreferences.Search.wholeWord) + + let literalItem = menu.addItem(withTitle: Localized.Search.literalSearch, action: #selector(toggleLiteralSearch(_:))) + literalItem.setOn(AppPreferences.Search.literalSearch) + menu.addItem(.separator()) + + let regexItem = menu.addItem(withTitle: Localized.Search.regularExpression, action: #selector(toggleRegularExpression(_:))) + regexItem.setOn(AppPreferences.Search.regularExpression) + menu.addItem(.separator()) + + let recentsTitleItem = menu.addItem(withTitle: Localized.Search.recentSearches) + recentsTitleItem.tag = NSSearchField.recentsTitleMenuItemTag + + // Just a placeholder with defined tag to help AppKit find the location + let recentsPlaceholderItem = menu.addItem(withTitle: "") + recentsPlaceholderItem.tag = NSSearchField.recentsMenuItemTag + menu.addItem(.separator()) + + let clearRecentsItem = menu.addItem(withTitle: Localized.Search.clearRecents) + clearRecentsItem.tag = NSSearchField.clearRecentsMenuItemTag + + searchField.recentsAutosaveName = "search.recents-autosaved" + searchField.maximumRecents = 5 + searchField.searchMenuTemplate = menu + } +} + +// MARK: - Private + +private extension EditorFindPanel { + @objc func enableFindMode(_ sender: NSMenuItem) { + delegate?.editorFindPanel(self, modeDidChange: .find) + } + + @objc func enableReplaceMode(_ sender: NSMenuItem) { + delegate?.editorFindPanel(self, modeDidChange: .replace) + } + + @objc func toggleCaseSensitive(_ sender: NSMenuItem) { + AppPreferences.Search.caseSensitive.toggle() + toggleMenuItem(sender) + } + + @objc func toggleWholeWord(_ sender: NSMenuItem) { + AppPreferences.Search.wholeWord.toggle() + toggleMenuItem(sender) + } + + @objc func toggleLiteralSearch(_ sender: NSMenuItem) { + AppPreferences.Search.literalSearch.toggle() + toggleMenuItem(sender) + } + + @objc func toggleRegularExpression(_ sender: NSMenuItem) { + AppPreferences.Search.regularExpression.toggle() + toggleMenuItem(sender) + } + + func toggleMenuItem(_ item: NSMenuItem) { + item.toggle() + delegate?.editorFindPanelDidChangeOptions(self) + } +} diff --git a/MarkEditMac/Sources/Panels/Find/EditorFindPanel+UI.swift b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+UI.swift new file mode 100644 index 00000000..8d5fa30e --- /dev/null +++ b/MarkEditMac/Sources/Panels/Find/EditorFindPanel+UI.swift @@ -0,0 +1,62 @@ +// +// EditorFindPanel+UI.swift +// MarkEditMac +// +// Created by cyan on 12/25/22. +// + +import AppKit + +extension EditorFindPanel { + private enum Constants { + static let panelHeight: Double = 36 + static let panelPadding: Double = 6 + } + + func setUp() { + frame = CGRect(x: 0, y: 0, width: 0, height: Constants.panelHeight) + alphaValue = 0 + resetMenu() + + searchField.placeholderString = Localized.Search.find + searchField.delegate = self + searchField.target = self + searchField.action = #selector(searchTermDidChange(_:)) + searchField.translatesAutoresizingMaskIntoConstraints = false + addSubview(searchField) + + findButtons.translatesAutoresizingMaskIntoConstraints = false + addSubview(findButtons) + + doneButton.target = self + doneButton.action = #selector(didClickDone(_:)) + doneButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(doneButton) + + NSLayoutConstraint.activate([ + doneButton.centerYAnchor.constraint(equalTo: centerYAnchor), + doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.panelPadding), + + findButtons.centerYAnchor.constraint(equalTo: centerYAnchor), + findButtons.heightAnchor.constraint(equalTo: doneButton.heightAnchor), + findButtons.widthAnchor.constraint(equalToConstant: findButtons.frame.width), + findButtons.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -Constants.panelPadding), + + searchField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.panelPadding), + searchField.trailingAnchor.constraint(equalTo: findButtons.leadingAnchor, constant: -Constants.panelPadding), + searchField.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } +} + +// MARK: - Private + +private extension EditorFindPanel { + @objc func searchTermDidChange(_ sender: NSTextField) { + delegate?.editorFindPanel(self, searchTermDidChange: sender.stringValue) + } + + @objc func didClickDone(_ sender: NSButton) { + delegate?.editorFindPanel(self, modeDidChange: .hidden) + } +} diff --git a/MarkEditMac/Sources/Panels/Find/EditorFindPanel.swift b/MarkEditMac/Sources/Panels/Find/EditorFindPanel.swift new file mode 100644 index 00000000..a3b20fe2 --- /dev/null +++ b/MarkEditMac/Sources/Panels/Find/EditorFindPanel.swift @@ -0,0 +1,67 @@ +// +// EditorFindPanel.swift +// MarkEditMac +// +// Created by cyan on 12/16/22. +// + +import AppKit +import AppKitControls + +@frozen enum EditorFindMode { + /// Find panel is not visible + case hidden + /// Find panel is visible, shows only find + case find + /// Find panel is visible, shows both find and replace + case replace +} + +protocol EditorFindPanelDelegate: AnyObject { + func editorFindPanel(_ sender: EditorFindPanel, modeDidChange mode: EditorFindMode) + func editorFindPanel(_ sender: EditorFindPanel, searchTermDidChange searchTerm: String) + func editorFindPanelDidChangeOptions(_ sender: EditorFindPanel) + func editorFindPanelDidPressTabKey(_ sender: EditorFindPanel) + func editorFindPanelDidClickNext(_ sender: EditorFindPanel) + func editorFindPanelDidClickPrevious(_ sender: EditorFindPanel) +} + +final class EditorFindPanel: EditorPanelView { + weak var delegate: EditorFindPanelDelegate? + var mode: EditorFindMode = .hidden + var numberOfItems: Int = 0 + let searchField = LabeledSearchField() + + private(set) lazy var findButtons = EditorFindButtons( + leftAction: { [weak self] in + guard let self else { return } + self.delegate?.editorFindPanelDidClickPrevious(self) + }, + rightAction: { [weak self] in + guard let self else { return } + self.delegate?.editorFindPanelDidClickNext(self) + } + ) + + private(set) lazy var doneButton = { + let button = NSButton() + button.title = Localized.General.done + button.bezelStyle = .roundRect + return button + }() + + override init() { + super.init() + setUp() + } +} + +// MARK: - Exposed Methods + +extension EditorFindPanel { + func updateResult(numberOfItems: Int, emptyInput: Bool) { + self.numberOfItems = numberOfItems + searchField.updateLabel(text: emptyInput ? "" : "\(numberOfItems)") + findButtons.isEnabled = numberOfItems > 0 + } +} diff --git a/MarkEditMac/Sources/Panels/Replace/EditorReplaceButtons.swift b/MarkEditMac/Sources/Panels/Replace/EditorReplaceButtons.swift new file mode 100644 index 00000000..26d1f043 --- /dev/null +++ b/MarkEditMac/Sources/Panels/Replace/EditorReplaceButtons.swift @@ -0,0 +1,21 @@ +// +// EditorReplaceButtons.swift +// MarkEditMac +// +// Created by cyan on 12/27/22. +// + +import AppKit +import AppKitControls + +final class EditorReplaceButtons: RoundedButtonGroup { + init(leftAction: @escaping (() -> Void), rightAction: @escaping (() -> Void)) { + let leftButton = TitleOnlyButton(title: Localized.Search.replace) + leftButton.addAction(leftAction) + + let rightButton = TitleOnlyButton(title: Localized.General.all) + rightButton.addAction(rightAction) + + super.init(leftButton: leftButton, rightButton: rightButton) + } +} diff --git a/MarkEditMac/Sources/Panels/Replace/EditorReplacePanel.swift b/MarkEditMac/Sources/Panels/Replace/EditorReplacePanel.swift new file mode 100644 index 00000000..3dab4d87 --- /dev/null +++ b/MarkEditMac/Sources/Panels/Replace/EditorReplacePanel.swift @@ -0,0 +1,99 @@ +// +// EditorReplacePanel.swift +// MarkEditMac +// +// Created by cyan on 12/26/22. +// + +import AppKit + +protocol EditorReplacePanelDelegate: AnyObject { + func editorReplacePanel(_ sender: EditorReplacePanel, replacementDidChange replacement: String) + func editorReplacePanelDidClickReplaceNext(_ sender: EditorReplacePanel) + func editorReplacePanelDidClickReplaceAll(_ sender: EditorReplacePanel) +} + +final class EditorReplacePanel: EditorPanelView { + weak var delegate: EditorReplacePanelDelegate? + + var layoutInfo: (textFieldFrame: CGRect, buttonHeight: CGFloat) = (.zero, 0) { + didSet { + needsLayout = true + } + } + + private(set) lazy var textField = { + let textField = NSTextField() + textField.placeholderString = Localized.Search.replace + textField.bezelStyle = .roundedBezel + textField.delegate = self + return textField + }() + + private lazy var replaceButtons = EditorReplaceButtons( + leftAction: { [weak self] in + guard let self else { return } + self.delegate?.editorReplacePanelDidClickReplaceNext(self) + }, + rightAction: { [weak self] in + guard let self else { return } + self.delegate?.editorReplacePanelDidClickReplaceAll(self) + } + ) + + override init() { + super.init() + alphaValue = 0 + isHidden = true + + addSubview(textField) + addSubview(replaceButtons) + } + + override func layout() { + super.layout() + let paddingX = layoutInfo.textFieldFrame.minX + let paddingY = layoutInfo.textFieldFrame.minY + + textField.frame = CGRect( + x: paddingX, + y: paddingY, + width: layoutInfo.textFieldFrame.width, + height: layoutInfo.textFieldFrame.height + ) + + replaceButtons.frame = CGRect( + x: textField.frame.maxX + paddingX, + y: paddingY + (layoutInfo.textFieldFrame.height - layoutInfo.buttonHeight) * 0.5, + width: frame.width - textField.frame.width - paddingX * 3, + height: layoutInfo.buttonHeight + ) + + mirrorImmediateSubviewsIfNeeded() + } +} + +// MARK: - Exposed Methods + +extension EditorReplacePanel { + func updateResult(numberOfItems: Int) { + replaceButtons.isEnabled = numberOfItems > 0 + } +} + +// MARK: - NSTextFieldDelegate + +extension EditorReplacePanel: NSTextFieldDelegate { + func controlTextDidChange(_ notification: Notification) { + delegate?.editorReplacePanel(self, replacementDidChange: textField.stringValue) + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool { + if selector == #selector(insertNewline(_:)) && replaceButtons.isEnabled { + delegate?.editorReplacePanelDidClickReplaceNext(self) + return true + } + + return false + } +} diff --git a/MarkEditMac/Sources/Settings/EditorSettingsView.swift b/MarkEditMac/Sources/Settings/EditorSettingsView.swift new file mode 100644 index 00000000..efe558e4 --- /dev/null +++ b/MarkEditMac/Sources/Settings/EditorSettingsView.swift @@ -0,0 +1,181 @@ +// +// EditorSettingsView.swift +// MarkEditMac +// +// Created by cyan on 1/26/23. +// + +import AppKit +import SwiftUI +import FontPicker +import SettingsUI +import MarkEditKit + +struct EditorSettingsView: View { + @State private var lightTheme = AppPreferences.Editor.lightTheme + @State private var darkTheme = AppPreferences.Editor.darkTheme + @State private var showLineNumbers = AppPreferences.Editor.showLineNumbers + @State private var showActiveLineIndicator = AppPreferences.Editor.showActiveLineIndicator + @State private var showInvisibles = AppPreferences.Editor.showInvisibles + @State private var showSelectionStatus = AppPreferences.Editor.showSelectionStatus + @State private var typewriterMode = AppPreferences.Editor.typewriterMode + @State private var focusMode = AppPreferences.Editor.focusMode + @State private var lineWrapping = AppPreferences.Editor.lineWrapping + @State private var lineHeight = AppPreferences.Editor.lineHeight + @State private var tabKeyBehavior = AppPreferences.Editor.tabKeyBehavior + @State private var indentUnit = AppPreferences.Editor.indentUnit + + var body: some View { + VStack(spacing: 0) { + FontPicker(configuration: fontPickerConfiguration, handlers: fontPickerHandlers) + .formLabel(Localized.Settings.font) + + Divider() + + SettingsForm { + Section { + Picker(Localized.Settings.lightTheme, selection: $lightTheme) { + ForEach(AppTheme.allCases, id: \.self) { + Text($0.description).tag($0.editorTheme) + } + } + .onChange(of: lightTheme) { + AppPreferences.Editor.lightTheme = $0 + } + .formMenuPicker() + + Picker(Localized.Settings.darkTheme, selection: $darkTheme) { + ForEach(AppTheme.allCases, id: \.self) { + Text($0.description).tag($0.editorTheme) + } + } + .onChange(of: darkTheme) { + AppPreferences.Editor.darkTheme = $0 + } + .formMenuPicker() + } + + Section { + VStack(alignment: .leading) { + Toggle(isOn: $showLineNumbers) { + Text(Localized.Settings.lineNumbers) + } + .onChange(of: showLineNumbers) { + AppPreferences.Editor.showLineNumbers = $0 + } + + Toggle(isOn: $showActiveLineIndicator) { + Text(Localized.Settings.activeLineIndicator) + } + .onChange(of: showActiveLineIndicator) { + AppPreferences.Editor.showActiveLineIndicator = $0 + } + + Toggle(isOn: $showInvisibles) { + Text(Localized.Settings.invisibleCharacters) + } + .onChange(of: showInvisibles) { + AppPreferences.Editor.showInvisibles = $0 + } + + Toggle(isOn: $showSelectionStatus) { + Text(Localized.Settings.selectionStatus) + } + .onChange(of: showSelectionStatus) { + AppPreferences.Editor.showSelectionStatus = $0 + } + } + .formLabel(alignment: .top, Localized.Settings.displayOptions) + } + + Section { + VStack(alignment: .leading) { + Toggle(isOn: $typewriterMode) { + Text(Localized.Settings.typewriterModeTitle) + } + .onChange(of: typewriterMode) { + AppPreferences.Editor.typewriterMode = $0 + } + + Toggle(isOn: $focusMode) { + Text(Localized.Settings.focusModeTitle) + } + .onChange(of: focusMode) { + AppPreferences.Editor.focusMode = $0 + } + } + .formLabel(alignment: .top, Localized.Settings.editBehavior) + + Toggle(isOn: $lineWrapping) { + Text(Localized.Settings.lineWrappingDescription) + } + .onChange(of: lineWrapping) { + AppPreferences.Editor.lineWrapping = $0 + } + .formLabel(Localized.Settings.lineWrappingLabel) + + Picker(Localized.Settings.lineHeight, selection: $lineHeight) { + Text(Localized.Settings.tightHeight).tag(LineHeight.tight) + Text(Localized.Settings.normalHeight).tag(LineHeight.normal) + Text(Localized.Settings.relaxedHeight).tag(LineHeight.relaxed) + } + .onChange(of: lineHeight) { + AppPreferences.Editor.lineHeight = $0 + } + .formHorizontalRadio() + } + + Section { + Picker(Localized.Settings.tabKeyBehavior, selection: $tabKeyBehavior) { + Text(Localized.Settings.insertsTab).tag(TabKeyBehavior.insertTab) + Text(Localized.Settings.insertsTwoSpaces).tag(TabKeyBehavior.insertTwoSpaces) + Text(Localized.Settings.insertsFourSpaces).tag(TabKeyBehavior.insertFourSpaces) + } + .onChange(of: tabKeyBehavior) { + AppPreferences.Editor.tabKeyBehavior = $0 + } + .formMenuPicker() + + Picker(Localized.Settings.indentUnit, selection: $indentUnit) { + Text(Localized.Settings.twoSpaces).tag(IndentUnit.twoSpaces) + Text(Localized.Settings.fourSpaces).tag(IndentUnit.fourSpaces) + Text(Localized.Settings.oneTab).tag(IndentUnit.oneTab) + Text(Localized.Settings.twoTabs).tag(IndentUnit.twoTabs) + } + .onChange(of: indentUnit) { + AppPreferences.Editor.indentUnit = $0 + } + .formMenuPicker() + } + } + } + } +} + +// MARK: - Private + +private extension EditorSettingsView { + var fontPickerConfiguration: FontPickerConfiguration { + FontPickerConfiguration( + selectedFontStyle: AppPreferences.Editor.fontStyle, + selectedFontSize: AppPreferences.Editor.fontSize, + selectButtonTitle: Localized.Settings.selectFont, + openPanelButtonTitle: Localized.Settings.openFontPanel, + defaultFontName: Localized.Settings.systemDefault, + monoFontName: Localized.Settings.systemMono, + roundedFontName: Localized.Settings.systemRounded, + serifFontName: Localized.Settings.systemSerif + ) + } + + var fontPickerHandlers: FontPickerHandlers { + FontPickerHandlers( + fontStyleDidChange: { fontStyle in + AppPreferences.Editor.fontStyle = fontStyle + }, + fontSizeDidChange: { fontSize in + AppPreferences.Editor.fontSize = fontSize + } + ) + } +} diff --git a/MarkEditMac/Sources/Settings/GeneralSettingsView.swift b/MarkEditMac/Sources/Settings/GeneralSettingsView.swift new file mode 100644 index 00000000..86a5c737 --- /dev/null +++ b/MarkEditMac/Sources/Settings/GeneralSettingsView.swift @@ -0,0 +1,69 @@ +// +// GeneralSettingsView.swift +// MarkEditMac +// +// Created by cyan on 1/26/23. +// + +import SwiftUI +import SettingsUI +import MarkEditKit + +struct GeneralSettingsView: View { + @State private var appearance = AppPreferences.General.appearance + @State private var newWindowBehavior = AppPreferences.General.newWindowBehavior + @State private var defaultTextEncoding = AppPreferences.General.defaultTextEncoding + @State private var defaultLineEndings = AppPreferences.General.defaultLineEndings + + var body: some View { + SettingsForm { + Section { + Picker(Localized.Settings.appearance, selection: $appearance) { + Text(Localized.Settings.system).tag(Appearance.system) + Divider() + Text(Localized.Settings.light).tag(Appearance.light) + Text(Localized.Settings.dark).tag(Appearance.dark) + } + .onChange(of: appearance) { + NSApp.appearance = $0.resolved() + AppPreferences.General.appearance = $0 + } + .formMenuPicker() + + Picker(Localized.Settings.newWindowBehavior, selection: $newWindowBehavior) { + Text(Localized.Document.openDocument).tag(NewWindowBehavior.openDocument) + Text(Localized.Document.newDocument).tag(NewWindowBehavior.newDocument) + } + .onChange(of: newWindowBehavior) { + AppPreferences.General.newWindowBehavior = $0 + } + .formMenuPicker() + } + + Section { + Picker(Localized.Settings.defaultTextEncoding, selection: $defaultTextEncoding) { + ForEach(EditorTextEncoding.allCases, id: \.self) { + Text($0.description) + + if EditorTextEncoding.groupingCases.contains($0) { + Divider() + } + } + } + .onChange(of: defaultTextEncoding) { + AppPreferences.General.defaultTextEncoding = $0 + } + + Picker(Localized.Settings.defaultLineEndings, selection: $defaultLineEndings) { + Text(Localized.Settings.macOSLineEndings).tag(LineEndings.lf) + Text(Localized.Settings.windowsLineEndings).tag(LineEndings.crlf) + Text(Localized.Settings.classicMacLineEndings).tag(LineEndings.cr) + } + .onChange(of: defaultLineEndings) { lineEndings in + AppPreferences.General.defaultLineEndings = lineEndings + } + .formMenuPicker() + } + } + } +} diff --git a/MarkEditMac/Sources/Settings/SettingTabs.swift b/MarkEditMac/Sources/Settings/SettingTabs.swift new file mode 100644 index 00000000..92762348 --- /dev/null +++ b/MarkEditMac/Sources/Settings/SettingTabs.swift @@ -0,0 +1,22 @@ +// +// SettingTabs.swift +// MarkEditMac +// +// Created by cyan on 1/26/23. +// + +import SettingsUI + +extension SettingsTabViewController { + static var editor: Self { + Self(EditorSettingsView(), title: Localized.Settings.editor, icon: Icons.characterCursorIbeam) + } + + static var general: Self { + Self(GeneralSettingsView(), title: Localized.Settings.general, icon: Icons.gearshape) + } + + static var window: Self { + Self(WindowSettingsView(), title: Localized.Settings.window, icon: Icons.macwindow) + } +} diff --git a/MarkEditMac/Sources/Settings/WindowSettingsView.swift b/MarkEditMac/Sources/Settings/WindowSettingsView.swift new file mode 100644 index 00000000..0de36c1f --- /dev/null +++ b/MarkEditMac/Sources/Settings/WindowSettingsView.swift @@ -0,0 +1,49 @@ +// +// WindowSettingsView.swift +// MarkEditMac +// +// Created by cyan on 1/26/23. +// + +import SwiftUI +import SettingsUI + +struct WindowSettingsView: View { + @State private var toolbarMode = AppPreferences.Window.toolbarMode + @State private var tabbingMode = AppPreferences.Window.tabbingMode + @State private var reduceTransparency = AppPreferences.Window.reduceTransparency + + var body: some View { + SettingsForm { + Section { + Picker(Localized.Settings.toolbarMode, selection: $toolbarMode) { + Text(Localized.Settings.normalMode).tag(ToolbarMode.normal) + Text(Localized.Settings.compactMode).tag(ToolbarMode.compact) + Text(Localized.Settings.hiddenMode).tag(ToolbarMode.hidden) + } + .onChange(of: toolbarMode) { + AppPreferences.Window.toolbarMode = $0 + } + .formMenuPicker() + + Picker(Localized.Settings.tabbingMode, selection: $tabbingMode) { + Text(Localized.Settings.automatic).tag(NSWindow.TabbingMode.automatic) + Text(Localized.Settings.preferred).tag(NSWindow.TabbingMode.preferred) + Text(Localized.Settings.disallowed).tag(NSWindow.TabbingMode.disallowed) + } + .onChange(of: tabbingMode) { + AppPreferences.Window.tabbingMode = $0 + } + .formMenuPicker() + } + + Section { + Toggle(Localized.Settings.reduceTransparencyDescription, isOn: $reduceTransparency) + .onChange(of: reduceTransparency) { + AppPreferences.Window.reduceTransparency = $0 + } + .formLabel(Localized.Settings.reduceTransparencyLabel) + } + } + } +} diff --git a/MarkEditMac/zh-Hans.lproj/Main.strings b/MarkEditMac/zh-Hans.lproj/Main.strings new file mode 100644 index 00000000..610e040e --- /dev/null +++ b/MarkEditMac/zh-Hans.lproj/Main.strings @@ -0,0 +1,429 @@ +/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ +"1b7-l0-nxx.title" = "查找"; + +/* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ +"1UK-8n-QPP.title" = "自定工具栏…"; + +/* Class = "NSMenuItem"; title = "MarkEdit"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "MarkEdit"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "转换"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "拼写"; + +/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ +"3rS-ZA-NoH.title" = "语音"; + +/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ +"4EN-yA-p0u.title" = "查找"; + +/* Class = "NSMenuItem"; title = "Grammarly"; ObjectID = "4Hc-LC-ge9"; */ +"4Hc-LC-ge9.title" = "Grammarly"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "进入全屏幕"; + +/* Class = "NSMenuItem"; title = "Quit MarkEdit"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "退出 MarkEdit"; + +/* Class = "NSMenu"; title = "Copy Path"; ObjectID = "5HO-Fv-Q1j"; */ +"5HO-Fv-Q1j.title" = "拷贝路径"; + +/* Class = "NSMenuItem"; title = "About MarkEdit"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "关于 MarkEdit"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "编辑"; + +/* Class = "NSMenuItem"; title = "Headers"; ObjectID = "5Zc-N5-YRN"; */ +"5Zc-N5-YRN.title" = "标题"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "重做"; + +/* Class = "NSMenu"; title = "Line Endings"; ObjectID = "7eH-ja-yIc"; */ +"7eH-ja-yIc.title" = "行尾格式"; + +/* Class = "NSMenuItem"; title = "Quote"; ObjectID = "8CK-gi-aLX"; */ +"8CK-gi-aLX.title" = "引用"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "替换"; + +/* Class = "NSMenu"; title = "Commands"; ObjectID = "9un-8n-UQZ"; */ +"9un-8n-UQZ.title" = "操作指令"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "智能拷贝/粘贴"; + +/* Class = "NSMenuItem"; title = "Copy Pandoc Command"; ObjectID = "53x-nK-wnX"; */ +"53x-nK-wnX.title" = "拷贝 Pandoc 指令"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "自动纠正拼写"; + +/* Class = "NSMenuItem"; title = "Classic Mac OS (CR)"; ObjectID = "ANU-wC-fmd"; */ +"ANU-wC-fmd.title" = "Classic Mac OS (CR)"; + +/* Class = "NSMenuItem"; title = "List"; ObjectID = "ANX-uP-CA4"; */ +"ANX-uP-CA4.title" = "列表"; + +/* Class = "NSMenuItem"; title = "Move Line Down"; ObjectID = "arb-IC-voG"; */ +"arb-IC-voG.title" = "向下移动行"; + +/* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ +"aTl-1u-JFS.title" = "打印…"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "窗口"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "主菜单"; + +/* Class = "NSMenuItem"; title = "Heading 3"; ObjectID = "BCM-0i-fPP"; */ +"BCM-0i-fPP.title" = "三级标题"; + +/* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ +"bib-Uj-vzu.title" = "文件"; + +/* Class = "NSMenuItem"; title = "Insert Link"; ObjectID = "BLl-I2-80X"; */ +"BLl-I2-80X.title" = "插入链接"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "设置…"; + +/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ +"buJ-ug-pKt.title" = "查找所选内容"; + +/* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ +"Bw7-FT-i3A.title" = "存储为…"; + +/* Class = "NSMenuItem"; title = "File Path"; ObjectID = "BxS-9Q-HQd"; */ +"BxS-9Q-HQd.title" = "文件路径"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "转换"; + +/* Class = "NSWindow"; title = "Window"; ObjectID = "Ckk-yw-fiv"; */ +"Ckk-yw-fiv.title" = "窗口"; + +/* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "crU-NP-BJu"; */ +"crU-NP-BJu.title" = "较小"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "智能链接"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "变为小写"; + +/* Class = "NSMenuItem"; title = "New Tab"; ObjectID = "dHZ-CH-KLC"; */ +"dHZ-CH-KLC.title" = "新建标签"; + +/* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ +"dMs-cI-mzQ.title" = "文件"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "撤销"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "拼写和语法"; + +/* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ +"DVo-aG-piG.title" = "关闭"; + +/* Class = "NSMenuItem"; title = "Todo"; ObjectID = "DZc-Ee-Asp"; */ +"DZc-Ee-Asp.title" = "待办"; + +/* Class = "NSMenuItem"; title = "Line Endings"; ObjectID = "EEg-eg-6lJ"; */ +"EEg-eg-6lJ.title" = "行尾格式"; + +/* Class = "NSMenuItem"; title = "Font"; ObjectID = "enN-sJ-W90"; */ +"enN-sJ-W90.title" = "字体"; + +/* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "ErA-Lh-vDf"; */ +"ErA-Lh-vDf.title" = "较大"; + +/* Class = "NSMenuItem"; title = "Table"; ObjectID = "ET1-7i-3N1"; */ +"ET1-7i-3N1.title" = "表格"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ +"F2S-fz-NVQ.title" = "帮助"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "替换"; + +/* Class = "NSMenuItem"; title = "MarkEdit Help"; ObjectID = "FKE-Sm-Kum"; */ +"FKE-Sm-Kum.title" = "MarkEdit 帮助"; + +/* Class = "NSMenuItem"; title = "Open In..."; ObjectID = "GdB-IP-6C8"; */ +"GdB-IP-6C8.title" = "使用应用打开…"; + +/* Class = "NSMenuItem"; title = "Heading 6"; ObjectID = "gfx-t1-1ED"; */ +"gfx-t1-1ED.title" = "六级标题"; + +/* Class = "NSMenuItem"; title = "Reset to Default"; ObjectID = "Ghc-zK-wBC"; */ +"Ghc-zK-wBC.title" = "重置为默认"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "粘贴"; + +/* Class = "NSMenuItem"; title = "Reveal in Finder"; ObjectID = "gwQ-Vq-avP"; */ +"gwQ-Vq-avP.title" = "在访达中找到"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "显示"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "显示拼写和语法"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "文本替换"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "智能引号"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "显示"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "立即检查文稿"; + +/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ +"hz9-B4-Xy5.title" = "服务"; + +/* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ +"IAo-SY-fd9.title" = "打开…"; + +/* Class = "NSMenuItem"; title = "Code"; ObjectID = "Ibb-aP-jnQ"; */ +"Ibb-aP-jnQ.title" = "代码"; + +/* Class = "NSMenuItem"; title = "Export to HTML"; ObjectID = "iIq-nl-rwB"; */ +"iIq-nl-rwB.title" = "导出到 HTML"; + +/* Class = "NSMenuItem"; title = "Select Line"; ObjectID = "InE-bU-3FX"; */ +"InE-bU-3FX.title" = "选择整行"; + +/* Class = "NSMenuItem"; title = "Copy Path"; ObjectID = "jv9-dC-jQp"; */ +"jv9-dC-jQp.title" = "拷贝路径"; + +/* Class = "NSMenuItem"; title = "Move Line Up"; ObjectID = "k8z-Oj-5dp"; */ +"k8z-Oj-5dp.title" = "向上移动行"; + +/* Class = "NSMenuItem"; title = "Italic"; ObjectID = "K67-Wt-aYF"; */ +"K67-Wt-aYF.title" = "斜体"; + +/* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ +"KaW-ft-85H.title" = "复原到已存储版本"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "全部显示"; + +/* Class = "NSMenuItem"; title = "Heading 1"; ObjectID = "kMr-G4-Kek"; */ +"kMr-G4-Kek.title" = "一级标题"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "前置全部窗口"; + +/* Class = "NSMenuItem"; title = "Format"; ObjectID = "lVU-H3-VpV"; */ +"lVU-H3-VpV.title" = "格式"; + +/* Class = "NSMenuItem"; title = "Windows (CRLF)"; ObjectID = "mc2-DH-5Iy"; */ +"mc2-DH-5Iy.title" = "Windows (CRLF)"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "检查拼写和语法"; + +/* Class = "NSMenuItem"; title = "Insert Image"; ObjectID = "N2l-0X-yEw"; */ +"N2l-0X-yEw.title" = "插入图片"; + +/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ +"NMo-om-nkz.title" = "服务"; + +/* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ +"oas-Oc-fiZ.title" = "打开最近使用"; + +/* Class = "NSMenuItem"; title = "Export to PDF"; ObjectID = "oc0-ce-Yrd"; */ +"oc0-ce-Yrd.title" = "导出到 PDF"; + +/* Class = "NSMenuItem"; title = "Hide MarkEdit"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "隐藏 MarkEdit"; + +/* Class = "NSMenuItem"; title = "Export to EPUB"; ObjectID = "ORv-QM-Kju"; */ +"ORv-QM-Kju.title" = "导出到 EPUB"; + +/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ +"OwM-mh-QMV.title" = "查找上一个"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "最小化"; + +/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ +"Oyz-dy-DGm.title" = "停止朗读"; + +/* Class = "NSMenuItem"; title = "Horizontal Rule"; ObjectID = "P3N-ql-hAd"; */ +"P3N-ql-hAd.title" = "水平分割线"; + +/* Class = "NSMenuItem"; title = "Heading 4"; ObjectID = "P4a-Ym-fhL"; */ +"P4a-Ym-fhL.title" = "四级标题"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "删除"; + +/* Class = "NSMenuItem"; title = "Copy Line Up"; ObjectID = "pTm-hL-bI5"; */ +"pTm-hL-bI5.title" = "向上拷贝行"; + +/* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ +"pxx-59-PXV.title" = "存储…"; + +/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ +"q09-fT-Sye.title" = "查找下一个"; + +/* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ +"qIS-W8-SiK.title" = "页面设置…"; + +/* Class = "NSMenuItem"; title = "macOS / Unix (LF)"; ObjectID = "qOk-v8-kKx"; */ +"qOk-v8-kKx.title" = "macOS / Unix (LF)"; + +/* Class = "NSMenuItem"; title = "Code Block"; ObjectID = "QOM-rI-Bxj"; */ +"QOM-rI-Bxj.title" = "代码块"; + +/* Class = "NSMenuItem"; title = "Export to RTF"; ObjectID = "QVA-GW-ruk"; */ +"QVA-GW-ruk.title" = "导出到 RTF"; + +/* Class = "NSMenu"; title = "Copy Pandoc Command"; ObjectID = "QZC-LS-WMo"; */ +"QZC-LS-WMo.title" = "拷贝 Pandoc 指令"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "缩放"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "键入时检查拼写"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "智能破折号"; + +/* Class = "NSMenuItem"; title = "Heading 2"; ObjectID = "Rpw-oR-sIY"; */ +"Rpw-oR-sIY.title" = "二级标题"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "全选"; + +/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ +"S0p-oC-mLd.title" = "跳到所选内容"; + +/* Class = "NSMenu"; title = "Reopen with Encoding"; ObjectID = "sPG-vJ-QST"; */ +"sPG-vJ-QST.title" = "以编码重新打开"; + +/* Class = "NSMenuItem"; title = "Math Block"; ObjectID = "Sqc-7N-3lD"; */ +"Sqc-7N-3lD.title" = "块状公式"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "窗口"; + +/* Class = "NSMenuItem"; title = "Heading 5"; ObjectID = "toe-mI-2pC"; */ +"toe-mI-2pC.title" = "五级标题"; + +/* Class = "NSMenuItem"; title = "Commands"; ObjectID = "tqa-Ue-2Ms"; */ +"tqa-Ue-2Ms.title" = "操作指令"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "数据检测器"; + +/* Class = "NSMenu"; title = "Open In..."; ObjectID = "Tuy-2U-frg"; */ +"Tuy-2U-frg.title" = "使用应用打开…"; + +/* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ +"tXI-mr-wws.title" = "打开最近使用"; + +/* Class = "NSMenuItem"; title = "Copy Line Down"; ObjectID = "U9S-1R-P8I"; */ +"U9S-1R-P8I.title" = "向下拷贝行"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "首字母大写"; + +/* Class = "NSMenuItem"; title = "Indent More"; ObjectID = "uHE-Ma-KIM"; */ +"uHE-Ma-KIM.title" = "增加缩进"; + +/* Class = "NSMenuItem"; title = "Export to Word Docx"; ObjectID = "Ukm-Sm-nrt"; */ +"Ukm-Sm-nrt.title" = "导出到 Word Docx"; + +/* Class = "NSMenu"; title = "MarkEdit"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "MarkEdit"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "剪切"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "隐藏其他"; + +/* Class = "NSMenuItem"; title = "Ordered List"; ObjectID = "vhv-qY-5Gc"; */ +"vhv-qY-5Gc.title" = "有序列表"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "变为大写"; + +/* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ +"vNY-rz-j42.title" = "清除菜单"; + +/* Class = "NSMenu"; title = "Font"; ObjectID = "VwK-Nx-wgD"; */ +"VwK-Nx-wgD.title" = "字体"; + +/* Class = "NSMenuItem"; title = "Indent Less"; ObjectID = "w9R-Kp-jCa"; */ +"w9R-Kp-jCa.title" = "减少缩进"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "编辑"; + +/* Class = "NSMenuItem"; title = "Bold"; ObjectID = "WAq-55-Lug"; */ +"WAq-55-Lug.title" = "粗体"; + +/* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ +"Was-JA-tGl.title" = "新建"; + +/* Class = "NSMenuItem"; title = "Go to Line..."; ObjectID = "wBj-kZ-XmW"; */ +"wBj-kZ-XmW.title" = "跳转到行…"; + +/* Class = "NSMenuItem"; title = "Math"; ObjectID = "wIF-eb-cez"; */ +"wIF-eb-cez.title" = "行内公式"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ +"wpr-3q-Mcd.title" = "帮助"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "拷贝"; + +/* Class = "NSMenuItem"; title = "Reopen with Encoding"; ObjectID = "XQw-pS-pD7"; */ +"XQw-pS-pD7.title" = "以编码重新打开"; + +/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ +"xrE-MZ-jX0.title" = "语音"; + +/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ +"Xz5-n4-O0W.title" = "查找…"; + +/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ +"YEy-JH-Tfz.title" = "查找和替换…"; + +/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ +"Ynk-f8-cLZ.title" = "开始朗读"; + +/* Class = "NSMenuItem"; title = "Learn More..."; ObjectID = "yv7-Am-Rb2"; */ +"yv7-Am-Rb2.title" = "了解更多…"; + +/* Class = "NSMenu"; title = "Format"; ObjectID = "yy7-cF-JbT"; */ +"yy7-cF-JbT.title" = "格式"; + +/* Class = "NSMenu"; title = "Headers"; ObjectID = "z5V-GU-gP0"; */ +"z5V-GU-gP0.title" = "标题"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "显示替换"; + +/* Class = "NSMenuItem"; title = "Strikethrough"; ObjectID = "z7z-Qk-jsC"; */ +"z7z-Qk-jsC.title" = "删除线"; + +/* Class = "NSMenuItem"; title = "Folder Path"; ObjectID = "zVr-EF-hMY"; */ +"zVr-EF-hMY.title" = "目录路径"; + diff --git a/MarkEditMac/zh-Hant.lproj/Main.strings b/MarkEditMac/zh-Hant.lproj/Main.strings new file mode 100644 index 00000000..68208987 --- /dev/null +++ b/MarkEditMac/zh-Hant.lproj/Main.strings @@ -0,0 +1,429 @@ +/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ +"1b7-l0-nxx.title" = "尋找"; + +/* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ +"1UK-8n-QPP.title" = "自定工具列…"; + +/* Class = "NSMenuItem"; title = "MarkEdit"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "MarkEdit"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "轉換"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "拼字"; + +/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ +"3rS-ZA-NoH.title" = "語音"; + +/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ +"4EN-yA-p0u.title" = "尋找"; + +/* Class = "NSMenuItem"; title = "Grammarly"; ObjectID = "4Hc-LC-ge9"; */ +"4Hc-LC-ge9.title" = "Grammarly"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "進入全螢幕"; + +/* Class = "NSMenuItem"; title = "Quit MarkEdit"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "結束 MarkEdit"; + +/* Class = "NSMenu"; title = "Copy Path"; ObjectID = "5HO-Fv-Q1j"; */ +"5HO-Fv-Q1j.title" = "拷貝路徑"; + +/* Class = "NSMenuItem"; title = "About MarkEdit"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "關於 MarkEdit"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "編輯"; + +/* Class = "NSMenuItem"; title = "Headers"; ObjectID = "5Zc-N5-YRN"; */ +"5Zc-N5-YRN.title" = "標題"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "重做"; + +/* Class = "NSMenu"; title = "Line Endings"; ObjectID = "7eH-ja-yIc"; */ +"7eH-ja-yIc.title" = "行尾格式"; + +/* Class = "NSMenuItem"; title = "Quote"; ObjectID = "8CK-gi-aLX"; */ +"8CK-gi-aLX.title" = "引用"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "替代"; + +/* Class = "NSMenu"; title = "Commands"; ObjectID = "9un-8n-UQZ"; */ +"9un-8n-UQZ.title" = "操作指令"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "智慧型拷貝/貼上"; + +/* Class = "NSMenuItem"; title = "Copy Pandoc Command"; ObjectID = "53x-nK-wnX"; */ +"53x-nK-wnX.title" = "拷貝 Pandoc 指令"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "自動修正拼字"; + +/* Class = "NSMenuItem"; title = "Classic Mac OS (CR)"; ObjectID = "ANU-wC-fmd"; */ +"ANU-wC-fmd.title" = "Classic Mac OS (CR)"; + +/* Class = "NSMenuItem"; title = "List"; ObjectID = "ANX-uP-CA4"; */ +"ANX-uP-CA4.title" = "列表"; + +/* Class = "NSMenuItem"; title = "Move Line Down"; ObjectID = "arb-IC-voG"; */ +"arb-IC-voG.title" = "向下移動行"; + +/* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ +"aTl-1u-JFS.title" = "列印…"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "視窗"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "主選單"; + +/* Class = "NSMenuItem"; title = "Heading 3"; ObjectID = "BCM-0i-fPP"; */ +"BCM-0i-fPP.title" = "三級標題"; + +/* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ +"bib-Uj-vzu.title" = "檔案"; + +/* Class = "NSMenuItem"; title = "Insert Link"; ObjectID = "BLl-I2-80X"; */ +"BLl-I2-80X.title" = "插入連結"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "設定…"; + +/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ +"buJ-ug-pKt.title" = "檢索所選內容"; + +/* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ +"Bw7-FT-i3A.title" = "儲存為…"; + +/* Class = "NSMenuItem"; title = "File Path"; ObjectID = "BxS-9Q-HQd"; */ +"BxS-9Q-HQd.title" = "檔案路徑"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "轉換"; + +/* Class = "NSWindow"; title = "Window"; ObjectID = "Ckk-yw-fiv"; */ +"Ckk-yw-fiv.title" = "視窗"; + +/* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "crU-NP-BJu"; */ +"crU-NP-BJu.title" = "縮小"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "智慧型連結"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "小寫"; + +/* Class = "NSMenuItem"; title = "New Tab"; ObjectID = "dHZ-CH-KLC"; */ +"dHZ-CH-KLC.title" = "新增標籤"; + +/* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ +"dMs-cI-mzQ.title" = "檔案"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "還原"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "拼字和文法檢查"; + +/* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ +"DVo-aG-piG.title" = "結束"; + +/* Class = "NSMenuItem"; title = "Todo"; ObjectID = "DZc-Ee-Asp"; */ +"DZc-Ee-Asp.title" = "待辦"; + +/* Class = "NSMenuItem"; title = "Line Endings"; ObjectID = "EEg-eg-6lJ"; */ +"EEg-eg-6lJ.title" = "行尾格式"; + +/* Class = "NSMenuItem"; title = "Font"; ObjectID = "enN-sJ-W90"; */ +"enN-sJ-W90.title" = "字體"; + +/* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "ErA-Lh-vDf"; */ +"ErA-Lh-vDf.title" = "放大"; + +/* Class = "NSMenuItem"; title = "Table"; ObjectID = "ET1-7i-3N1"; */ +"ET1-7i-3N1.title" = "表格"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ +"F2S-fz-NVQ.title" = "輔助說明"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "替代"; + +/* Class = "NSMenuItem"; title = "MarkEdit Help"; ObjectID = "FKE-Sm-Kum"; */ +"FKE-Sm-Kum.title" = "MarkEdit 輔助說明"; + +/* Class = "NSMenuItem"; title = "Open In..."; ObjectID = "GdB-IP-6C8"; */ +"GdB-IP-6C8.title" = "使用應用打開…"; + +/* Class = "NSMenuItem"; title = "Heading 6"; ObjectID = "gfx-t1-1ED"; */ +"gfx-t1-1ED.title" = "六級標題"; + +/* Class = "NSMenuItem"; title = "Reset to Default"; ObjectID = "Ghc-zK-wBC"; */ +"Ghc-zK-wBC.title" = "重置為預設"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "貼上"; + +/* Class = "NSMenuItem"; title = "Reveal in Finder"; ObjectID = "gwQ-Vq-avP"; */ +"gwQ-Vq-avP.title" = "在 Finder 中找到"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "顯示方式"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "顯示拼字和文法檢查"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "替代文字"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "智慧型引號"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "顯示方式"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "立即檢查文件"; + +/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ +"hz9-B4-Xy5.title" = "服務"; + +/* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ +"IAo-SY-fd9.title" = "打開…"; + +/* Class = "NSMenuItem"; title = "Code"; ObjectID = "Ibb-aP-jnQ"; */ +"Ibb-aP-jnQ.title" = "程式碼"; + +/* Class = "NSMenuItem"; title = "Export to HTML"; ObjectID = "iIq-nl-rwB"; */ +"iIq-nl-rwB.title" = "輸出為 HTML"; + +/* Class = "NSMenuItem"; title = "Select Line"; ObjectID = "InE-bU-3FX"; */ +"InE-bU-3FX.title" = "選擇整行"; + +/* Class = "NSMenuItem"; title = "Copy Path"; ObjectID = "jv9-dC-jQp"; */ +"jv9-dC-jQp.title" = "拷貝路徑"; + +/* Class = "NSMenuItem"; title = "Move Line Up"; ObjectID = "k8z-Oj-5dp"; */ +"k8z-Oj-5dp.title" = "向上移動行"; + +/* Class = "NSMenuItem"; title = "Italic"; ObjectID = "K67-Wt-aYF"; */ +"K67-Wt-aYF.title" = "斜體"; + +/* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ +"KaW-ft-85H.title" = "復原到已儲存版本"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "顯示全部"; + +/* Class = "NSMenuItem"; title = "Heading 1"; ObjectID = "kMr-G4-Kek"; */ +"kMr-G4-Kek.title" = "一級標題"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "將此程式所有視窗移至最前"; + +/* Class = "NSMenuItem"; title = "Format"; ObjectID = "lVU-H3-VpV"; */ +"lVU-H3-VpV.title" = "格式"; + +/* Class = "NSMenuItem"; title = "Windows (CRLF)"; ObjectID = "mc2-DH-5Iy"; */ +"mc2-DH-5Iy.title" = "Windows (CRLF)"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "檢查拼字文法"; + +/* Class = "NSMenuItem"; title = "Insert Image"; ObjectID = "N2l-0X-yEw"; */ +"N2l-0X-yEw.title" = "插入圖片"; + +/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ +"NMo-om-nkz.title" = "服務"; + +/* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ +"oas-Oc-fiZ.title" = "打開最近使用過的檔案"; + +/* Class = "NSMenuItem"; title = "Export to PDF"; ObjectID = "oc0-ce-Yrd"; */ +"oc0-ce-Yrd.title" = "輸出為 PDF"; + +/* Class = "NSMenuItem"; title = "Hide MarkEdit"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "隱藏 MarkEdit"; + +/* Class = "NSMenuItem"; title = "Export to EPUB"; ObjectID = "ORv-QM-Kju"; */ +"ORv-QM-Kju.title" = "輸出為 EPUB"; + +/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ +"OwM-mh-QMV.title" = "尋找上一個"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "縮到最小"; + +/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ +"Oyz-dy-DGm.title" = "停止朗讀"; + +/* Class = "NSMenuItem"; title = "Horizontal Rule"; ObjectID = "P3N-ql-hAd"; */ +"P3N-ql-hAd.title" = "水平分割線"; + +/* Class = "NSMenuItem"; title = "Heading 4"; ObjectID = "P4a-Ym-fhL"; */ +"P4a-Ym-fhL.title" = "四級標題"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "刪除"; + +/* Class = "NSMenuItem"; title = "Copy Line Up"; ObjectID = "pTm-hL-bI5"; */ +"pTm-hL-bI5.title" = "向上拷貝行"; + +/* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ +"pxx-59-PXV.title" = "儲存…"; + +/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ +"q09-fT-Sye.title" = "尋找下一個"; + +/* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ +"qIS-W8-SiK.title" = "設定頁面…"; + +/* Class = "NSMenuItem"; title = "macOS / Unix (LF)"; ObjectID = "qOk-v8-kKx"; */ +"qOk-v8-kKx.title" = "macOS / Unix (LF)"; + +/* Class = "NSMenuItem"; title = "Code Block"; ObjectID = "QOM-rI-Bxj"; */ +"QOM-rI-Bxj.title" = "程式碼塊"; + +/* Class = "NSMenuItem"; title = "Export to RTF"; ObjectID = "QVA-GW-ruk"; */ +"QVA-GW-ruk.title" = "輸出為 RTF"; + +/* Class = "NSMenu"; title = "Copy Pandoc Command"; ObjectID = "QZC-LS-WMo"; */ +"QZC-LS-WMo.title" = "拷貝 Pandoc 指令"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "縮放"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "在輸入時同步檢查拼字"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "智慧型破折號"; + +/* Class = "NSMenuItem"; title = "Heading 2"; ObjectID = "Rpw-oR-sIY"; */ +"Rpw-oR-sIY.title" = "二級標題"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "全選"; + +/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ +"S0p-oC-mLd.title" = "跳至所選範圍"; + +/* Class = "NSMenu"; title = "Reopen with Encoding"; ObjectID = "sPG-vJ-QST"; */ +"sPG-vJ-QST.title" = "以編碼重新打開"; + +/* Class = "NSMenuItem"; title = "Math Block"; ObjectID = "Sqc-7N-3lD"; */ +"Sqc-7N-3lD.title" = "塊狀公式"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "視窗"; + +/* Class = "NSMenuItem"; title = "Heading 5"; ObjectID = "toe-mI-2pC"; */ +"toe-mI-2pC.title" = "五級標題"; + +/* Class = "NSMenuItem"; title = "Commands"; ObjectID = "tqa-Ue-2Ms"; */ +"tqa-Ue-2Ms.title" = "操作指令"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "資料偵測器"; + +/* Class = "NSMenu"; title = "Open In..."; ObjectID = "Tuy-2U-frg"; */ +"Tuy-2U-frg.title" = "使用應用打開…"; + +/* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ +"tXI-mr-wws.title" = "打開最近使用過的檔案"; + +/* Class = "NSMenuItem"; title = "Copy Line Down"; ObjectID = "U9S-1R-P8I"; */ +"U9S-1R-P8I.title" = "向下拷貝行"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "大寫"; + +/* Class = "NSMenuItem"; title = "Indent More"; ObjectID = "uHE-Ma-KIM"; */ +"uHE-Ma-KIM.title" = "增加縮排"; + +/* Class = "NSMenuItem"; title = "Export to Word Docx"; ObjectID = "Ukm-Sm-nrt"; */ +"Ukm-Sm-nrt.title" = "輸出為 Word Docx"; + +/* Class = "NSMenu"; title = "MarkEdit"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "MarkEdit"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "剪下"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "隱藏其他"; + +/* Class = "NSMenuItem"; title = "Ordered List"; ObjectID = "vhv-qY-5Gc"; */ +"vhv-qY-5Gc.title" = "有序列表"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "大寫"; + +/* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ +"vNY-rz-j42.title" = "清除選單"; + +/* Class = "NSMenu"; title = "Font"; ObjectID = "VwK-Nx-wgD"; */ +"VwK-Nx-wgD.title" = "字體"; + +/* Class = "NSMenuItem"; title = "Indent Less"; ObjectID = "w9R-Kp-jCa"; */ +"w9R-Kp-jCa.title" = "減少縮排"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "編輯"; + +/* Class = "NSMenuItem"; title = "Bold"; ObjectID = "WAq-55-Lug"; */ +"WAq-55-Lug.title" = "粗體"; + +/* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ +"Was-JA-tGl.title" = "新增"; + +/* Class = "NSMenuItem"; title = "Go to Line..."; ObjectID = "wBj-kZ-XmW"; */ +"wBj-kZ-XmW.title" = "跳轉到行…"; + +/* Class = "NSMenuItem"; title = "Math"; ObjectID = "wIF-eb-cez"; */ +"wIF-eb-cez.title" = "行內公式"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ +"wpr-3q-Mcd.title" = "輔助說明"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "拷貝"; + +/* Class = "NSMenuItem"; title = "Reopen with Encoding"; ObjectID = "XQw-pS-pD7"; */ +"XQw-pS-pD7.title" = "以編碼重新打開"; + +/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ +"xrE-MZ-jX0.title" = "語音"; + +/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ +"Xz5-n4-O0W.title" = "尋找…"; + +/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ +"YEy-JH-Tfz.title" = "尋找與取代…"; + +/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ +"Ynk-f8-cLZ.title" = "開始朗讀"; + +/* Class = "NSMenuItem"; title = "Learn More..."; ObjectID = "yv7-Am-Rb2"; */ +"yv7-Am-Rb2.title" = "瞭解更多…"; + +/* Class = "NSMenu"; title = "Format"; ObjectID = "yy7-cF-JbT"; */ +"yy7-cF-JbT.title" = "格式"; + +/* Class = "NSMenu"; title = "Headers"; ObjectID = "z5V-GU-gP0"; */ +"z5V-GU-gP0.title" = "標題"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "顯示替代視窗"; + +/* Class = "NSMenuItem"; title = "Strikethrough"; ObjectID = "z7z-Qk-jsC"; */ +"z7z-Qk-jsC.title" = "刪除線"; + +/* Class = "NSMenuItem"; title = "Folder Path"; ObjectID = "zVr-EF-hMY"; */ +"zVr-EF-hMY.title" = "目錄路徑"; + diff --git a/MarkEditMacTests/MarkEditMacTests.swift b/MarkEditMacTests/MarkEditMacTests.swift new file mode 100644 index 00000000..138cc416 --- /dev/null +++ b/MarkEditMacTests/MarkEditMacTests.swift @@ -0,0 +1,12 @@ +// +// MarkEditMacTests.swift +// MarkEditMacTests +// +// Created by cyan on 2/2/23. +// + +import XCTest + +final class MarkEditMacTests: XCTestCase { + // no-op +} diff --git a/MarkEditTools/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/MarkEditTools/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/MarkEditTools/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MarkEditTools/Package.swift b/MarkEditTools/Package.swift new file mode 100644 index 00000000..f28a5c10 --- /dev/null +++ b/MarkEditTools/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MarkEditTools", + platforms: [ + .iOS(.v15), + .macOS(.v12), + ], + products: [ + .plugin(name: "SwiftLint", targets: ["SwiftLint"]), + ], + targets: [ + .binaryTarget( + name: "SwiftLintBinary", + url: "https://github.com/realm/SwiftLint/releases/download/0.50.3/SwiftLintBinary-macos.artifactbundle.zip", + checksum: "abe7c0bb505d26c232b565c3b1b4a01a8d1a38d86846e788c4d02f0b1042a904" + ), + .plugin( + name: "SwiftLint", + capability: .buildTool(), + dependencies: ["SwiftLintBinary"] + ), + ] +) diff --git a/MarkEditTools/Plugins/SwiftLint/main.swift b/MarkEditTools/Plugins/SwiftLint/main.swift new file mode 100644 index 00000000..a074b221 --- /dev/null +++ b/MarkEditTools/Plugins/SwiftLint/main.swift @@ -0,0 +1,36 @@ +// +// SwiftLintPlugin.swift +// +// Created by cyan on 1/30/23. +// + +import PackagePlugin +import XcodeProjectPlugin + +@main +struct Main: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + // XcodeBuildToolPlugin would be good enough + return [] + } +} + +extension Main: XcodeBuildToolPlugin { + func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { + [ + .buildCommand( + displayName: "Running SwiftLint for \(target.displayName)", + executable: try context.tool(named: "swiftlint").path, + arguments: [ + "lint", + "--strict", + "--config", + "\(context.xcodeProject.directory.string)/.swiftlint.yml", + "--cache-path", + "\(context.pluginWorkDirectory.string)/cache", + context.xcodeProject.directory.string, + ] + ), + ] + } +} diff --git a/MarkEditTools/README.md b/MarkEditTools/README.md new file mode 100644 index 00000000..ab7b10d1 --- /dev/null +++ b/MarkEditTools/README.md @@ -0,0 +1,5 @@ +# MarkEditTools + +This package provides dev tools for all targets, such as SwiftLint. + +> Note that this package should be platform-independent. \ No newline at end of file diff --git a/PreviewExtension/Base.lproj/Main.xib b/PreviewExtension/Base.lproj/Main.xib new file mode 100644 index 00000000..2db8e771 --- /dev/null +++ b/PreviewExtension/Base.lproj/Main.xib @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PreviewExtension/Info.entitlements b/PreviewExtension/Info.entitlements new file mode 100644 index 00000000..625af03d --- /dev/null +++ b/PreviewExtension/Info.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/PreviewExtension/Info.plist b/PreviewExtension/Info.plist new file mode 100644 index 00000000..a5d591e9 --- /dev/null +++ b/PreviewExtension/Info.plist @@ -0,0 +1,24 @@ + + + + + NSExtension + + NSExtensionAttributes + + QLIsDataBasedPreview + + QLSupportedContentTypes + + net.daringfireball.markdown + + QLSupportsSearchableItems + + + NSExtensionPointIdentifier + com.apple.quicklook.preview + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PreviewViewController + + + diff --git a/PreviewExtension/PreviewViewController.swift b/PreviewExtension/PreviewViewController.swift new file mode 100644 index 00000000..6d1c0683 --- /dev/null +++ b/PreviewExtension/PreviewViewController.swift @@ -0,0 +1,89 @@ +// +// PreviewViewController.swift +// PreviewExtension +// +// Created by cyan on 12/20/22. +// + +import Cocoa +import QuickLookUI +import WebKit +import MarkEditCore + +final class PreviewViewController: NSViewController, QLPreviewingController { + private var appearanceObservation: NSKeyValueObservation? + private let webView: WKWebView = { + let config = WKWebViewConfiguration() + if config.responds(to: sel_getUid("_drawsBackground")) { + config.setValue(false, forKey: "drawsBackground") + } + + return WKWebView(frame: .zero, configuration: config) + }() + + override var nibName: NSNib.Name? { + NSNib.Name("Main") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(webView) + + appearanceObservation = NSApp.observe(\.effectiveAppearance) { [weak self] _, _ in + self?.updateEditorTheme() + } + } + + override func viewDidLayout() { + super.viewDidLayout() + webView.frame = view.bounds + } + + func preparePreviewOfFile(at url: URL) async throws { + let data = try Data(contentsOf: url) + let config = EditorConfig( + text: data.toString() ?? "", + theme: effectiveTheme, + fontFamily: "ui-monospace", + fontSize: 12, + showLineNumbers: false, + showActiveLineIndicator: false, + showInvisibles: false, + typewriterMode: false, + focusMode: false, + lineWrapping: true, + lineHeight: 1.4, + defaultLineBreak: nil, + tabKeyBehavior: nil, + indentUnit: nil, + localizable: nil + ) + + webView.loadHTMLString(config.toHtml, baseURL: nil) + } +} + +// MARK: - Private + +private extension PreviewViewController { + var effectiveTheme: String { + NSApp.effectiveAppearance.isDarkMode ? "github-dark" : "github-light" + } + + func updateEditorTheme() { + // To keep the app size smaller, we don't have bridge here, + // construct script literals directly. + webView.evaluateJavaScript("setTheme(`\(effectiveTheme)`)") + } +} + +private extension NSAppearance { + var isDarkMode: Bool { + switch name { + case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: + return true + default: + return false + } + } +} diff --git a/README.md b/README.md index 67081285..cec2db1e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ MarkEdit is a free and **open-source** Markdown editor, for macOS. It's just lik Download on the Mac App Store -> **Note** The source code will be released as soon as the application is released. Join [TestFlight](https://testflight.apple.com/join/Nv4YUeHT). - ## Screenshots ![Screenshots 01](/Screenshots/01.png) @@ -47,6 +45,27 @@ After successfully building `CoreEditor`, open `MarkEdit.xcodeproj`, and build t It's recommended to override build settings by adding a `Local.xcconfig` file under the root folder, including code signing identity, development team, etc. +## Testing MarkEdit Locally + +Unit tests are run automatically by [GitHub actions](https://github.com/MarkEdit-app/MarkEdit/actions), you can also run them on your machine. + +### Testing CoreEditor + +Make sure dependencies are installed and run: + +``` +cd CoreEditor +yarn test +``` + +### Testing MarkEditMac + +MarkEditMac consists of several targets, here's an example of testing `MarkEditCoreTests`: + +``` +xcodebuild test -project MarkEdit.xcodeproj -scheme MarkEditCoreTests -destination 'platform=macOS' +``` + ## Contributing to MarkEdit For bug reports, please [open an issue](https://github.com/MarkEdit-app/MarkEdit/issues/new).

0{{R307*qoM6N<$f&%CY3;+NC literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..1898626c57c40e1b8660ea547fe72af1027f524d GIT binary patch literal 25301 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 literal 0 HcmV?d00001 diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7a1d85dd7762c528462f974ee0b40f61b16f58f0 GIT binary patch literal 81043 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