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 00000000..9197b9fa Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ 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 00000000..204a9741 Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ 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 00000000..1d37c14a Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..6bbc7573 Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ 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 00000000..a1902382 Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ 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 00000000..df56620d Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ 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 00000000..704c2641 Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ 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 00000000..0fb70a48 Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ 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 00000000..1898626c Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ 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 00000000..7a1d85dd Binary files /dev/null and b/MarkEditMac/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ 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 00000000..ed60b89c Binary files /dev/null and b/MarkEditMac/Resources/en.lproj/Localizable.strings differ 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).