diff --git a/src/.gitattributes b/src/.gitattributes
new file mode 100644
index 0000000..3937673
--- /dev/null
+++ b/src/.gitattributes
@@ -0,0 +1,13 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Declare files that will always have CRLF line endings on checkout.
+*.cs text eol=crlf
+*.xaml text eol=crlf
+*.resw text eol=crlf
+*.csproj text eol=crlf
+*.sln text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
\ No newline at end of file
diff --git a/src/.github/FUNDING.yml b/src/.github/FUNDING.yml
new file mode 100644
index 0000000..d6aab31
--- /dev/null
+++ b/src/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: 0x7c13
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: jackieliu
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: paypal.me/jackil
diff --git a/src/.github/ISSUE_TEMPLATE/bug_report.md b/src/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..0ce21b9
--- /dev/null
+++ b/src/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,27 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[Bug]"
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. Windows 10 1809 17763.593]
+ - Version [e.g. v0.9.3.0]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/src/.github/ISSUE_TEMPLATE/feature_request.md b/src/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..38ac82c
--- /dev/null
+++ b/src/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[Feature request]"
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/src/.github/PULL_REQUEST_TEMPLATE.md b/src/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..d4c96b6
--- /dev/null
+++ b/src/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,25 @@
+
+
+## PR Type
+What kind of change does this PR introduce?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other information
diff --git a/src/.github/RELEASE_TEMPLATE/changelog_config.json b/src/.github/RELEASE_TEMPLATE/changelog_config.json
new file mode 100644
index 0000000..a3c4c1a
--- /dev/null
+++ b/src/.github/RELEASE_TEMPLATE/changelog_config.json
@@ -0,0 +1,13 @@
+{
+ "conventionalCommitsParserOptions": {
+ "revertPattern": "/^(?:Revert|revert:)\\s\"?([\\s\\S]+?)\"?\\s*This reverts commit (\\w*)\\./i",
+ "issuePrefixes": [ "#", "OLDARCH-" ]
+ },
+ "handleBarsOptions": {
+ "setupFile": null,
+ "template": ".github/RELEASE_TEMPLATE/changelog_template.hbs",
+ "compileOptions": { "noEscape": true }
+ },
+ "breakingChangesPattern": "/^breaking\\s+change$/gim",
+ "hostname": "https://github.com"
+}
diff --git a/src/.github/RELEASE_TEMPLATE/changelog_template.hbs b/src/.github/RELEASE_TEMPLATE/changelog_template.hbs
new file mode 100644
index 0000000..2fdfd15
--- /dev/null
+++ b/src/.github/RELEASE_TEMPLATE/changelog_template.hbs
@@ -0,0 +1,43 @@
+{{#with release}}
+## [{{name}}]({{href}})
+{{/with}}
+
+{{#commit-list commits heading='### 💥 Breaking Changes' breaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### ✨ Features' type='feat' excludeBreaking=true}}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 🐛 Fixes' type='fix' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 🔥 Refactorings' type='refactor' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 🐎 Performance Improvements' type='perf' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 🛠 Maintenance' types='chore,ci' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### ✅ Tests' type='test' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 📚 Documentation' type='doc,docs' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 💄 Style' type='style' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
+
+{{#commit-list commits heading='### 📢 Translations' type='lang,trans' excludeBreaking=true }}
+- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
+{{/commit-list}}
diff --git a/src/.github/dependabot.yml b/src/.github/dependabot.yml
new file mode 100644
index 0000000..4b4120d
--- /dev/null
+++ b/src/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ # default location of `.github/workflows`
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ commit-message:
+ prefix: 'action-deps: '
+
+ - package-ecosystem: "nuget"
+ # location of package manifests
+ directory: "/src/Notepads"
+ schedule:
+ interval: "daily"
+ commit-message:
+ prefix: 'nuget-deps: '
+
+# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
\ No newline at end of file
diff --git a/src/.github/issue_label_bot.yaml b/src/.github/issue_label_bot.yaml
new file mode 100644
index 0000000..b642226
--- /dev/null
+++ b/src/.github/issue_label_bot.yaml
@@ -0,0 +1,4 @@
+label-alias:
+ bug: 'bug'
+ feature_request: 'enhancement'
+ question: 'question'
diff --git a/src/.github/workflows/csa-bulk-dismissal.yml b/src/.github/workflows/csa-bulk-dismissal.yml
new file mode 100644
index 0000000..64b5725
--- /dev/null
+++ b/src/.github/workflows/csa-bulk-dismissal.yml
@@ -0,0 +1,128 @@
+name: Code scanning alerts bulk dismissal
+
+on:
+ workflow_run:
+ workflows: [ "Notepads CI/CD Pipeline" ]
+ types:
+ - completed
+ workflow_dispatch:
+ inputs:
+ type:
+ description: Type of filter to use ("path" for using path and "desc" for using description)
+ required: true
+ default: 'path'
+ reason:
+ description: Reason for dismissal ("fp" for "false positive", "wf" for "won't fix" and "ut" for "used in tests")
+ required: true
+ default: 'wf'
+
+jobs:
+ setup:
+ runs-on: windows-latest
+ outputs:
+ matrix: ${{ steps.set_filter_matrix.outputs.matrix }}
+ steps:
+ - name: Setup filter matrix
+ id: set_filter_matrix
+ shell: pwsh
+ run: |
+ $FILTER_TYPE = $env:FILTER_TYPE
+ if ( !( $env:FILTER_TYPE -ieq 'path' ) -And !( $env:FILTER_TYPE -ieq 'desc' ) ) {
+ $FILTER_TYPE = 'path'
+ }
+
+ switch ( $env:REASON ) {
+ fp {
+ $REASON = "false positive"
+ }
+ wf {
+ $REASON = "won't fix"
+ }
+ ut {
+ $REASON = "used in tests"
+ }
+ default {
+ $REASON = "won't fix"
+ }
+ }
+
+ if ( $FILTER_TYPE -ieq 'path' ) {
+ $MATRIX = @{
+ include = @(
+ @{
+ filter = "*/obj/*"
+ }
+ )
+ }
+ } elseif ( $FILTER_TYPE -ieq 'desc' ) {
+ $MATRIX = @{
+ include = @(
+ @{
+ filter = "Calls to unmanaged code"
+ },
+ @{
+ filter = "Unmanaged code"
+ }
+ )
+ }
+ } else {
+ throw "Invalid filter type argument"
+ }
+
+ $MATRIX.include | Foreach-Object {
+ $_.Add('type',"$FILTER_TYPE")
+ $_.Add('reason',"$REASON")
+ }
+ echo "::set-output name=matrix::$($MATRIX | ConvertTo-Json -depth 32 -Compress)"
+ env:
+ FILTER_TYPE: ${{ github.event.inputs.type }}
+ REASON: ${{ github.event.inputs.reason }}
+ dismiss-alerts:
+ name: Dismiss alerts
+ needs: setup
+ runs-on: windows-latest
+ strategy:
+ matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
+ env:
+ # Settings
+ OWNER: ${{ github.repository_owner }} # verbatim from URL
+ PROJECT_NAME: ${{ github.event.repository.name }} # verbatim from URL
+ ACCESS_TOKEN: ${{ secrets.CSA_ACCESS_TOKEN }} # requires security_events read/write permissions
+ DISMISS_REASON: ${{ matrix.reason }} # "false positive", "won't fix" or "used in tests".
+ ALERTS_PER_PAGE: 100 # maximum is 100
+ FILTER: ${{ matrix.filter }}
+ FILTER_TYPE: ${{ matrix.type }}
+ steps:
+ - name: Run automation
+ id: run_automation
+ shell: pwsh
+ run: |
+ $HEADERS = @{
+ Authorization = 'Basic {0}' -f [System.Convert]::ToBase64String([char[]]"$($env:OWNER):$($env:ACCESS_TOKEN)")
+ Accept = 'application/vnd.github.v3+json'
+ }
+
+ $page = 1
+ $FETCH_URL = "https://api.github.com/repos/$env:OWNER/$env:PROJECT_NAME/code-scanning/alerts?state=open&page={0}&per_page=$env:ALERTS_PER_PAGE"
+ $LIST_OF_ALERTS = Invoke-RestMethod -Method Get -Headers $HEADERS -Uri $($FETCH_URL -f $page)
+ while ( $LIST_OF_ALERTS -ne $null ) {
+ if ( $env:FILTER_TYPE -ieq 'path' ) {
+ $MATCHES += $($LIST_OF_ALERTS | Where-Object { $_.most_recent_instance.location.path -like "$env:FILTER" })
+ } else {
+ $MATCHES += $($LIST_OF_ALERTS | Where-Object { $_.rule.description -like "$env:FILTER" })
+ }
+
+ $page += 1
+ $LIST_OF_ALERTS = Invoke-RestMethod -Method Get -Headers $HEADERS -Uri $($FETCH_URL -f $page)
+ }
+
+ $ALERT_URL = "https://api.github.com/repos/$env:OWNER/$env:PROJECT_NAME/code-scanning/alerts/{0}"
+ $BODY = @{
+ state = 'dismissed'
+ dismissed_reason = "$env:DISMISS_REASON"
+ } | ConvertTo-Json
+ foreach ($index in $MATCHES.number) {
+ Invoke-RestMethod -Method Patch -Headers $HEADERS -Uri $($ALERT_URL -f $index) -Body $BODY
+ }
+
+# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
\ No newline at end of file
diff --git a/src/.github/workflows/main.yml b/src/.github/workflows/main.yml
new file mode 100644
index 0000000..4edca64
--- /dev/null
+++ b/src/.github/workflows/main.yml
@@ -0,0 +1,402 @@
+name: Notepads CI/CD Pipeline
+
+on:
+ push:
+ #paths-ignore:
+ #- '**.md'
+ #- 'ScreenShots/**'
+ #- '.whitesource'
+ #- 'azure-pipelines.yml'
+ #- '.github/**'
+ #- '!.github/workflows/main.yml'
+ branches-ignore:
+ # PRs made by bots trigger both 'push' and 'pull_request' event, ignore 'push' event in that case
+ - 'dependabot**'
+ - 'imgbot**'
+ tags-ignore:
+ - '**'
+ pull_request:
+ paths-ignore:
+ - '**.md'
+ - 'ScreenShots/**'
+ - '.whitesource'
+ - 'azure-pipelines.yml'
+ - '.github/**'
+ - '!.github/workflows/main.yml'
+ workflow_dispatch:
+ inputs:
+ param:
+ description: Optional parameter for additional actions
+ # Type '(major|maj) (realease|rel)' or '(minor|min) (realease|rel)' or release for major,miner,patch release respectively
+ # Or explicitly provide version number to create release with that version
+ required: false
+ schedule:
+ - cron: '0 8 * * *'
+
+jobs:
+ setup:
+ runs-on: windows-latest
+ outputs:
+ matrix: ${{ steps.set_matrix.outputs.matrix }}
+ steps:
+ - name: Setup strategy matrix
+ id: set_matrix
+ shell: pwsh
+ run: |
+ $MATRIX = @{
+ include = @( [ordered]@{
+ configuration= "Debug"
+ appxBundlePlatforms = "x86|x64"
+ oldVersion = ""
+ newVersion = ""
+ debug = $true
+ runCodeqlAnalysis = $false
+ runSonarCloudScan = $false
+ }, [ordered]@{
+ configuration= "Release"
+ appxBundlePlatforms= "x86|x64|ARM64"
+ oldVersion = ""
+ newVersion = ""
+ debug = $true
+ runCodeqlAnalysis= $false
+ runSonarCloudScan= $false
+ }, [ordered]@{
+ configuration= "Production"
+ appxBundlePlatforms= "x86|x64|ARM64"
+ oldVersion = ""
+ newVersion = ""
+ debug = $true
+ runCodeqlAnalysis= $false
+ runSonarCloudScan= $false
+ }
+ )
+ }
+
+ if ( ( $env:GITHUB_EVENT -eq 'pull_request' ) `
+ -or ( $env:GITHUB_EVENT -eq 'schedule' ) `
+ -or ( $env:FORK -eq 'true' ) ) {
+ $MATRIX.include | Foreach-Object { $_.runSonarCloudScan = $false }
+ }
+
+ if ( ( $env:GITHUB_EVENT -ne 'push' ) `
+ -and ( $env:GITHUB_EVENT -ne 'pull_request' ) ) {
+ $MATRIX.include = @($MATRIX.include | Where-Object { $_.configuration -eq "$env:RELEASE_CONFIGURATION" })
+
+ if ( ( $env:GITHUB_EVENT -eq 'workflow_dispatch' ) `
+ -and ( $env:GITHUB_REF -eq 'refs/heads/master' ) ) {
+ $FETCH_URL = "https://api.github.com/repos/$env:GIT_REPOSITORY/tags?per_page=1"
+ $OLD_VER = [System.Version]::Parse($(Invoke-RestMethod -Method Get -Uri $FETCH_URL).name -replace 'v')
+
+ [System.Int32[]]$VER_INPUT = $($env:PARAM -replace '[a-zA-Z]| ').Split('.')
+ if ( ( $VER_INPUT.Count -gt 1 ) -or ( $VER_INPUT[0] -gt 0 ) ) {
+ $NEW_VER = [System.Version]::new($VER_INPUT[0],`
+ (if ( $VER_INPUT.Count -ge 1 ) { $VER_INPUT[1] } else { 0 }),`
+ (if ( $VER_INPUT.Count -ge 2 ) { $VER_INPUT[2] } else { 0 }),`
+ (if ( $VER_INPUT.Count -ge 3 ) { $VER_INPUT[3] } else { 0 }))
+ } elseif ( $env:PARAM -match 'rel' ) {
+ if ( $env:PARAM -match 'maj' ) {
+ $NEW_VER = [System.Version]::new($OLD_VER.Major + 1, 0, 0, 0)
+ } elseif ( $env:PARAM -match 'min' ) {
+ $NEW_VER = [System.Version]::new($OLD_VER.Major, $OLD_VER.Minor + 1, 0, 0)
+ } else {
+ $NEW_VER = [System.Version]::new($OLD_VER.Major, $OLD_VER.Minor, $OLD_VER.Build + 1, 0)
+ }
+ }
+
+ if ( ![System.String]::IsNullOrEmpty($OLD_VER) `
+ -and ![System.String]::IsNullOrEmpty($NEW_VER) `
+ -and ( $NEW_VER -gt $OLD_VER ) ) {
+ $MATRIX.include | Foreach-Object { $_.oldVersion = $OLD_VER.ToString() }
+ $MATRIX.include | Foreach-Object { $_.newVersion = $NEW_VER.ToString() }
+ }
+
+ $MATRIX.include | Foreach-Object { $_.runCodeqlAnalysis = $false }
+ $MATRIX.include | Foreach-Object {
+ if ( $_.configuration -eq "$env:RELEASE_CONFIGURATION" ) { $_.release = $true }
+ }
+ } else {
+ $MATRIX.include | Foreach-Object { $_.appxBundlePlatforms = 'x64' }
+ if ( $env:GITHUB_EVENT -ne 'schedule' ) {
+ $MATRIX.include | Foreach-Object { $_.runCodeqlAnalysis = $false }
+ }
+ }
+ }
+ echo "::set-output name=matrix::$($MATRIX | ConvertTo-Json -depth 32 -Compress)"
+ env:
+ FORK: ${{ github.event.repository.fork }}
+ PARAM: ${{ github.event.inputs.param }}
+ GITHUB_REF: ${{ github.ref }}
+ GITHUB_EVENT: ${{ github.event_name }}
+ RELEASE_CONFIGURATION: Production
+
+ ci:
+ needs: setup
+ runs-on: windows-latest
+ strategy:
+ matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
+ outputs:
+ old_version: ${{ matrix.oldVersion }}
+ new_version: ${{ matrix.newVersion }}
+ env:
+ SOLUTION_NAME: src\Notepads.sln
+ CONFIGURATION: ${{ matrix.configuration }}
+ DEFAULT_DIR: ${{ github.workspace }}
+ steps:
+ - if: matrix.runSonarCloudScan
+ name: Set up JDK 11
+ id: Setup_JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: 1.11
+
+ - name: Setup MSBuild
+ id: setup_msbuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Setup NuGet
+ id: setup-nuget
+ uses: NuGet/setup-nuget@v2.0.1
+
+ - name: Checkout repository
+ id: checkout_repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 50
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ # Due to the insufficient memory allocated by default, CodeQL sometimes requires more to be manually allocated
+ - if: matrix.runCodeqlAnalysis
+ name: Configure Pagefile
+ id: config_pagefile
+ uses: al-cheb/configure-pagefile-action@v1.4
+ with:
+ minimum-size: 8GB
+ maximum-size: 10GB
+
+ - if: matrix.newVersion != ''
+ name: Bump GitHub tag and Update manifest
+ id: tag_manifest_generator
+ shell: pwsh
+ run: |
+ git config --global user.name $env:GIT_USER_NAME
+ git config --global user.email $env:GIT_USER_EMAIL
+ git tag -a -m "$env:NEW_VERSION_TAG" $env:NEW_VERSION_TAG
+ git push --follow-tags
+ $xml = [xml](Get-Content $env:APPXMANIFEST_PATH)
+ $xml.Package.Identity.Version = $env:NEW_VERSION
+ $xml.save($env:APPXMANIFEST_PATH)
+ env:
+ GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }}
+ GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }}
+ APPXMANIFEST_PATH: src\Notepads\Package.appxmanifest
+ NEW_VERSION: ${{ matrix.newVersion }}
+ NEW_VERSION_TAG: v${{ matrix.newVersion }}
+
+ - if: matrix.runSonarCloudScan
+ name: Cache SonarCloud packages
+ id: cache_sonar_packages
+ uses: actions/cache@v4.2.3
+ with:
+ path: ~\sonar\cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - if: matrix.runSonarCloudScan
+ name: Cache SonarCloud scanner
+ id: cache_sonar_scanner
+ uses: actions/cache@v4.2.3
+ with:
+ path: .\.sonar\scanner
+ key: ${{ runner.os }}-sonar-scanner
+ restore-keys: ${{ runner.os }}-sonar-scanner
+
+ - if: matrix.runSonarCloudScan && steps.cache_sonar_scanner.outputs.cache-hit != 'true'
+ name: Install SonarCloud scanner
+ id: install_sonar_scanner
+ shell: pwsh
+ run: |
+ New-Item -Path .\.sonar\scanner -ItemType Directory
+ dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
+
+ - if: matrix.runSonarCloudScan
+ name: Initialize SonarCloud scanner
+ id: init_sonar_scanner
+ shell: pwsh
+ run: |
+ $LOWERCASE_REPOSITORY_NAME = "${{ github.event.repository.name }}".ToLower()
+ .\.sonar\scanner\dotnet-sonarscanner begin `
+ /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" `
+ /o:"$LOWERCASE_REPOSITORY_NAME" `
+ /d:sonar.login="$env:SONAR_TOKEN" `
+ /d:sonar.host.url="https://sonarcloud.io"
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - if: matrix.newVersion != ''
+ name: Create and validate PFX certificate for AppxBundle
+ id: create_validate_pfx_cert
+ shell: pwsh
+ run: |
+ $TARGET_FILE = "$env:DEFAULT_DIR\cert.pfx"
+ $FROM_BASE64_STR = [System.Convert]::FromBase64String($env:BASE64_STR)
+ [System.IO.File]::WriteAllBytes($TARGET_FILE, $FROM_BASE64_STR)
+
+ $FILE_STREAM = [System.IO.File]::OpenRead($TARGET_FILE)
+ $FILE_STREAM.Position = 0
+ $SHA256 = [System.Security.Cryptography.SHA256]::Create()
+ $HASH_BUILDER = [System.Text.StringBuilder]::new()
+ $SHA256.ComputeHash($FILE_STREAM) | ForEach-Object { $HASH_BUILDER.Append($_.ToString("x2")) }
+ if ( $HASH_BUILDER.ToString() -cne $env:SHA256_HASH ) {
+ throw [System.Exception]::new("Created certificate hash $($HASH_BUILDER.ToString()) $(
+ )doesn't match provided hash $($env:SHA256_HASH)")
+ }
+ env:
+ BASE64_STR: ${{ secrets.PACKAGE_CERTIFICATE_BASE64 }}
+ SHA256_HASH: ${{ secrets.PACKAGE_CERTIFICATE_SHA256 }}
+
+ - name: Restore the application
+ id: restore_application
+ shell: pwsh
+ run: |
+ msbuild $env:SOLUTION_NAME /t:Restore
+ nuget restore $env:SOLUTION_NAME
+
+ - if: matrix.runCodeqlAnalysis
+ name: Initialize CodeQL
+ id: init_codeql
+ uses: github/codeql-action/init@v3
+ with:
+ queries: security-and-quality
+ languages: csharp
+
+ - name: Build and generate bundles
+ id: build_app
+ shell: pwsh
+ run: |
+ msbuild $env:SOLUTION_NAME `
+ /p:Platform=$env:PLATFORM `
+ /p:Configuration=$env:CONFIGURATION `
+ /p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE `
+ /p:AppxBundle=$env:APPX_BUNDLE `
+ /p:AppxPackageSigningEnabled=$env:APPX_PACKAGE_SIGNING_ENABLED `
+ /p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS `
+ /p:AppxPackageDir=$env:ARTIFACTS_DIR `
+ /p:PackageCertificateKeyFile=$env:PACKAGE_CERTIFICATE_KEYFILE `
+ /p:PackageCertificatePassword=$env:PACKAGE_CERTIFICATE_PASSWORD
+ env:
+ PLATFORM: x64
+ UAP_APPX_PACKAGE_BUILD_MODE: StoreUpload
+ APPX_BUNDLE: Always
+ APPX_PACKAGE_SIGNING_ENABLED: ${{ matrix.newVersion != '' }}
+ APPX_BUNDLE_PLATFORMS: ${{ matrix.appxBundlePlatforms }}
+ ARTIFACTS_DIR: ${{ github.workspace }}\Artifacts
+ PACKAGE_CERTIFICATE_KEYFILE: ${{ github.workspace }}\cert.pfx
+ PACKAGE_CERTIFICATE_PASSWORD: ${{ secrets.PACKAGE_CERTIFICATE_PWD }}
+ APP_CENTER_SECRET: ${{ secrets.APP_CENTER_SECRET }}
+
+ - if: matrix.debug && !contains( matrix.appxBundlePlatforms, 'arm64' )
+ name: Test ARM build in debug configuration
+ id: build_app_arm_debug
+ shell: pwsh
+ run: |
+ msbuild $env:SOLUTION_NAME `
+ /p:Platform=$env:PLATFORM `
+ /p:Configuration=$env:CONFIGURATION `
+ /p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE `
+ /p:AppxBundle=$env:APPX_BUNDLE `
+ /p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS
+ env:
+ PLATFORM: ARM64
+ UAP_APPX_PACKAGE_BUILD_MODE: StoreUpload
+ APPX_BUNDLE: Always
+ APPX_BUNDLE_PLATFORMS: ARM64
+
+ - if: matrix.runCodeqlAnalysis
+ name: Perform CodeQL Analysis
+ id: analyze_codeql
+ uses: github/codeql-action/analyze@v3
+ continue-on-error: true
+
+ - if: matrix.runSonarCloudScan
+ name: Send SonarCloud results
+ id: send_sonar_results
+ shell: pwsh
+ run: |
+ .\.sonar\scanner\dotnet-sonarscanner end `
+ /d:sonar.login="$env:SONAR_TOKEN"
+ env:
+ GITHUB_TOKEN: ${{ secrets.SONAR_GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - if: matrix.newVersion != ''
+ name: Upload build artifacts
+ id: upload_artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: Build artifacts
+ path: Artifacts/
+
+ cd:
+ # This job will execute when the workflow is triggered on a 'workflow_dispatch' event,
+ # the target branch is 'master' and required parameter provided for release.
+ if: needs.ci.outputs.new_version != ''
+ needs: [ setup, ci ]
+ runs-on: windows-latest
+ env:
+ OLD_VERSION: ${{ needs.ci.outputs.old_version }}
+ NEW_VERSION: ${{ needs.ci.outputs.new_version }}
+ steps:
+ - name: Checkout repository
+ id: checkout_repo
+ uses: actions/checkout@v4
+
+ - name: Download and extract MSIX package
+ id: dl_package_artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: Build artifacts
+ path: Artifacts/
+
+ - name: Create deployment payload
+ id: create_notepads_zip
+ shell: pwsh
+ run: |
+ Get-ChildItem -Filter *Production* -Recurse | Rename-Item -NewName { $_.name -replace "_Production|_Test",'' }
+ Compress-Archive -Path "Notepads_$($env:NEW_VERSION)\*" `
+ -DestinationPath "Notepads_$($env:NEW_VERSION)\Notepads_$($env:NEW_VERSION)_x86_x64_ARM64.zip"
+ working-directory: ./Artifacts
+
+ - name: Generate changelog
+ id: generate_changlog
+ uses: mrchief/universal-changelog-action@v1.3.2
+ with:
+ previousReleaseTagNameOrSha: v${{ env.OLD_VERSION }}
+ nextReleaseTagName: v${{ env.NEW_VERSION }}
+ nextReleaseName: v${{ env.NEW_VERSION }}
+ configFilePath: .github/RELEASE_TEMPLATE/changelog_config.json
+
+ - name: Create and publish release
+ id: create_release
+ uses: ncipollo/release-action@v1.16.0
+ with:
+ allowUpdates: true
+ replacesArtifacts: true
+ tag: v${{ env.NEW_VERSION }}
+ name: Notepads v${{ env.NEW_VERSION }}
+ body: ${{ steps.generate_changlog.outputs.changelog }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ artifacts:
+ Artifacts/Notepads_${{ env.NEW_VERSION }}/Notepads_${{ env.NEW_VERSION }}_x86_x64_ARM64.msixbundle
+ Artifacts/Notepads_${{ env.NEW_VERSION }}/Notepads_${{ env.NEW_VERSION }}_x86_x64_ARM64.zip
+
+# - name: Publish to Windows Store
+# id: publish_to_store
+# uses: isaacrlevin/windows-store-action@1.0
+# with:
+# tenant-id: ${{ secrets.AZURE_AD_TENANT_ID }}
+# client-id: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }}
+# client-secret: ${{ secrets.AZURE_AD_APPLICATION_SECRET }}
+# app-id: ${{ secrets.STORE_APP_ID }}
+# package-path: "${{ github.workspace }}/Artifacts/"
+
+# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..a89b1c8
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1,355 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- Backup*.rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Logs and databases #
+######################
+*.log
+*.sql
+*.sqlite
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
diff --git a/src/.whitesource b/src/.whitesource
new file mode 100644
index 0000000..e0aaa3e
--- /dev/null
+++ b/src/.whitesource
@@ -0,0 +1,8 @@
+{
+ "checkRunSettings": {
+ "vulnerableCheckRunConclusionLevel": "failure"
+ },
+ "issueSettings": {
+ "minSeverityLevel": "LOW"
+ }
+}
\ No newline at end of file
diff --git a/src/CI-CD_DOCUMENTATION.md b/src/CI-CD_DOCUMENTATION.md
new file mode 100644
index 0000000..022ff80
--- /dev/null
+++ b/src/CI-CD_DOCUMENTATION.md
@@ -0,0 +1,419 @@
+# Notepads CI/CD documentation
+
+- after merging the PR, the first run of the "Notepads CI/CD Pipeline" workflow will not complete successfully, because it requires specific setup explained in this documentation. The two other workflows "CodeQL Analysis" and "Build", should complete successfully.
+
+## 1. Set up SonarCloud
+
+### SonarCloud is a cloud-based code quality and security service
+
+#### Create SonarCloud project
+
+- Go to https://sonarcloud.io/
+
+- Click the "Log in" button and create a new account or connect with GitHub account (recommended)
+
+- At the top right corner click the "+" sign
+
+- From the dropdown select "Create new Organization"
+
+- Click the "Choose an organization on GitHub" button
+
+- Select an account for the organization setup
+
+- On Repository Access select "Only select repositories" and select the project and click the "Save" button
+
+- On the "Create organization page" don't change the Key and click "Continue"
+
+- Select the Free plan then click the "Create Organization" button to finalize the creation of the Organization
+
+#### Configure SonarCloud project
+
+- At the top right corner click the "+" sign and select "Analyze new project"
+
+- Select the project and click the "Set Up" button in the box on the right
+
+- Under "Choose your analysis method" click "With GitHub Actions" and **keep the following page open**
+
+- [Create a new PAT with **repo_deployment** and **read:packages** permissions](#7-how-to-create-a-pat) and copy the value of the generated token
+
+- In the project's GitHub repository, go to the **Settings** tab -> Secrets
+
+- Click on **New Repository secret** and create a new secret with the name **SONAR_GITHUB_TOKEN** and the token you just copied as the value
+
+- Create another secret with the two values from the SonarCloud page you kept open, which you can close after completing this step
+
+
+
+- [Run the "Notepads CI/CD Pipeline" workflow manually](#2-run-workflow-manually)
+
+#### Set Quality Gate
+
+- After the "Notepads CI/CD Pipeline" workflow has executed successfully, go to https://sonarcloud.io/projects and click on the project
+
+- In the alert bar above the results, click the "Set new code definition" button and select "Previous version" (notice the "New Code definition has been updated" alert at the top)
+
+- The Quality Gate will become active as soon as the next SonarCloud scan completes successfully
+
+
+
+
+## 2. Run workflow manually
+
+Once you've set up all the steps above correctly, you should be able to successfully complete a manual execution of the "Notepads CI/CD Pipeline" workflow.
+
+1. Go to the project's GitHub repository and click on the **Actions** tab
+
+2. From the "Workflows" list on the left, click on "Notepads CI/CD Pipeline"
+
+3. On the right, next to the "This workflow has a workflow_dispatch event trigger" label, click on the "Run workflow" dropdown, make sure the default branch is selected (if not manually changed, should be main or master) in the "Use workflow from" dropdown and click the "Run workflow" button
+
+4. You can optionally fill the argument textbox with "release" to trigger [GitHub Release](#github_release) and [Store Upload](#store_upload)
+
+
+
+5. Once the workflow run has completed successfully, move on to the next step of the documentation
+
+NOTE: **screenshots are only exemplary**
+
+
+
+## 3. Set up Dependabot
+
+Dependabot is a GitHub native security tool that goes through the dependencies in the project and creates alerts, and PRs with updates when a new and/or non-vulnerable version is found.
+
+- for PRs with version updates, this pipeline comes pre-configured for all current dependency sources in the project, so at "Insights" tab -> "Dependency graph" -> "Dependabot", you should be able to see all tracked sources of dependencies, when they have been checked last and view a full log of the last check
+
+
+
+
+
+### Set up security alerts and updates
+
+##### - GitHub, through Dependabot, also natively offers a security check for vulnerable dependencies
+
+1. Go to "Settings" tab of the repo
+
+2. Go to "Security & analysis" section
+
+3. Click "Enable" for both "Dependabot alerts" and "Dependabot security updates"
+
+- By enabling "Dependabot alerts", you would be notified for any vulnerable dependencies in the project. At "Security" tab -> "Dependabot alerts", you can manage all alerts. By clicking on an alert, you would be able to see a detailed explanation of the vulnerability and a viable solution.
+
+
+
+
+
+- By enabling "Dependabot security updates", you authorize Dependabot to create PRs specifically for **security updates**
+
+
+
+### Set up Dependency graph
+
+##### - The "Dependency graph" option should be enabled by default for all public repos, but in case it isn't:
+
+1. Go to "Settings" tab of the repo
+
+2. Go to "Security&Analysis" section
+
+3. Click "Enable" for the "Dependency graph" option
+
+- this option enables the "Insights" tab -> "Dependency graph" section -> "Dependencies" tab, in which all the dependencies for the project are listed, under the different manifests they are included in
+
+
+
+NOTE: **screenshots are only exemplary**
+
+
+
+## 4. CodeQL
+
+CodeQL is GitHub's own industry-leading semantic code analysis engine. CodeQL requires no setup, because it comes fully pre-configured by us.
+
+To activate it and see its results, only a push commit or a merge of a PR to the default branch of the repository, is required.
+
+We've also configured CodeQL to run on schedule, so every day at 8:00AM UTC, it automatically scans the code.
+
+- you can see the results here at **Security** tab -> **Code scanning alerts** -> **CodeQL**:
+
+
+
+- on the page of each result, you can see an explanation of what the problem is and also one or more solutions:
+
+
+
+### Code scanning alerts bulk dismissal tool
+
+##### - currently, GitHub allows for only 25 code scanning alerts to be dismissed at a time. Sometimes, you might have hundreds you would like to dismiss, so you will have to click many times and wait for a long time to dismiss them. Via the "csa-bulk-dismissal.yml", you can automatically dismiss unnecessary alerts or manually do that with one click.
+
+NOTE: This tool executes automatically when **Notepads CI/CD Pipeline** action completes.
+
+#### 1. Setup
+
+1. In the repository, go to the **Settings** tab -> **Secrets**
+
+
+
+2. Add the following secrets with the name and the corresponding value, by at the upper right of the section, clicking on the **New repository secret** button:
+
+
+
+
+
+- CSA_ACCESS_TOKEN - [create a PAT with "security_events" permission only](#7-how-to-create-a-pat).
+
+#### 2. Execution
+
+1. This tool is automatically triggered when **Notepads CI/CD Pipeline** task completes, if you want to manually execute this follow next steps
+
+2. In your repo, click on the Actions tab and on the left, in the Workflows list, click on the "Code scanning alerts bulk dismissal"
+
+
+
+3. On the right, click on the "Run workflow" dropdown. Under "Use workflow from" choose your default branch (usually main/master), in the **Type of filter to use** field type "path"/"desc" depending upon whether dismiss alerts based on predefined paths or description respectively (default is "path"), in the **Reason for dismissal** type "fp"/"wf"/"ut" for "false positive"/"won't fix"/"used in tests" respectively (default is "wf") and click on the **Run workflow** button
+
+
+
+
+NOTE: if any unsupported values are entered default values will be used
+
+4. If everything was set up currently in the "Setup" phase, the "Code scanning alerts bulk dismissal" workflow is going to be executed successfully, which after some time, would result in **all** previously open code scanning alerts, with a certain description be dismissed
+
+
+
+
+
+
+
+NOTE: "closed" refers to "dismissed" alerts
+
+#### 3. Customization
+
+The "setup" job in the pipeline, allows for more precise filtering of alerts to bulk dismiss. It uses the filter type to choose (filter based on path or description) from the alert to determine if it has to be dismissed or not. We've added the following paths and alert descriptions by default:
+
+##### Paths:
+
+- "\*/obj/\*" (if path contains `obj` folder at any position)
+
+##### Descriptions:
+
+- "Calls to unmanaged code"
+- "Unmanaged code"
+
+##### To add more paths, follow these steps:
+
+1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
+
+2. From line 50 to 56, notice "$MATRIX = **". This is the [powershell hashtable](https://docs.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1) of filters that the CSABD (Code scanning alerts bulk dismissal) tool uses to filter through the alerts:
+
+
+
+3. To add more paths under **include** element use comma separation and followed from next line add `@{ filter = "New path" }`. Replace "New path" with the path (with or without [wild cards](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_wildcards?view=powershell-7.1)) you want:
+
+
+
+##### To add more descriptions, follow these steps:
+
+1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
+
+2. From line 58 to 67, notice "$MATRIX = **". This is the [powershell hashtable](https://docs.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1) of filters that the CSABD (Code scanning alerts bulk dismissal) tool uses to filter through the alerts:
+
+
+
+3. To add more descriptions under **include** element use comma separation and followed from next line add `@{ filter = "New description" }`. Replace "New description" with the description you want:
+
+
+
+##### To change default filter type and dismissal reason, follow these steps:
+
+1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
+
+2. To change default filter type change **$FILTER_TYPE** variable in line 31 to something else (default is "path", supported are: "desc" and "path"):
+
+
+
+3. To change dismissal reason change **$REASON** variable in line 45 to something else (default is "won't fix", supported are: "false positive", "won't fix" and "used in tests"):
+
+
+
+NOTE: changing default filter type and dismissal reason won't change dafault value typed when [manually executing](#csa_execute) tool, change values in line 13 and 17 respectively to reflect the change
+
+
+
+
+
+
+## 5. Automated GitHub release
+
+When triggered bumps up the GitHub tag in the repo and executes the CD job and produces release with changelogs
+
+Note: **not every commit to your master branch are included in changelog**
+
+#### Setup
+
+Add the following secrets by going to the repo **Settings** tab -> **Secrets**:
+
+1. **PACKAGE_CERTIFICATE_BASE64**
+
+- used to dynamically create the PFX file required for the signing of the **msixbundle**
+- use the following PowerShell code locally to turn your PFX file into Base64:
+
+```
+# read from PFX as binary
+$PFX_FILE = [IO.File]::ReadAllBytes('absolute_path_to_PFX')
+# convert to Base64 and write in txt
+[System.Convert]::ToBase64String($PFX_FILE) | Out-File 'absolute_path\cert.txt'
+```
+
+- copy the contents of the **cert.txt** and paste as the value of the secret
+
+2. **PACKAGE_CERTIFICATE_PWD**
+
+- used in the build of the project to authenticate the PFX
+- copy and paste the password of your PFX as the value of this secret
+
+NOTE:
+
+- none of those values are visible in the logs of the pipeline, nor are available to anyone outside of the original repository e.g. forks, anonymous clones etc.
+- the dynamically created PFX file lives only for the duration of the pipeline execution
+
+#### Execution
+
+[Once you've set up all the steps for manual execution of the "Notepads CI/CD Pipeline" workflow correctly](#workflow_dispatch), you should be able to successfully trigger release with the same workflow.
+
+1. Go to the project's GitHub repository and click on the **Actions** tab
+
+2. From the "Workflows" list on the left, click on "Notepads CI/CD Pipeline"
+
+3. On the right, next to the "This workflow has a workflow_dispatch event trigger" label, click on the "Run workflow" dropdown, make sure the default branch is selected (if not manually changed, should be main or master) in the "Use workflow from" dropdown, type "release" in the argument textbox (By default "test" is typed) and click the "Run workflow" button
+
+
+
+4. The workflow will produce release assets and calculate version, generate changelogs from valid commits since previous tag.
+
+NOTE: **screenshots are only exemplary**
+
+
+
+#### - follow these instructions for any commit (push or PR merge) to your master branch, you would like to see in changelog and count towards version change.
+
+You would need one of three keywords at the start of your commit title. Each of the three keywords corresponds to a number in your release version i.e. v1.2.3. The release versioning uses the ["Conventional Commits" specification](https://www.conventionalcommits.org/en/v1.0.0/):
+
+- "fix: ..." - this keyword corresponds to the last number v1.2.**3**, also known as PATCH;
+- "feat: ..." - this keyword corresponds to the middle number v1.**2**.3, also known as MINOR;
+- "perf: ..." - this keyword corresponds to the first number v**1**.2.3, also known as MAJOR. In addition, to trigger a MAJOR release, you would need to write "BREAKING CHANGE: ..." in the description of the commit, with an empty line above it to indicate it is in the portion of the description;
+
+Note: when making a MAJOR release by committing through a terminal, use the multiple line syntax to add the commit title on one line and then adding an empty line, and then adding the "BREAKING CHANGE: " label
+
+
+#### Examples
+
+Example(fix/PATCH):
+`git commit -a -m "fix: this is a PATCH release triggering commit"`
+
+`git push origin master`
+
+
+On triggering `Release`:
+
+Result: v1.2.3 -> **v1.2.4**
+
+
+
+Example(feat/MINOR):
+`git commit -a -m "feat: this is a MINOR release triggering commit"`
+
+`git push origin master`
+
+
+On triggering `Release`:
+
+Result: v1.2.3 -> **v1.3.0**
+
+
+
+Example(perf/MAJOR):
+`` git commit -a -m "perf: this is a MAJOR release triggering commit ` ``
+
+>>
+>> `BREAKING CHANGE: this is the breaking change"`
+
+`git push origin master`
+
+
+On triggering `Release`:
+
+Result: v1.2.3 -> **v2.0.0**
+
+
+Note: in the MAJOR release example, the PowerShell multiline syntax ` (backtick) is used. After writing a backtick, a press of the Enter key should open a new line.
+
+
+
+
+## 6. Setup automated publishing to the Windows Store
+
+#### - for the automation to work, at least one submission needs to be already created manually
+
+- [Create an Azure AD tenant](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant) or use an existing one
+
+- Associate your [Microsoft Partner Center with the Azure AD tenant](https://docs.microsoft.com/en-us/windows/uwp/publish/associate-azure-ad-with-partner-center)
+
+- [Create a new app registration](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml) or use an existing one from the list in your **portal.azure.com** -> **Azure Active Directory** -> **App registrations** section
+
+- Add the [Azure AD application to the Microsoft Partner Center](https://docs.microsoft.com/en-us/partner-center/service-principal) and give it "Manager" permissions
+
+- In the project's GitHub repo, create the following secrets:
+
+1. **AZURE_AD_TENANT_ID** and **AZURE_AD_APPLICATION_CLIENT_ID**
+
+ - copy and paste the values shown in the screenshot below to the appropriate secret:
+
+
+
+Note: screenshot is taken from **portal.azure.com** -> **Azure AD** -> **App registrations** -> **app-name** page
+
+2. **AZURE_AD_APPLICATION_SECRET**
+
+ - copy and paste the value you get on the page following from **Account settings** -> **User management** -> **Azure AD applications** -> click on the added application:
+
+
+
+3. **STORE_APP_ID**
+
+ - copy and paste the highlighted code as the value of this secret:
+
+
+
+- If everything was setup correctly, on your next push commit to the `master` branch with a new `Identity.Version` in the `Package.appxmanifest`, a new submission in the Microsoft Partner Center with the new `*.msixupload` package should appear and be automatically submitted if all verifications pass
+
+
+
+## 7. How to create a PAT
+
+- In a new tab open GitHub, at the top right corner, click on your profile picture and click on **Settings** from the dropdown.
+
+ 
+
+- Go to Developer Settings -> Personal access tokens.
+
+ 
+
+ 
+
+- Click the **Generate new token** button and enter password if prompted.
+
+ 
+
+- Name the token, from the permissions list choose the ones needed and at the bottom click on the **Generate token** button.
+
+ 
+
+- Copy the token value and paste it wherever its needed
+
+ 
+
+NOTE: once you close or refresh the page, you won't be able to copy the value of the PAT again!
+
+#
+
+Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
diff --git a/src/CODE_OF_CONDUCT.md b/src/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..5242dd8
--- /dev/null
+++ b/src/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at NotepadsApp@outlook.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/src/CONTRIBUTING.md b/src/CONTRIBUTING.md
new file mode 100644
index 0000000..6b3007f
--- /dev/null
+++ b/src/CONTRIBUTING.md
@@ -0,0 +1,35 @@
+# How to Contribute:
+
+You can contribute to Notepads project by:
+- Report issues and bugs [here](https://github.com/0x7c13/Notepads/issues)
+- Submit feature requests [here](https://github.com/0x7c13/Notepads/issues)
+- Create a pull request to help me (Let me know before you do so):
+ * Fix an existing bug, prefix title with `fix: `.
+ * Implement new features, prefix title with `feat: `.
+ * Fix grammar errors or improve my documentations, prefix title with `doc: `.
+ * Improve CI/CD pipeline, prefix title with `ci: `.
+ * Cleanup code and code refactoring or anything else you want to change in the project not listed above, prefix title with `other: ` or assign a custom prefix with the same format (`label: `).
+- Internationalization and localization:
+ * My only inputs for the work here is to recommend you guys to use existing phrases that you found in win32 notepad.exe or vs code or notepad++ as much as possible. It makes your translations more consistent and easier to understand by end users.
+ * Since Notepads is still in early beta. I might change texts and add texts now and then for the upcoming months. Whenever that happens, I will notify you in [Notepads Discord Server](https://discord.gg/VqetCub) (Please join it if possible) and in [GitHub Discussions](https://github.com/0x7c13/Notepads/discussions/818) (Subscribe to notifications). If someday you lose the passion, feel free to let me know so I can assign your language to others.
+ * OK, here are the steps you need to follow if you want to contribute:
+ 1. Make sure you can build and run Notepads project on your machine so that you can test it after your work.
+ 2. Click [here](https://github.com/0x7c13/Notepads/discussions/818) and provide your information.
+ 3. Do your work and test it on your machine and check your work to make sure it is not breaking any existing layout.
+ 4. Finish your work and create a PR, prefix PR title with `lang: ` (Example: https://github.com/0x7c13/Notepads/pull/30)
+ 5. Let me know and I will merge it if it looks good to me.
+ Notes: You should use the language code as your folder name listed here: https://docs.microsoft.com/en-us/windows/uwp/publish/supported-languages
+
+Note: This repository follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), format your pull request title according to specifications.
+
+# How to Build and Run Notepads from source:
+* Make sure your machine is running on Windows 10 1903+.
+* Make sure you have Visual Studio 2019 16.2 or newer installed.
+* Make sure you have "Universal Windows Platform development" component installed for Visual Studio.
+* Make sure you installed "Windows 10 SDK (10.0.17763.0 + 10.0.19041.0)" as well.
+* Open src/Notepads.sln with Visual Studio and set Solution Platform to x64(amd64).
+* Once opened, right click on the solution and click on "Restore NuGet Packages".
+* Now you should be able to build and run Notepads on your machine. If it fails, try close the solution and reopen it again.
+
+# TL;DR:
+This is my first UWP project and I learn as I go. As a result, the code base is not well organized, and it is not well written. The philosophy here is to create a text editor that is easy to use, lightweight and yet stylish instead of creating another Notepad++ or VS Code in anyway. If you are looking for a code/programming editor, you might want to use VS Code instead. If you are looking for a lightweight text editor, you come to the right place. Notepads is here to help you do small things quicker and you should always install and use other editors that suit your need.
diff --git a/src/LICENSE.txt b/src/LICENSE.txt
new file mode 100644
index 0000000..1173911
--- /dev/null
+++ b/src/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-2024 Jackie (Jiaqi) Liu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/Notepads b/src/Notepads
deleted file mode 160000
index 39a2159..0000000
--- a/src/Notepads
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 39a2159ca24d2c04889b651c41889c72a7a9db43
diff --git a/src/PRIVACY.md b/src/PRIVACY.md
new file mode 100644
index 0000000..366163b
--- /dev/null
+++ b/src/PRIVACY.md
@@ -0,0 +1,24 @@
+Privacy Policy
+----------------
+
+### Introduction
+Our privacy policy will help you understand what information we collect by *[Notepads](https://github.com/0x7c13/Notepads)* app, how *[Notepads](https://github.com/0x7c13/Notepads)* app uses it, and what choices you have.
+*[0x7c13](https://github.com/0x7c13)* with the help of the Github Notepads App community built the *[Notepads](https://github.com/0x7c13/Notepads)* app as a free app. This APP is provided by *[0x7c13](https://github.com/0x7c13)* at no cost and is intended for use as is.
+If you choose to use this app, then you agree to the collection and use of information in relation with this policy. The data that we collect are used for providing and improving the app service. We will not use or share your information or usage data with anyone except as described in this Privacy Policy.
+
+### Information Collection and Use
+For a better experience while using this app, certain usage data and errors are collected for identifying issues or improving the user experience of the app. *[Visual Studio AppCenter](https://visualstudio.microsoft.com/app-center/)* analytics service is used in this app to collect basic usage data plus some minimum telemetry to help debug runtime errors. See thread [#334](https://github.com/0x7c13/Notepads/issues/334) for more details.
+The app does NOT use third party services that may collect information used to identify you.
+
+Note: Visual Studio App Center is scheduled for retirement on March 31, 2025. Notepads v1.5.6.0+ starts to use Microsoft Store Services SDK to log non-privacy usage data and errors.
+
+### Data Security
+We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
+
+### Changes to This Privacy Policy
+We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately, after they are posted on this page.
+
+### Contact Us
+If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.
+Contact Information:
+Email: notepadsapp@outlook.com
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..f977697
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,145 @@
+
+
+
+
+ Notepads
+
+
+ A modern, lightweight text editor with a minimalist design.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## What is Notepads and why do I care?
+
+I have been waiting long enough for a modern Windows 10 notepad app to come before I decided to create one myself. Don’t get me wrong, Notepad++, VS Code, and Sublime are great text editors. I have used them all and I will continue to use them in the future. However, they are either too heavy or look less appealing. There are times that I just wanted to use Windows notepad for things like writing notes or editing config files. So I decided to create a win32 notepad replacement here and try to give it a modern look and feel. Most importantly, it has to be blazingly fast and appeals to everyone.
+
+So here comes the “Notepads” 🎉 (s stands for Sets).
+
+* Fluent design with a built-in tab system.
+* Blazingly fast and lightweight.
+* Launch from the command line or PowerShell by typing: `notepads` or `notepads %FilePath%`.
+* Multi-line handwriting support.
+* Built-in Markdown live preview.
+* Built-in diff viewer (preview your changes).
+* Session snapshot and multi-instances.
+
+
+
+
+
+
+## Shortcuts:
+
+* Ctrl+N/T to create new tab.
+* Ctrl+(Shift)+Tab to switch between tabs.
+* Ctrl+Num(1-9) to quickly switch to specified tab.
+* Ctrl+"+"/"-" for zooming. Ctrl+"0" to reset zooming to default.
+* Ctrl+L/R to change text flow direction. (LTR/RTL)
+* Alt+P to toggle preview split view for Markdown file.
+* Alt+D to toggle side-by-side diff viewer.
+
+## Platform limitations (UWP):
+
+* You won't be able to save files to system folders due to UWP restriction (windows, system32, etc.).
+* You cannot associate potentially harmful file types (.cmd, .bat etc.) with Notepads.
+* Notepads does not work well with large files; the file size limit is set to 1MB for now. I will add large file support later.
+
+## Downloads:
+
+Notepads is available in the Microsoft Store. You can get the latest version of Notepads here for free: [Microsoft Store Link](https://www.microsoft.com/store/apps/9nhl4nsc67wm).
+
+You can also use the Windows Package Manager to install notepads:
+```cmd
+winget install "Notepads App"
+```
+
+## Changelog:
+
+* [Notepads Releases](https://github.com/0x7c13/Notepads/releases)
+
+## Disclaimer and Privacy statement:
+
+To be 100% transparent:
+
+* Notepads does not and will never collect user information in terms of user privacy.
+* I will not track your IP.
+* I will not record your typings or read any of your files created in Notepads including file name and file path.
+* No typings or files will be sent to me or third parties.
+
+I am using analytics service "AppCenter" to collect basic usage data plus some minimum telemetry to help me debug runtime errors. Here is the thread I made clear on this topic: https://github.com/0x7c13/Notepads/issues/334
+
+Feel free to review the source code or build your own version of Notepads since it is 100% open sourced.
+
+#### More to read here: [[Privacy Policy](PRIVACY.md)]
+
+TL;DR: You might notice that I work for Microsoft but Notepads is my personal project that I accomplish during free time (to empower every person and every organization on the planet to achieve more😃). I do not work for the Windows team, nor do I work for a Microsoft UX/App team. I am not expert on creating Windows apps either. I learned how to code UWP as soon as I started this project, so don’t put too much hope on me or treat it as a project sponsored by Microsoft.
+
+## Contributing:
+
+* [How to contribute?](CONTRIBUTING.md)
+* Notepads is free and open source, if you like my work, please consider:
+ * Star this project on GitHub
+ * Leave me a review [here](https://www.microsoft.com/store/apps/9nhl4nsc67wm)
+ * [](https://ko-fi.com/D1D6Y3C6)
+
+## Dependencies and References:
+* [Windows Community Toolkit](https://github.com/windows-toolkit/WindowsCommunityToolkit)
+* [XAML Controls Gallery](https://github.com/microsoft/Xaml-Controls-Gallery)
+* [Windows UI Library](https://github.com/Microsoft/microsoft-ui-xaml)
+* [ColorCode Universal](https://github.com/WilliamABradley/ColorCode-Universal)
+* [UTF Unknown](https://github.com/CharsetDetector/UTF-unknown)
+* [DiffPlex](https://github.com/mmanela/diffplex)
+* [Win2D](https://github.com/microsoft/Win2D)
+
+## Special Thanks:
+
+* [Yi Zhou](http://zhouyiwork.com/) - App icon designer, Notepads App Icon (old) is greatly inspired by the new icon for Windows Terminal.
+* [Mahmoud Qurashy](https://github.com/mah-qurashy) - App icon and file icon(s) designer, creator of the new Notepads App Icon.
+
+* Alexandru Sterpu - App Tester, who helped me a lot during preview/beta testing.
+* Code Contributors: [DanverZ](https://github.com/chenghanzou), [BernhardWebstudio](https://github.com/BernhardWebstudio), [Csányi István](https://github.com/AmionSky), [Pavel Erokhin](https://github.com/MairwunNx), [Sergio Pedri](https://github.com/Sergio0694), [Lucas Pinho B. Santos](https://github.com/pinholucas), [Soumya Ranjan Mahunt](https://github.com/soumyamahunt), [Belleve Invis](https://github.com/be5invis), [Maickonn Richard](https://github.com/Maickonn), [Xam](https://github.com/XamDR)
+* Documentation Contributors: [Craig S.](https://github.com/sercraig)
+* Localization Contributors:
+ * [fr-FR][French (France)]: [François Rousselet](https://github.com/frousselet), [François-Joseph du Fou](https://github.com/FJduFou), [Armand Delessert](https://github.com/ArmandDelessert)
+ * [es-ES][Spanish (Spain)]: [Jose Pinilla](https://github.com/joseppinilla)
+ * [zh-CN][Chinese (S)]: [lindexi](https://github.com/lindexi), [walterlv](https://github.com/walterlv), [0x7c13](https://github.com/0x7c13)
+ * [hu-HU][Hungarian (Hungary)]: [Csányi István](https://github.com/AmionSky), [Kristóf Kékesi](https://github.com/KristofKekesi)
+ * [tr-TR][Turkish (Turkey)]: [Mert Can Demir](https://github.com/validatedev), [Emirhakan Tanhan](https://github.com/EmirhakanTanhan)
+ * [ja-JP][Japanese (Japan)]: [Mamoru Satoh](https://github.com/pnp0a03)
+ * [de-DE][German (Germany)]/[de-CH][German (Switzerland)]: [Walter Wolf](https://github.com/WalterWolf49)
+ * [ru-RU][Russian (Russia)]: [Pavel Erokhin](https://github.com/MairwunNx), [krlvm](https://github.com/krlvm)
+ * [fi-FI][Finnish (Finland)]: [Esa Elo](https://github.com/sauihdik)
+ * [uk-UA][Ukrainian (Ukraine)]: [Taras Fomin aka Tarik02](https://github.com/Tarik02)
+ * [it-IT][Italian (Italy)]: [Andrea Guarinoni](https://github.com/guari), [Bunz](https://github.com/66Bunz)
+ * [cs-CZ][Czech (Czech Republic)]: [Jan Rajnoha](https://github.com/JanRajnoha)
+ * [pt-BR][Portuguese (Brazil)]: [Lucas Pinho B. Santos](https://github.com/pinholucas)
+ * [ko-KR][Korean (Korea)]: [Donghyeok Tak](https://github.com/tdh8316)
+ * [hi-IN][Hindi (India)]/[or-IN][Odia (India)]: [Soumya Ranjan Mahunt](https://github.com/soumyamahunt)
+ * [pl-PL][Polish (Poland)]: [Daxxxis](https://github.com/Daxxxis)
+ * [ka-GE][Georgian (Georgia)]: [guram mazanashvili](https://github.com/gmaza)
+ * [hr-HR][Croatian (Croatia)]: [milotype](https://github.com/milotype)
+ * [zh-TW][Chinese (T)]: [Tony Yao](https://github.com/SeaBao)
+ * [pt-PT][Portuguese (Portugal)]: [O.Leitão](https://github.com/oleitao)
+ * [sr-Latn][Serbian (Latin)]: [bzzrak](https://github.com/bzzrak)
+ * [sr-cyrl][Serbian (Cyrillic)]: [bzzrak](https://github.com/bzzrak)
+ * [nl-NL][Dutch (Netherlands)]: [Stephan Paternotte](https://github.com/Stephan-P)
+
+* Notepads CI/CD pipeline: Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
+
+[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/0)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/1)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/2)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/3)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/4)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/5)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/6)[](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/7)
+
+## Stay tuned 📢:
+* [Notepads Discord Server](https://discord.gg/VqetCub)
diff --git a/src/ScreenShots/1.png b/src/ScreenShots/1.png
new file mode 100644
index 0000000..3674463
Binary files /dev/null and b/src/ScreenShots/1.png differ
diff --git a/src/ScreenShots/2.png b/src/ScreenShots/2.png
new file mode 100644
index 0000000..bbb0c5f
Binary files /dev/null and b/src/ScreenShots/2.png differ
diff --git a/src/ScreenShots/3.png b/src/ScreenShots/3.png
new file mode 100644
index 0000000..2853fb3
Binary files /dev/null and b/src/ScreenShots/3.png differ
diff --git a/src/ScreenShots/4.png b/src/ScreenShots/4.png
new file mode 100644
index 0000000..4f853ec
Binary files /dev/null and b/src/ScreenShots/4.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Actions_workflow_dispatch.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Actions_workflow_dispatch.png
new file mode 100644
index 0000000..8c4860e
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Actions_workflow_dispatch.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_1.png
new file mode 100644
index 0000000..c3f8c40
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_1.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_2.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_2.png
new file mode 100644
index 0000000..2d3a6cc
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_2.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_3.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_3.png
new file mode 100644
index 0000000..1f08cf4
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_3.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_4.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_4.png
new file mode 100644
index 0000000..4b9a836
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_4.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_5.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_5.png
new file mode 100644
index 0000000..070e1e7
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_5.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_6.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_6.png
new file mode 100644
index 0000000..1a24636
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_6.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_7.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_7.png
new file mode 100644
index 0000000..00ff62f
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_7.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_1.png
new file mode 100644
index 0000000..ef13b05
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_1.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_2.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_2.png
new file mode 100644
index 0000000..6aa5cf3
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_2.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_3.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_3.png
new file mode 100644
index 0000000..6b93593
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_3.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_4.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_4.png
new file mode 100644
index 0000000..770e3ff
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_4.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_5.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_5.png
new file mode 100644
index 0000000..d1a5d1f
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_5.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_1.png
new file mode 100644
index 0000000..ce90051
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_1.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_2.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_2.png
new file mode 100644
index 0000000..1f64440
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_2.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_3.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_3.png
new file mode 100644
index 0000000..e6d68f7
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_3.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_4.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_4.png
new file mode 100644
index 0000000..d3fb2d1
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_4.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_5.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_5.png
new file mode 100644
index 0000000..f2ccd44
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_5.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_6.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_6.png
new file mode 100644
index 0000000..8a4f6b8
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_6.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_secret.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_secret.png
new file mode 100644
index 0000000..65929a1
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_new_secret.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secret_add.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secret_add.png
new file mode 100644
index 0000000..ec35a98
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secret_add.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secrets.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secrets.png
new file mode 100644
index 0000000..544426d
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_secrets.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_owner.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_owner.png
new file mode 100644
index 0000000..2071c20
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_owner.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_repo.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_repo.png
new file mode 100644
index 0000000..5efc634
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CSA_url_repo.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_alert_page.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_alert_page.png
new file mode 100644
index 0000000..818ae78
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_alert_page.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_results.png b/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_results.png
new file mode 100644
index 0000000..a950861
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/CodeQL_results.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_PRs.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_PRs.png
new file mode 100644
index 0000000..9972ada
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_PRs.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alert_page.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alert_page.png
new file mode 100644
index 0000000..4b8faf0
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alert_page.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alerts_page.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alerts_page.png
new file mode 100644
index 0000000..d59f206
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alerts_page.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_dependency_graph.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_dependency_graph.png
new file mode 100644
index 0000000..c94e96d
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_dependency_graph.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_log_page.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_log_page.png
new file mode 100644
index 0000000..8035fa2
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_log_page.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_tab.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_tab.png
new file mode 100644
index 0000000..cf138ec
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Dependabot_tab.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_1.png
new file mode 100644
index 0000000..7fd1cf4
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_1.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_2.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_2.png
new file mode 100644
index 0000000..f90b541
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_2.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_3.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_3.png
new file mode 100644
index 0000000..9dc866b
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_3.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Release_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Release_1.png
new file mode 100644
index 0000000..2fd10d1
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Release_1.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/Release_2.png b/src/ScreenShots/CI-CD_DOCUMENTATION/Release_2.png
new file mode 100644
index 0000000..2a31c82
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/Release_2.png differ
diff --git a/src/ScreenShots/CI-CD_DOCUMENTATION/SonarCloud_1.png b/src/ScreenShots/CI-CD_DOCUMENTATION/SonarCloud_1.png
new file mode 100644
index 0000000..48f3df8
Binary files /dev/null and b/src/ScreenShots/CI-CD_DOCUMENTATION/SonarCloud_1.png differ
diff --git a/src/azure-pipelines.yml b/src/azure-pipelines.yml
new file mode 100644
index 0000000..2d0ced9
--- /dev/null
+++ b/src/azure-pipelines.yml
@@ -0,0 +1,39 @@
+# Universal Windows Platform build definition
+
+trigger:
+ paths:
+ exclude:
+ - '*.md'
+ - 'ScreenShots/'
+ - '.whitesource'
+ - '.github/'
+ tags:
+ exclude:
+ - '*'
+
+pool:
+ vmImage: 'windows-latest'
+
+variables:
+ solution: '**/*.sln'
+ buildPlatform: 'x86|x64|arm64'
+ buildConfiguration: 'Production'
+ appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\'
+
+steps:
+- task: NuGetToolInstaller@1
+
+- task: NuGetCommand@2
+ inputs:
+ restoreSolution: '$(solution)'
+
+- task: VSBuild@1
+ inputs:
+ platform: 'x64'
+ solution: '$(solution)'
+ configuration: '$(buildConfiguration)'
+ msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)"
+ /p:AppxPackageDir="$(appxPackageDir)"
+ /p:AppxBundle=Always
+ /p:UapAppxPackageBuildMode=StoreUpload
+ /p:AppxPackageSigningEnabled=false'
diff --git a/src/src/.editorconfig b/src/src/.editorconfig
new file mode 100644
index 0000000..fa05f37
--- /dev/null
+++ b/src/src/.editorconfig
@@ -0,0 +1,323 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:silent
+csharp_style_throw_expression = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:suggestion
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = true:silent
+csharp_style_var_for_built_in_types = true:silent
+csharp_style_var_when_type_is_apparent = true:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:silent
+csharp_style_pattern_matching_over_is_with_cast_check = true:silent
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
+
+# Code-block preferences
+csharp_prefer_braces = false:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+
+# Naming Symbols
+# constant_fields - Define constant fields
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+# non_private_readonly_fields - Define public, internal and protected readonly fields
+dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected
+dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
+# static_readonly_fields - Define static and readonly fields
+dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly
+# private_readonly_fields - Define private readonly fields
+dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly
+# public_internal_fields - Define public and internal fields
+dotnet_naming_symbols.public_internal_protected_fields.applicable_accessibilities = public, internal, protected
+dotnet_naming_symbols.public_internal_protected_fields.applicable_kinds = field
+# private_protected_fields - Define private and protected fields
+dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected
+dotnet_naming_symbols.private_protected_fields.applicable_kinds = field
+# public_symbols - Define any public symbol
+dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal
+dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate
+# parameters - Defines any parameter
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+# non_interface_types - Defines class, struct, enum and delegate types
+dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate
+# interface_types - Defines interfaces
+dotnet_naming_symbols.interface_types.applicable_kinds = interface
+
+# Naming Styles
+# camel_case - Define the camelCase style
+dotnet_naming_style.camel_case.capitalization = camel_case
+# pascal_case - Define the Pascal_case style
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+# first_upper - The first character must start with an upper-case character
+dotnet_naming_style.first_upper.capitalization = first_word_upper
+# prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I'
+dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case
+dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I
+
+# Naming Rules
+
+# Async
+dotnet_naming_rule.async_methods_end_in_async.severity = silent
+dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
+dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
+
+dotnet_naming_symbols.any_async_methods.applicable_kinds = method
+dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
+dotnet_naming_symbols.any_async_methods.required_modifiers = async
+
+dotnet_naming_style.end_in_async.required_suffix = Async
+dotnet_naming_style.end_in_async.capitalization = pascal_case
+
+# Constant fields must be PascalCase
+dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = silent
+dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case
+# Public, internal and protected readonly fields must be PascalCase
+dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = silent
+dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields
+dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case
+# Static readonly fields must be PascalCase
+dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = silent
+dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields
+dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case
+# Private readonly fields must be camelCase
+dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = silent
+dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields
+dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case
+# Public and internal fields must be PascalCase
+dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.severity = silent
+dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.symbols = public_internal_protected_fields
+dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.style = pascal_case
+# Private and protected fields must be camelCase
+dotnet_naming_rule.private_fields_must_be_camel_case.severity = silent
+dotnet_naming_rule.private_fields_must_be_camel_case.symbols = private_protected_fields
+dotnet_naming_rule.private_fields_must_be_camel_case.style = prefix_private_field_with_underscore
+# Public members must be capitalized
+dotnet_naming_rule.public_members_must_be_capitalized.severity = silent
+dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
+dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper
+# Parameters must be camelCase
+dotnet_naming_rule.parameters_must_be_camel_case.severity = silent
+dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters
+dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case
+# Class, struct, enum and delegates must be PascalCase
+dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = silent
+dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types
+dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case
+# Interfaces must be PascalCase and start with an 'I'
+dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = silent
+dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types
+dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i
+# prefix_private_field_with_underscore - Private fields must be prefixed with _
+dotnet_naming_style.prefix_private_field_with_underscore.capitalization = camel_case
+dotnet_naming_style.prefix_private_field_with_underscore.required_prefix = _
+
+# Code files
+[*.{cs,vb}]
+
+# Migrate back from old Toolkit.ruleset
+dotnet_diagnostic.CA1001.severity = warning
+dotnet_diagnostic.CA1009.severity = warning
+dotnet_diagnostic.CA1016.severity = warning
+dotnet_diagnostic.CA1033.severity = warning
+dotnet_diagnostic.CA1049.severity = warning
+dotnet_diagnostic.CA1060.severity = warning
+dotnet_diagnostic.CA1061.severity = warning
+dotnet_diagnostic.CA1063.severity = warning
+dotnet_diagnostic.CA1065.severity = warning
+dotnet_diagnostic.CA1301.severity = warning
+dotnet_diagnostic.CA1400.severity = warning
+dotnet_diagnostic.CA1401.severity = warning
+dotnet_diagnostic.CA1403.severity = warning
+dotnet_diagnostic.CA1404.severity = warning
+dotnet_diagnostic.CA1405.severity = warning
+dotnet_diagnostic.CA1410.severity = warning
+dotnet_diagnostic.CA1415.severity = warning
+dotnet_diagnostic.CA1821.severity = warning
+dotnet_diagnostic.CA1900.severity = warning
+dotnet_diagnostic.CA1901.severity = warning
+dotnet_diagnostic.CA2002.severity = warning
+dotnet_diagnostic.CA2100.severity = warning
+dotnet_diagnostic.CA2101.severity = warning
+dotnet_diagnostic.CA2108.severity = warning
+dotnet_diagnostic.CA2111.severity = warning
+dotnet_diagnostic.CA2112.severity = warning
+dotnet_diagnostic.CA2114.severity = warning
+dotnet_diagnostic.CA2116.severity = warning
+dotnet_diagnostic.CA2117.severity = warning
+dotnet_diagnostic.CA2122.severity = warning
+dotnet_diagnostic.CA2123.severity = warning
+dotnet_diagnostic.CA2124.severity = warning
+dotnet_diagnostic.CA2126.severity = warning
+dotnet_diagnostic.CA2131.severity = warning
+dotnet_diagnostic.CA2132.severity = warning
+dotnet_diagnostic.CA2133.severity = warning
+dotnet_diagnostic.CA2134.severity = warning
+dotnet_diagnostic.CA2137.severity = warning
+dotnet_diagnostic.CA2138.severity = warning
+dotnet_diagnostic.CA2140.severity = warning
+dotnet_diagnostic.CA2141.severity = warning
+dotnet_diagnostic.CA2146.severity = warning
+dotnet_diagnostic.CA2147.severity = warning
+dotnet_diagnostic.CA2149.severity = warning
+dotnet_diagnostic.CA2200.severity = warning
+dotnet_diagnostic.CA2202.severity = warning
+dotnet_diagnostic.CA2207.severity = warning
+dotnet_diagnostic.CA2212.severity = warning
+dotnet_diagnostic.CA2213.severity = warning
+dotnet_diagnostic.CA2214.severity = warning
+dotnet_diagnostic.CA2216.severity = warning
+dotnet_diagnostic.CA2220.severity = warning
+dotnet_diagnostic.CA2229.severity = warning
+dotnet_diagnostic.CA2231.severity = warning
+dotnet_diagnostic.CA2232.severity = warning
+dotnet_diagnostic.CA2235.severity = warning
+dotnet_diagnostic.CA2236.severity = warning
+dotnet_diagnostic.CA2237.severity = warning
+dotnet_diagnostic.CA2238.severity = warning
+dotnet_diagnostic.CA2240.severity = warning
+dotnet_diagnostic.CA2241.severity = warning
+dotnet_diagnostic.CA2242.severity = warning
+dotnet_diagnostic.CA1031.severity = none # Disable https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1031?view=vs-2019
+dotnet_diagnostic.SA1011.severity = none
+dotnet_diagnostic.SA1101.severity = none
+dotnet_diagnostic.SA1118.severity = none
+dotnet_diagnostic.SA1200.severity = none
+dotnet_diagnostic.SA1201.severity = none
+dotnet_diagnostic.SA1202.severity = none
+dotnet_diagnostic.SA1309.severity = none
+dotnet_diagnostic.SA1310.severity = none
+dotnet_diagnostic.SA1600.severity = none
+dotnet_diagnostic.SA1602.severity = none
+dotnet_diagnostic.SA1611.severity = none
+dotnet_diagnostic.SA1633.severity = none
+dotnet_diagnostic.SA1634.severity = none
+dotnet_diagnostic.SA1652.severity = none
+
+dotnet_diagnostic.SA1629.severity = none # DocumentationTextMustEndWithAPeriod: Let's enable this rule back when we shift to WinUI3 (v8.x). If we do it now, it would mean more than 400 file changes.
+dotnet_diagnostic.SA1413.severity = none # UseTrailingCommasInMultiLineInitializers: This would also mean a lot of changes at the end of all multiline intializers. It's also debatable if we want this or not.
+dotnet_diagnostic.SA1314.severity = none # TypeParameterNamesMustBeginWithT: We do have a few templates that don't start with T. We need to double check that changing this is not a breaking change. If not, we can re-enable this.
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.Properties.cs b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.Properties.cs
new file mode 100644
index 0000000..e850f07
--- /dev/null
+++ b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.Properties.cs
@@ -0,0 +1,204 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/DropShadowPanel
+
+namespace Notepads.Controls
+{
+ using Windows.UI;
+ using Windows.UI.Composition;
+ using Windows.UI.Xaml;
+
+ ///
+ /// The control allows the creation of a DropShadow for any Xaml FrameworkElement in markup
+ /// making it easier to add shadows to Xaml without having to directly drop down to Windows.UI.Composition APIs.
+ ///
+ public partial class DropShadowPanel
+ {
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty BlurRadiusProperty =
+ DependencyProperty.Register(nameof(BlurRadius), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(9.0, OnBlurRadiusChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColorProperty =
+ DependencyProperty.Register(nameof(Color), typeof(Color), typeof(DropShadowPanel), new PropertyMetadata(Colors.Black, OnColorChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty OffsetXProperty =
+ DependencyProperty.Register(nameof(OffsetX), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetXChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty OffsetYProperty =
+ DependencyProperty.Register(nameof(OffsetY), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetYChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty OffsetZProperty =
+ DependencyProperty.Register(nameof(OffsetZ), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetZChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ShadowOpacityProperty =
+ DependencyProperty.Register(nameof(ShadowOpacity), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(1.0, OnShadowOpacityChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsMaskedProperty =
+ DependencyProperty.Register(nameof(IsMasked), typeof(bool), typeof(DropShadowPanel), new PropertyMetadata(true, OnIsMaskedChanged));
+
+ ///
+ /// Gets DropShadow. Exposes the underlying composition object to allow custom Windows.UI.Composition animations.
+ ///
+ public DropShadow DropShadow => _dropShadow;
+
+ ///
+ /// Gets or sets the mask of the underlying .
+ /// Allows for a custom to be set.
+ ///
+ public CompositionBrush Mask
+ {
+ get => _dropShadow?.Mask;
+
+ set
+ {
+ if (_dropShadow != null)
+ {
+ _dropShadow.Mask = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the blur radius of the drop shadow.
+ ///
+ public double BlurRadius
+ {
+ get => (double)GetValue(BlurRadiusProperty);
+ set => SetValue(BlurRadiusProperty, value);
+ }
+
+ ///
+ /// Gets or sets the color of the drop shadow.
+ ///
+ public Color Color
+ {
+ get => (Color)GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ /// Gets or sets the x offset of the drop shadow.
+ ///
+ public double OffsetX
+ {
+ get => (double)GetValue(OffsetXProperty);
+ set => SetValue(OffsetXProperty, value);
+ }
+
+ ///
+ /// Gets or sets the y offset of the drop shadow.
+ ///
+ public double OffsetY
+ {
+ get => (double)GetValue(OffsetYProperty);
+ set => SetValue(OffsetYProperty, value);
+ }
+
+ ///
+ /// Gets or sets the z offset of the drop shadow.
+ ///
+ public double OffsetZ
+ {
+ get => (double)GetValue(OffsetZProperty);
+ set => SetValue(OffsetZProperty, value);
+ }
+
+ ///
+ /// Gets or sets the opacity of the drop shadow.
+ ///
+ public double ShadowOpacity
+ {
+ get => (double)GetValue(ShadowOpacityProperty);
+ set => SetValue(ShadowOpacityProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the panel uses an alpha mask to create a more precise shadow vs. a quicker rectangle shape.
+ ///
+ ///
+ /// Turn this off to lose fidelity and gain performance of the panel.
+ ///
+ public bool IsMasked
+ {
+ get => (bool)GetValue(IsMaskedProperty);
+ set => SetValue(IsMaskedProperty, value);
+ }
+
+ private static void OnBlurRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnBlurRadiusChanged((double)e.NewValue);
+ }
+ }
+
+ private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnColorChanged((Color)e.NewValue);
+ }
+ }
+
+ private static void OnOffsetXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnOffsetXChanged((double)e.NewValue);
+ }
+ }
+
+ private static void OnOffsetYChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnOffsetYChanged((double)e.NewValue);
+ }
+ }
+
+ private static void OnOffsetZChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnOffsetZChanged((double)e.NewValue);
+ }
+ }
+
+ private static void OnShadowOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.OnShadowOpacityChanged((double)e.NewValue);
+ }
+ }
+
+ private static void OnIsMaskedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is DropShadowPanel panel)
+ {
+ panel.UpdateShadowMask();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.cs b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.cs
new file mode 100644
index 0000000..81a4ca1
--- /dev/null
+++ b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.cs
@@ -0,0 +1,191 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/DropShadowPanel
+
+namespace Notepads.Controls
+{
+ using System.Numerics;
+ using Windows.UI;
+ using Windows.UI.Composition;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Hosting;
+ using Windows.UI.Xaml.Shapes;
+
+ ///
+ /// The control allows the creation of a DropShadow for any Xaml FrameworkElement in markup
+ /// making it easier to add shadows to Xaml without having to directly drop down to Windows.UI.Composition APIs.
+ ///
+ [TemplatePart(Name = PartShadow, Type = typeof(Border))]
+ public partial class DropShadowPanel : ContentControl
+ {
+ private const string PartShadow = "ShadowElement";
+
+ private readonly DropShadow _dropShadow;
+ private readonly SpriteVisual _shadowVisual;
+ private Border _border;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DropShadowPanel()
+ {
+ DefaultStyleKey = typeof(DropShadowPanel);
+
+ Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
+
+ _shadowVisual = compositor.CreateSpriteVisual();
+
+ _dropShadow = compositor.CreateDropShadow();
+ _shadowVisual.Shadow = _dropShadow;
+ }
+
+ ///
+ /// Update the visual state of the control when its template is changed.
+ ///
+ protected override void OnApplyTemplate()
+ {
+ _border = GetTemplateChild(PartShadow) as Border;
+
+ if (_border != null)
+ {
+ ElementCompositionPreview.SetElementChildVisual(_border, _shadowVisual);
+ }
+
+ ConfigureShadowVisualForCastingElement();
+
+ base.OnApplyTemplate();
+ }
+
+ ///
+ protected override void OnContentChanged(object oldContent, object newContent)
+ {
+ if (oldContent != null)
+ {
+ if (oldContent is FrameworkElement oldElement)
+ {
+ oldElement.SizeChanged -= OnSizeChanged;
+ }
+ }
+
+ if (newContent != null)
+ {
+ if (newContent is FrameworkElement newElement)
+ {
+ newElement.SizeChanged += OnSizeChanged;
+ }
+ }
+
+ base.OnContentChanged(oldContent, newContent);
+ }
+
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ UpdateShadowSize();
+ }
+
+ private void ConfigureShadowVisualForCastingElement()
+ {
+ UpdateShadowMask();
+ UpdateShadowSize();
+ }
+
+ private void OnBlurRadiusChanged(double newValue)
+ {
+ if (_dropShadow != null)
+ {
+ _dropShadow.BlurRadius = (float)newValue;
+ }
+ }
+
+ private void OnColorChanged(Color newValue)
+ {
+ if (_dropShadow != null)
+ {
+ _dropShadow.Color = newValue;
+ }
+ }
+
+ private void OnOffsetXChanged(double newValue)
+ {
+ if (_dropShadow != null)
+ {
+ UpdateShadowOffset((float)newValue, _dropShadow.Offset.Y, _dropShadow.Offset.Z);
+ }
+ }
+
+ private void OnOffsetYChanged(double newValue)
+ {
+ if (_dropShadow != null)
+ {
+ UpdateShadowOffset(_dropShadow.Offset.X, (float)newValue, _dropShadow.Offset.Z);
+ }
+ }
+
+ private void OnOffsetZChanged(double newValue)
+ {
+ if (_dropShadow != null)
+ {
+ UpdateShadowOffset(_dropShadow.Offset.X, _dropShadow.Offset.Y, (float)newValue);
+ }
+ }
+
+ private void OnShadowOpacityChanged(double newValue)
+ {
+ if (_dropShadow != null)
+ {
+ _dropShadow.Opacity = (float)newValue;
+ }
+ }
+
+ private void UpdateShadowMask()
+ {
+ if (Content != null && IsMasked)
+ {
+ CompositionBrush mask = null;
+
+ if (Content is Image image)
+ {
+ mask = image.GetAlphaMask();
+ }
+ else if (Content is Shape shape)
+ {
+ mask = shape.GetAlphaMask();
+ }
+ else if (Content is TextBlock textBlock)
+ {
+ mask = textBlock.GetAlphaMask();
+ }
+
+ _dropShadow.Mask = mask;
+ }
+ else
+ {
+ _dropShadow.Mask = null;
+ }
+ }
+
+ private void UpdateShadowOffset(float x, float y, float z)
+ {
+ if (_dropShadow != null)
+ {
+ _dropShadow.Offset = new Vector3(x, y, z);
+ }
+ }
+
+ private void UpdateShadowSize()
+ {
+ if (_shadowVisual != null)
+ {
+ Vector2 newSize = new Vector2(0, 0);
+ if (Content is FrameworkElement content)
+ {
+ newSize = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);
+ }
+
+ _shadowVisual.Size = newSize;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.xaml b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.xaml
new file mode 100644
index 0000000..d56d2d6
--- /dev/null
+++ b/src/src/Notepads.Controls/DropShadowPanel/DropShadowPanel.xaml
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.Data.cs b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Data.cs
new file mode 100644
index 0000000..2a2f726
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Data.cs
@@ -0,0 +1,158 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ ///
+ /// Represents the control that redistributes space between columns or rows of a Grid control.
+ ///
+ public partial class GridSplitter
+ {
+ ///
+ /// Enum to indicate whether GridSplitter resizes Columns or Rows
+ ///
+ public enum GridResizeDirection
+ {
+ ///
+ /// Determines whether to resize rows or columns based on its Alignment and
+ /// width compared to height
+ ///
+ Auto,
+
+ ///
+ /// Resize columns when dragging Splitter.
+ ///
+ Columns,
+
+ ///
+ /// Resize rows when dragging Splitter.
+ ///
+ Rows
+ }
+
+ ///
+ /// Enum to indicate what Columns or Rows the GridSplitter resizes
+ ///
+ public enum GridResizeBehavior
+ {
+ ///
+ /// Determine which columns or rows to resize based on its Alignment.
+ ///
+ BasedOnAlignment,
+
+ ///
+ /// Resize the current and next Columns or Rows.
+ ///
+ CurrentAndNext,
+
+ ///
+ /// Resize the previous and current Columns or Rows.
+ ///
+ PreviousAndCurrent,
+
+ ///
+ /// Resize the previous and next Columns or Rows.
+ ///
+ PreviousAndNext
+ }
+
+ ///
+ /// Enum to indicate the supported gripper cursor types.
+ ///
+ public enum GripperCursorType
+ {
+ ///
+ /// Change the cursor based on the splitter direction
+ ///
+ Default = -1,
+
+ ///
+ /// Standard Arrow cursor
+ ///
+ Arrow,
+
+ ///
+ /// Standard Cross cursor
+ ///
+ Cross,
+
+ ///
+ /// Standard Custom cursor
+ ///
+ Custom,
+
+ ///
+ /// Standard Hand cursor
+ ///
+ Hand,
+
+ ///
+ /// Standard Help cursor
+ ///
+ Help,
+
+ ///
+ /// Standard IBeam cursor
+ ///
+ IBeam,
+
+ ///
+ /// Standard SizeAll cursor
+ ///
+ SizeAll,
+
+ ///
+ /// Standard SizeNortheastSouthwest cursor
+ ///
+ SizeNortheastSouthwest,
+
+ ///
+ /// Standard SizeNorthSouth cursor
+ ///
+ SizeNorthSouth,
+
+ ///
+ /// Standard SizeNorthwestSoutheast cursor
+ ///
+ SizeNorthwestSoutheast,
+
+ ///
+ /// Standard SizeWestEast cursor
+ ///
+ SizeWestEast,
+
+ ///
+ /// Standard UniversalNo cursor
+ ///
+ UniversalNo,
+
+ ///
+ /// Standard UpArrow cursor
+ ///
+ UpArrow,
+
+ ///
+ /// Standard Wait cursor
+ ///
+ Wait
+ }
+
+ ///
+ /// Enum to indicate the behavior of window cursor on grid splitter hover
+ ///
+ public enum SplitterCursorBehavior
+ {
+ ///
+ /// Update window cursor on Grid Splitter hover
+ ///
+ ChangeOnSplitterHover,
+
+ ///
+ /// Update window cursor on Grid Splitter Gripper hover
+ ///
+ ChangeOnGripperHover
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.Events.cs b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Events.cs
new file mode 100644
index 0000000..b57cee2
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Events.cs
@@ -0,0 +1,302 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Represents the control that redistributes space between columns or rows of a Grid control.
+ ///
+ public partial class GridSplitter
+ {
+ // Symbols for GripperBar in Segoe MDL2 Assets
+ private const string GripperBarVertical = "\xE784";
+ private const string GripperBarHorizontal = "\xE76F";
+ private const string GripperDisplayFont = "Segoe MDL2 Assets";
+
+ private void GridSplitter_Loaded(object sender, RoutedEventArgs e)
+ {
+ _resizeDirection = GetResizeDirection();
+ _resizeBehavior = GetResizeBehavior();
+
+ // Adding Grip to Grid Splitter
+ if (Element == default(UIElement))
+ {
+ CreateGripperDisplay();
+ Element = _gripperDisplay;
+ }
+
+ if (_hoverWrapper == null)
+ {
+ var hoverWrapper = new GripperHoverWrapper(
+ CursorBehavior == SplitterCursorBehavior.ChangeOnSplitterHover
+ ? this
+ : Element,
+ _resizeDirection,
+ GripperCursor,
+ GripperCustomCursorResource);
+ ManipulationStarted += hoverWrapper.SplitterManipulationStarted;
+ ManipulationCompleted += hoverWrapper.SplitterManipulationCompleted;
+
+ _hoverWrapper = hoverWrapper;
+ }
+ }
+
+ private void CreateGripperDisplay()
+ {
+ if (_gripperDisplay == null)
+ {
+ _gripperDisplay = new TextBlock
+ {
+ FontFamily = new FontFamily(GripperDisplayFont),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Foreground = GripperForeground,
+ Text = _resizeDirection == GridResizeDirection.Columns ? GripperBarVertical : GripperBarHorizontal
+ };
+ }
+ }
+
+ ///
+ protected override void OnKeyDown(KeyRoutedEventArgs e)
+ {
+ var step = 1;
+ var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
+ if (ctrl.HasFlag(CoreVirtualKeyStates.Down))
+ {
+ step = 5;
+ }
+
+ if (_resizeDirection == GridResizeDirection.Columns)
+ {
+ if (e.Key == VirtualKey.Left)
+ {
+ HorizontalMove(-step);
+ }
+ else if (e.Key == VirtualKey.Right)
+ {
+ HorizontalMove(step);
+ }
+ else
+ {
+ return;
+ }
+
+ e.Handled = true;
+ return;
+ }
+
+ if (_resizeDirection == GridResizeDirection.Rows)
+ {
+ if (e.Key == VirtualKey.Up)
+ {
+ VerticalMove(-step);
+ }
+ else if (e.Key == VirtualKey.Down)
+ {
+ VerticalMove(step);
+ }
+ else
+ {
+ return;
+ }
+
+ e.Handled = true;
+ }
+
+ base.OnKeyDown(e);
+ }
+
+ ///
+ protected override void OnManipulationStarted(ManipulationStartedRoutedEventArgs e)
+ {
+ // saving the previous state
+ PreviousCursor = Window.Current.CoreWindow.PointerCursor;
+ _resizeDirection = GetResizeDirection();
+ _resizeBehavior = GetResizeBehavior();
+
+ if (_resizeDirection == GridResizeDirection.Columns)
+ {
+ Window.Current.CoreWindow.PointerCursor = ColumnsSplitterCursor;
+ }
+ else if (_resizeDirection == GridResizeDirection.Rows)
+ {
+ Window.Current.CoreWindow.PointerCursor = RowSplitterCursor;
+ }
+
+ base.OnManipulationStarted(e);
+ }
+
+ ///
+ protected override void OnManipulationCompleted(ManipulationCompletedRoutedEventArgs e)
+ {
+ Window.Current.CoreWindow.PointerCursor = PreviousCursor;
+
+ base.OnManipulationCompleted(e);
+ }
+
+ ///
+ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
+ {
+ var horizontalChange = e.Delta.Translation.X;
+ var verticalChange = e.Delta.Translation.Y;
+
+ if (_resizeDirection == GridResizeDirection.Columns)
+ {
+ if (HorizontalMove(horizontalChange))
+ {
+ return;
+ }
+ }
+ else if (_resizeDirection == GridResizeDirection.Rows)
+ {
+ if (VerticalMove(verticalChange))
+ {
+ return;
+ }
+ }
+
+ base.OnManipulationDelta(e);
+ }
+
+ private bool VerticalMove(double verticalChange)
+ {
+ if (CurrentRow == null || SiblingRow == null)
+ {
+ return true;
+ }
+
+ // if current row has fixed height then resize it
+ if (!IsStarRow(CurrentRow))
+ {
+ // No need to check for the row Min height because it is automatically respected
+ if (!SetRowHeight(CurrentRow, verticalChange, GridUnitType.Pixel))
+ {
+ return true;
+ }
+ }
+
+ // if sibling row has fixed width then resize it
+ else if (!IsStarRow(SiblingRow))
+ {
+ // Would adding to this column make the current column violate the MinWidth?
+ if (IsValidRowHeight(CurrentRow, verticalChange) == false)
+ {
+ return false;
+ }
+
+ if (!SetRowHeight(SiblingRow, verticalChange * -1, GridUnitType.Pixel))
+ {
+ return true;
+ }
+ }
+
+ // if both row haven't fixed height (auto *)
+ else
+ {
+ // change current row height to the new height with respecting the auto
+ // change sibling row height to the new height relative to current row
+ // respect the other star row height by setting it's height to it's actual height with stars
+
+ // We need to validate current and sibling height to not cause any un expected behavior
+ if (!IsValidRowHeight(CurrentRow, verticalChange) ||
+ !IsValidRowHeight(SiblingRow, verticalChange * -1))
+ {
+ return true;
+ }
+
+ foreach (var rowDefinition in Resizable.RowDefinitions)
+ {
+ if (rowDefinition == CurrentRow)
+ {
+ SetRowHeight(CurrentRow, verticalChange, GridUnitType.Star);
+ }
+ else if (rowDefinition == SiblingRow)
+ {
+ SetRowHeight(SiblingRow, verticalChange * -1, GridUnitType.Star);
+ }
+ else if (IsStarRow(rowDefinition))
+ {
+ rowDefinition.Height = new GridLength(rowDefinition.ActualHeight, GridUnitType.Star);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private bool HorizontalMove(double horizontalChange)
+ {
+ if (CurrentColumn == null || SiblingColumn == null)
+ {
+ return true;
+ }
+
+ // if current column has fixed width then resize it
+ if (!IsStarColumn(CurrentColumn))
+ {
+ // No need to check for the Column Min width because it is automatically respected
+ if (!SetColumnWidth(CurrentColumn, horizontalChange, GridUnitType.Pixel))
+ {
+ return true;
+ }
+ }
+
+ // if sibling column has fixed width then resize it
+ else if (!IsStarColumn(SiblingColumn))
+ {
+ // Would adding to this column make the current column violate the MinWidth?
+ if (IsValidColumnWidth(CurrentColumn, horizontalChange) == false)
+ {
+ return false;
+ }
+
+ if (!SetColumnWidth(SiblingColumn, horizontalChange * -1, GridUnitType.Pixel))
+ {
+ return true;
+ }
+ }
+
+ // if both column haven't fixed width (auto *)
+ else
+ {
+ // change current column width to the new width with respecting the auto
+ // change sibling column width to the new width relative to current column
+ // respect the other star column width by setting it's width to it's actual width with stars
+
+ // We need to validate current and sibling width to not cause any un expected behavior
+ if (!IsValidColumnWidth(CurrentColumn, horizontalChange) ||
+ !IsValidColumnWidth(SiblingColumn, horizontalChange * -1))
+ {
+ return true;
+ }
+
+ foreach (var columnDefinition in Resizable.ColumnDefinitions)
+ {
+ if (columnDefinition == CurrentColumn)
+ {
+ SetColumnWidth(CurrentColumn, horizontalChange, GridUnitType.Star);
+ }
+ else if (columnDefinition == SiblingColumn)
+ {
+ SetColumnWidth(SiblingColumn, horizontalChange * -1, GridUnitType.Star);
+ }
+ else if (IsStarColumn(columnDefinition))
+ {
+ columnDefinition.Width = new GridLength(columnDefinition.ActualWidth, GridUnitType.Star);
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.Helper.cs b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Helper.cs
new file mode 100644
index 0000000..1c53cf1
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Helper.cs
@@ -0,0 +1,261 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+
+ ///
+ /// Represents the control that redistributes space between columns or rows of a Grid control.
+ ///
+ public partial class GridSplitter
+ {
+ private static bool IsStarColumn(ColumnDefinition definition)
+ {
+ return ((GridLength)definition.GetValue(ColumnDefinition.WidthProperty)).IsStar;
+ }
+
+ private static bool IsStarRow(RowDefinition definition)
+ {
+ return ((GridLength)definition.GetValue(RowDefinition.HeightProperty)).IsStar;
+ }
+
+ private bool SetColumnWidth(ColumnDefinition columnDefinition, double horizontalChange, GridUnitType unitType)
+ {
+ var newWidth = columnDefinition.ActualWidth + horizontalChange;
+
+ var minWidth = columnDefinition.MinWidth;
+ if (!double.IsNaN(minWidth) && newWidth < minWidth)
+ {
+ newWidth = minWidth;
+ }
+
+ var maxWidth = columnDefinition.MaxWidth;
+ if (!double.IsNaN(maxWidth) && newWidth > maxWidth)
+ {
+ newWidth = maxWidth;
+ }
+
+ if (newWidth > ActualWidth)
+ {
+ columnDefinition.Width = new GridLength(newWidth, unitType);
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsValidColumnWidth(ColumnDefinition columnDefinition, double horizontalChange)
+ {
+ var newWidth = columnDefinition.ActualWidth + horizontalChange;
+
+ var minWidth = columnDefinition.MinWidth;
+ if (!double.IsNaN(minWidth) && newWidth < minWidth)
+ {
+ return false;
+ }
+
+ var maxWidth = columnDefinition.MaxWidth;
+ if (!double.IsNaN(maxWidth) && newWidth > maxWidth)
+ {
+ return false;
+ }
+
+ if (newWidth <= ActualWidth)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool SetRowHeight(RowDefinition rowDefinition, double verticalChange, GridUnitType unitType)
+ {
+ var newHeight = rowDefinition.ActualHeight + verticalChange;
+
+ var minHeight = rowDefinition.MinHeight;
+ if (!double.IsNaN(minHeight) && newHeight < minHeight)
+ {
+ newHeight = minHeight;
+ }
+
+ var maxWidth = rowDefinition.MaxHeight;
+ if (!double.IsNaN(maxWidth) && newHeight > maxWidth)
+ {
+ newHeight = maxWidth;
+ }
+
+ if (newHeight > ActualHeight)
+ {
+ rowDefinition.Height = new GridLength(newHeight, unitType);
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsValidRowHeight(RowDefinition rowDefinition, double verticalChange)
+ {
+ var newHeight = rowDefinition.ActualHeight + verticalChange;
+
+ var minHeight = rowDefinition.MinHeight;
+ if (!double.IsNaN(minHeight) && newHeight < minHeight)
+ {
+ return false;
+ }
+
+ var maxHeight = rowDefinition.MaxHeight;
+ if (!double.IsNaN(maxHeight) && newHeight > maxHeight)
+ {
+ return false;
+ }
+
+ if (newHeight <= ActualHeight)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Return the targeted Column based on the resize behavior
+ private int GetTargetedColumn()
+ {
+ var currentIndex = Grid.GetColumn(TargetControl);
+ return GetTargetIndex(currentIndex);
+ }
+
+ // Return the sibling Row based on the resize behavior
+ private int GetTargetedRow()
+ {
+ var currentIndex = Grid.GetRow(TargetControl);
+ return GetTargetIndex(currentIndex);
+ }
+
+ // Return the sibling Column based on the resize behavior
+ private int GetSiblingColumn()
+ {
+ var currentIndex = Grid.GetColumn(TargetControl);
+ return GetSiblingIndex(currentIndex);
+ }
+
+ // Return the sibling Row based on the resize behavior
+ private int GetSiblingRow()
+ {
+ var currentIndex = Grid.GetRow(TargetControl);
+ return GetSiblingIndex(currentIndex);
+ }
+
+ // Gets index based on resize behavior for first targeted row/column
+ private int GetTargetIndex(int currentIndex)
+ {
+ switch (_resizeBehavior)
+ {
+ case GridResizeBehavior.CurrentAndNext:
+ return currentIndex;
+ case GridResizeBehavior.PreviousAndNext:
+ return currentIndex - 1;
+ case GridResizeBehavior.PreviousAndCurrent:
+ return currentIndex - 1;
+ default:
+ return -1;
+ }
+ }
+
+ // Gets index based on resize behavior for second targeted row/column
+ private int GetSiblingIndex(int currentIndex)
+ {
+ switch (_resizeBehavior)
+ {
+ case GridResizeBehavior.CurrentAndNext:
+ return currentIndex + 1;
+ case GridResizeBehavior.PreviousAndNext:
+ return currentIndex + 1;
+ case GridResizeBehavior.PreviousAndCurrent:
+ return currentIndex;
+ default:
+ return -1;
+ }
+ }
+
+ // Checks the control alignment and Width/Height to detect the control resize direction columns/rows
+ private GridResizeDirection GetResizeDirection()
+ {
+ GridResizeDirection direction = ResizeDirection;
+
+ if (direction == GridResizeDirection.Auto)
+ {
+ // When HorizontalAlignment is Left, Right or Center, resize Columns
+ if (HorizontalAlignment != HorizontalAlignment.Stretch)
+ {
+ direction = GridResizeDirection.Columns;
+ }
+
+ // When VerticalAlignment is Top, Bottom or Center, resize Rows
+ else if (VerticalAlignment != VerticalAlignment.Stretch)
+ {
+ direction = GridResizeDirection.Rows;
+ }
+
+ // Check Width vs Height
+ else if (ActualWidth <= ActualHeight)
+ {
+ direction = GridResizeDirection.Columns;
+ }
+ else
+ {
+ direction = GridResizeDirection.Rows;
+ }
+ }
+
+ return direction;
+ }
+
+ // Get the resize behavior (Which columns/rows should be resized) based on alignment and Direction
+ private GridResizeBehavior GetResizeBehavior()
+ {
+ GridResizeBehavior resizeBehavior = ResizeBehavior;
+
+ if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
+ {
+ if (_resizeDirection == GridResizeDirection.Columns)
+ {
+ switch (HorizontalAlignment)
+ {
+ case HorizontalAlignment.Left:
+ resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+ break;
+ case HorizontalAlignment.Right:
+ resizeBehavior = GridResizeBehavior.CurrentAndNext;
+ break;
+ default:
+ resizeBehavior = GridResizeBehavior.PreviousAndNext;
+ break;
+ }
+ }
+
+ // resize direction is vertical
+ else
+ {
+ switch (VerticalAlignment)
+ {
+ case VerticalAlignment.Top:
+ resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+ break;
+ case VerticalAlignment.Bottom:
+ resizeBehavior = GridResizeBehavior.CurrentAndNext;
+ break;
+ default:
+ resizeBehavior = GridResizeBehavior.PreviousAndNext;
+ break;
+ }
+ }
+ }
+
+ return resizeBehavior;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.Options.cs b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Options.cs
new file mode 100644
index 0000000..aab678b
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.Options.cs
@@ -0,0 +1,225 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Represents the control that redistributes space between columns or rows of a Grid control.
+ ///
+ public partial class GridSplitter
+ {
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ElementProperty
+ = DependencyProperty.Register(
+ nameof(Element),
+ typeof(UIElement),
+ typeof(GridSplitter),
+ new PropertyMetadata(default(UIElement), OnElementPropertyChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ResizeDirectionProperty
+ = DependencyProperty.Register(
+ nameof(ResizeDirection),
+ typeof(GridResizeDirection),
+ typeof(GridSplitter),
+ new PropertyMetadata(GridResizeDirection.Auto));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ResizeBehaviorProperty
+ = DependencyProperty.Register(
+ nameof(ResizeBehavior),
+ typeof(GridResizeBehavior),
+ typeof(GridSplitter),
+ new PropertyMetadata(GridResizeBehavior.BasedOnAlignment));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty GripperForegroundProperty
+ = DependencyProperty.Register(
+ nameof(GripperForeground),
+ typeof(Brush),
+ typeof(GridSplitter),
+ new PropertyMetadata(default(Brush), OnGripperForegroundPropertyChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ParentLevelProperty
+ = DependencyProperty.Register(
+ nameof(ParentLevel),
+ typeof(int),
+ typeof(GridSplitter),
+ new PropertyMetadata(default(int)));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty GripperCursorProperty =
+ DependencyProperty.RegisterAttached(
+ nameof(GripperCursor),
+ typeof(CoreCursorType?),
+ typeof(GridSplitter),
+ new PropertyMetadata(GripperCursorType.Default, OnGripperCursorPropertyChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty GripperCustomCursorResourceProperty =
+ DependencyProperty.RegisterAttached(
+ nameof(GripperCustomCursorResource),
+ typeof(uint),
+ typeof(GridSplitter),
+ new PropertyMetadata(GripperCustomCursorDefaultResource, GripperCustomCursorResourcePropertyChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty CursorBehaviorProperty =
+ DependencyProperty.RegisterAttached(
+ nameof(CursorBehavior),
+ typeof(SplitterCursorBehavior),
+ typeof(GridSplitter),
+ new PropertyMetadata(SplitterCursorBehavior.ChangeOnSplitterHover, CursorBehaviorPropertyChanged));
+
+ ///
+ /// Gets or sets the visual content of this Grid Splitter
+ ///
+ public UIElement Element
+ {
+ get => (UIElement)GetValue(ElementProperty);
+ set => SetValue(ElementProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether the Splitter resizes the Columns, Rows, or Both.
+ ///
+ public GridResizeDirection ResizeDirection
+ {
+ get => (GridResizeDirection)GetValue(ResizeDirectionProperty);
+ set => SetValue(ResizeDirectionProperty, value);
+ }
+
+ ///
+ /// Gets or sets which Columns or Rows the Splitter resizes.
+ ///
+ public GridResizeBehavior ResizeBehavior
+ {
+ get => (GridResizeBehavior)GetValue(ResizeBehaviorProperty);
+ set => SetValue(ResizeBehaviorProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground color of grid splitter grip
+ ///
+ public Brush GripperForeground
+ {
+ get => (Brush)GetValue(GripperForegroundProperty);
+ set => SetValue(GripperForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the level of the parent grid to resize
+ ///
+ public int ParentLevel
+ {
+ get => (int)GetValue(ParentLevelProperty);
+ set => SetValue(ParentLevelProperty, value);
+ }
+
+ ///
+ /// Gets or sets the gripper Cursor type
+ ///
+ public GripperCursorType GripperCursor
+ {
+ get => (GripperCursorType)GetValue(GripperCursorProperty);
+ set => SetValue(GripperCursorProperty, value);
+ }
+
+ ///
+ /// Gets or sets the gripper Custom Cursor resource number
+ ///
+ public int GripperCustomCursorResource
+ {
+ get => (int)GetValue(GripperCustomCursorResourceProperty);
+ set => SetValue(GripperCustomCursorResourceProperty, value);
+ }
+
+ ///
+ /// Gets or sets splitter cursor on hover behavior
+ ///
+ public SplitterCursorBehavior CursorBehavior
+ {
+ get => (SplitterCursorBehavior)GetValue(CursorBehaviorProperty);
+ set => SetValue(CursorBehaviorProperty, value);
+ }
+
+ private static void OnGripperForegroundPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var gridSplitter = (GridSplitter)d;
+
+ if (gridSplitter._gripperDisplay == null)
+ {
+ return;
+ }
+
+ gridSplitter._gripperDisplay.Foreground = gridSplitter.GripperForeground;
+ }
+
+ private static void OnGripperCursorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var gridSplitter = (GridSplitter)d;
+
+ if (gridSplitter._hoverWrapper == null)
+ {
+ return;
+ }
+
+ gridSplitter._hoverWrapper.GripperCursor = gridSplitter.GripperCursor;
+ }
+
+ private static void GripperCustomCursorResourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var gridSplitter = (GridSplitter)d;
+
+ if (gridSplitter._hoverWrapper == null)
+ {
+ return;
+ }
+
+ gridSplitter._hoverWrapper.GripperCustomCursorResource = gridSplitter.GripperCustomCursorResource;
+ }
+
+ private static void CursorBehaviorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var gridSplitter = (GridSplitter)d;
+
+ gridSplitter._hoverWrapper?.UpdateHoverElement(gridSplitter.CursorBehavior ==
+ SplitterCursorBehavior.ChangeOnSplitterHover
+ ? gridSplitter
+ : gridSplitter.Element);
+ }
+
+ private static void OnElementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var gridSplitter = (GridSplitter)d;
+
+ gridSplitter._hoverWrapper?.UpdateHoverElement(gridSplitter.CursorBehavior ==
+ SplitterCursorBehavior.ChangeOnSplitterHover
+ ? gridSplitter
+ : gridSplitter.Element);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.cs b/src/src/Notepads.Controls/GridSplitter/GridSplitter.cs
new file mode 100644
index 0000000..e65946d
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.cs
@@ -0,0 +1,246 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Automation;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+
+ ///
+ /// Represents the control that redistributes space between columns or rows of a Grid control.
+ ///
+ public partial class GridSplitter : Control
+ {
+ internal const int GripperCustomCursorDefaultResource = -1;
+ internal static readonly CoreCursor ColumnsSplitterCursor = new CoreCursor(CoreCursorType.SizeWestEast, 1);
+ internal static readonly CoreCursor RowSplitterCursor = new CoreCursor(CoreCursorType.SizeNorthSouth, 1);
+
+ internal CoreCursor PreviousCursor { get; set; }
+
+ private GridResizeDirection _resizeDirection;
+ private GridResizeBehavior _resizeBehavior;
+ private GripperHoverWrapper _hoverWrapper;
+ private TextBlock _gripperDisplay;
+
+ private bool _pressed = false;
+ private bool _dragging = false;
+ private bool _pointerEntered = false;
+
+ ///
+ /// Gets the target parent grid from level
+ ///
+ private FrameworkElement TargetControl
+ {
+ get
+ {
+ if (ParentLevel == 0)
+ {
+ return this;
+ }
+
+ var parent = Parent;
+ for (int i = 2; i < ParentLevel; i++)
+ {
+ if (parent is FrameworkElement frameworkElement)
+ {
+ parent = frameworkElement.Parent;
+ }
+ }
+
+ return parent as FrameworkElement;
+ }
+ }
+
+ ///
+ /// Gets GridSplitter Container Grid
+ ///
+ private Grid Resizable => TargetControl?.Parent as Grid;
+
+ ///
+ /// Gets the current Column definition of the parent Grid
+ ///
+ private ColumnDefinition CurrentColumn
+ {
+ get
+ {
+ if (Resizable == null)
+ {
+ return null;
+ }
+
+ var gridSplitterTargetedColumnIndex = GetTargetedColumn();
+
+ if ((gridSplitterTargetedColumnIndex >= 0)
+ && (gridSplitterTargetedColumnIndex < Resizable.ColumnDefinitions.Count))
+ {
+ return Resizable.ColumnDefinitions[gridSplitterTargetedColumnIndex];
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Gets the Sibling Column definition of the parent Grid
+ ///
+ private ColumnDefinition SiblingColumn
+ {
+ get
+ {
+ if (Resizable == null)
+ {
+ return null;
+ }
+
+ var gridSplitterSiblingColumnIndex = GetSiblingColumn();
+
+ if ((gridSplitterSiblingColumnIndex >= 0)
+ && (gridSplitterSiblingColumnIndex < Resizable.ColumnDefinitions.Count))
+ {
+ return Resizable.ColumnDefinitions[gridSplitterSiblingColumnIndex];
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Gets the current Row definition of the parent Grid
+ ///
+ private RowDefinition CurrentRow
+ {
+ get
+ {
+ if (Resizable == null)
+ {
+ return null;
+ }
+
+ var gridSplitterTargetedRowIndex = GetTargetedRow();
+
+ if ((gridSplitterTargetedRowIndex >= 0)
+ && (gridSplitterTargetedRowIndex < Resizable.RowDefinitions.Count))
+ {
+ return Resizable.RowDefinitions[gridSplitterTargetedRowIndex];
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Gets the Sibling Row definition of the parent Grid
+ ///
+ private RowDefinition SiblingRow
+ {
+ get
+ {
+ if (Resizable == null)
+ {
+ return null;
+ }
+
+ var gridSplitterSiblingRowIndex = GetSiblingRow();
+
+ if ((gridSplitterSiblingRowIndex >= 0)
+ && (gridSplitterSiblingRowIndex < Resizable.RowDefinitions.Count))
+ {
+ return Resizable.RowDefinitions[gridSplitterSiblingRowIndex];
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GridSplitter()
+ {
+ DefaultStyleKey = typeof(GridSplitter);
+ Loaded += GridSplitter_Loaded;
+ string automationName = "GridSpliter";
+ AutomationProperties.SetName(this, automationName);
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ // Unhook registered events
+ Loaded -= GridSplitter_Loaded;
+ PointerEntered -= GridSplitter_PointerEntered;
+ PointerExited -= GridSplitter_PointerExited;
+ PointerPressed -= GridSplitter_PointerPressed;
+ PointerReleased -= GridSplitter_PointerReleased;
+ ManipulationStarted -= GridSplitter_ManipulationStarted;
+ ManipulationCompleted -= GridSplitter_ManipulationCompleted;
+
+ _hoverWrapper?.UnhookEvents();
+
+ // Register Events
+ Loaded += GridSplitter_Loaded;
+ PointerEntered += GridSplitter_PointerEntered;
+ PointerExited += GridSplitter_PointerExited;
+ PointerPressed += GridSplitter_PointerPressed;
+ PointerReleased += GridSplitter_PointerReleased;
+ ManipulationStarted += GridSplitter_ManipulationStarted;
+ ManipulationCompleted += GridSplitter_ManipulationCompleted;
+
+ _hoverWrapper?.UpdateHoverElement(Element);
+
+ ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
+ }
+
+ private void GridSplitter_PointerReleased(object sender, PointerRoutedEventArgs e)
+ {
+ _pressed = false;
+ VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
+ }
+
+ private void GridSplitter_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ _pressed = true;
+ VisualStateManager.GoToState(this, "Pressed", true);
+ }
+
+ private void GridSplitter_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerEntered = false;
+
+ if (!_pressed && !_dragging)
+ {
+ VisualStateManager.GoToState(this, "Normal", true);
+ }
+ }
+
+ private void GridSplitter_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerEntered = true;
+
+ if (!_pressed && !_dragging)
+ {
+ VisualStateManager.GoToState(this, "PointerOver", true);
+ }
+ }
+
+ private void GridSplitter_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
+ {
+ _dragging = false;
+ _pressed = false;
+ VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
+ }
+
+ private void GridSplitter_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
+ {
+ _dragging = true;
+ VisualStateManager.GoToState(this, "Pressed", true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/GridSplitter/GridSplitter.xaml b/src/src/Notepads.Controls/GridSplitter/GridSplitter.xaml
new file mode 100644
index 0000000..02242fe
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GridSplitter.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.Controls/GridSplitter/GripperHoverWrapper.cs b/src/src/Notepads.Controls/GridSplitter/GripperHoverWrapper.cs
new file mode 100644
index 0000000..d806e26
--- /dev/null
+++ b/src/src/Notepads.Controls/GridSplitter/GripperHoverWrapper.cs
@@ -0,0 +1,154 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Input;
+
+ internal class GripperHoverWrapper
+ {
+ private readonly GridSplitter.GridResizeDirection _gridSplitterDirection;
+
+ private CoreCursor _splitterPreviousPointer;
+ private CoreCursor _previousCursor;
+ private GridSplitter.GripperCursorType _gripperCursor;
+ private int _gripperCustomCursorResource;
+ private bool _isDragging;
+ private UIElement _element;
+
+ internal GridSplitter.GripperCursorType GripperCursor
+ {
+ get => _gripperCursor;
+ set => _gripperCursor = value;
+ }
+
+ internal int GripperCustomCursorResource
+ {
+ get => _gripperCustomCursorResource;
+ set => _gripperCustomCursorResource = value;
+ }
+
+ ///
+ /// Initializes a new instance of the class that add cursor change on hover functionality for GridSplitter.
+ ///
+ /// UI element to apply cursor change on hover
+ /// GridSplitter resize direction
+ /// GridSplitter gripper on hover cursor type
+ /// GridSplitter gripper custom cursor resource number
+ internal GripperHoverWrapper(UIElement element, GridSplitter.GridResizeDirection gridSplitterDirection, GridSplitter.GripperCursorType gripperCursor, int gripperCustomCursorResource)
+ {
+ _gridSplitterDirection = gridSplitterDirection;
+ _gripperCursor = gripperCursor;
+ _gripperCustomCursorResource = gripperCustomCursorResource;
+ _element = element;
+ UnhookEvents();
+ _element.PointerEntered += Element_PointerEntered;
+ _element.PointerExited += Element_PointerExited;
+ }
+
+ internal void UpdateHoverElement(UIElement element)
+ {
+ UnhookEvents();
+ _element = element;
+ _element.PointerEntered += Element_PointerEntered;
+ _element.PointerExited += Element_PointerExited;
+ }
+
+ private void Element_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ if (_isDragging)
+ {
+ // if dragging don't update the curser just update the splitter cursor with the last window cursor,
+ // because the splitter is still using the arrow cursor and will revert to original case when drag completes
+ _splitterPreviousPointer = _previousCursor;
+ }
+ else
+ {
+ Window.Current.CoreWindow.PointerCursor = _previousCursor;
+ }
+ }
+
+ private void Element_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ // if not dragging
+ if (!_isDragging)
+ {
+ _previousCursor = _splitterPreviousPointer = Window.Current.CoreWindow.PointerCursor;
+ UpdateDisplayCursor();
+ }
+
+ // if dragging
+ else
+ {
+ _previousCursor = _splitterPreviousPointer;
+ }
+ }
+
+ private void UpdateDisplayCursor()
+ {
+ if (_gripperCursor == GridSplitter.GripperCursorType.Default)
+ {
+ if (_gridSplitterDirection == GridSplitter.GridResizeDirection.Columns)
+ {
+ Window.Current.CoreWindow.PointerCursor = GridSplitter.ColumnsSplitterCursor;
+ }
+ else if (_gridSplitterDirection == GridSplitter.GridResizeDirection.Rows)
+ {
+ Window.Current.CoreWindow.PointerCursor = GridSplitter.RowSplitterCursor;
+ }
+ }
+ else
+ {
+ var coreCursor = (CoreCursorType)((int)_gripperCursor);
+ if (_gripperCursor == GridSplitter.GripperCursorType.Custom)
+ {
+ if (_gripperCustomCursorResource > GridSplitter.GripperCustomCursorDefaultResource)
+ {
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(coreCursor, (uint)_gripperCustomCursorResource);
+ }
+ }
+ else
+ {
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(coreCursor, 1);
+ }
+ }
+ }
+
+ internal void SplitterManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
+ {
+ if (!(sender is GridSplitter splitter))
+ {
+ return;
+ }
+
+ _splitterPreviousPointer = splitter.PreviousCursor;
+ _isDragging = true;
+ }
+
+ internal void SplitterManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
+ {
+ if (!(sender is GridSplitter splitter))
+ {
+ return;
+ }
+
+ Window.Current.CoreWindow.PointerCursor = splitter.PreviousCursor = _splitterPreviousPointer;
+ _isDragging = false;
+ }
+
+ internal void UnhookEvents()
+ {
+ if (_element == null)
+ {
+ return;
+ }
+
+ _element.PointerEntered -= Element_PointerEntered;
+ _element.PointerExited -= Element_PointerExited;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Helpers/DispatcherQueueHelper.cs b/src/src/Notepads.Controls/Helpers/DispatcherQueueHelper.cs
new file mode 100644
index 0000000..5eb2f34
--- /dev/null
+++ b/src/src/Notepads.Controls/Helpers/DispatcherQueueHelper.cs
@@ -0,0 +1,250 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/8464f8e5263686c1484732bdea86ebba3f30a075/Microsoft.Toolkit.Uwp/Helpers/DispatcherQueueHelper.cs
+
+namespace Notepads.Controls.Helpers
+{
+ using System;
+ using System.Threading.Tasks;
+ using Windows.System;
+
+ ///
+ /// This class provides static methods helper for executing code in a DispatcherQueue.
+ ///
+ public static class DispatcherQueueHelper
+ {
+ ///
+ /// Extension method for . Offering an actual awaitable with optional result that will be executed on the given dispatcher.
+ ///
+ /// DispatcherQueue of a thread to run .
+ /// Function to be executed on the given dispatcher.
+ /// DispatcherQueue execution priority, default is normal.
+ /// An awaitable for the operation.
+ /// If the current thread has UI access, will be invoked directly.
+ public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Action function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
+ {
+ if (function is null)
+ {
+ throw new ArgumentNullException(nameof(function));
+ }
+
+ /* Run the function directly when we have thread access.
+ * Also reuse Task.CompletedTask in case of success,
+ * to skip an unnecessary heap allocation for every invocation. */
+
+ // Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
+ /*
+ if (dispatcher.HasThreadAccess)
+ {
+ try
+ {
+ function();
+
+ return Task.CompletedTask;
+ }
+ catch (Exception e)
+ {
+ return Task.FromException(e);
+ }
+ }
+ */
+
+ var taskCompletionSource = new TaskCompletionSource();
+
+ _ = dispatcher?.TryEnqueue(priority, () =>
+ {
+ try
+ {
+ function();
+
+ taskCompletionSource.SetResult(null);
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ }
+ });
+
+ return taskCompletionSource.Task;
+ }
+
+ ///
+ /// Extension method for . Offering an actual awaitable with optional result that will be executed on the given dispatcher.
+ ///
+ /// Returned data type of the function.
+ /// DispatcherQueue of a thread to run .
+ /// Function to be executed on the given dispatcher.
+ /// DispatcherQueue execution priority, default is normal.
+ /// An awaitable for the operation.
+ /// If the current thread has UI access, will be invoked directly.
+ public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Func function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
+ {
+ if (function is null)
+ {
+ throw new ArgumentNullException(nameof(function));
+ }
+
+ // Skip the dispatch, if possible
+ // Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
+ /*
+ if (dispatcher.HasThreadAccess)
+ {
+ try
+ {
+ return Task.FromResult(function());
+ }
+ catch (Exception e)
+ {
+ return Task.FromException(e);
+ }
+ }
+ */
+
+ var taskCompletionSource = new TaskCompletionSource();
+
+ _ = dispatcher?.TryEnqueue(priority, () =>
+ {
+ try
+ {
+ taskCompletionSource.SetResult(function());
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ }
+ });
+
+ return taskCompletionSource.Task;
+ }
+
+ ///
+ /// Extension method for . Offering an actual awaitable with optional result that will be executed on the given dispatcher.
+ ///
+ /// DispatcherQueue of a thread to run .
+ /// Asynchronous function to be executed on the given dispatcher.
+ /// DispatcherQueue execution priority, default is normal.
+ /// An awaitable for the operation.
+ /// If the current thread has UI access, will be invoked directly.
+ public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Func function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
+ {
+ if (function is null)
+ {
+ throw new ArgumentNullException(nameof(function));
+ }
+
+ /* If we have thread access, we can retrieve the task directly.
+ * We don't use ConfigureAwait(false) in this case, in order
+ * to let the caller continue its execution on the same thread
+ * after awaiting the task returned by this function. */
+
+ // Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
+ /*
+ if (dispatcher.HasThreadAccess)
+ {
+ try
+ {
+ if (function() is Task awaitableResult)
+ {
+ return awaitableResult;
+ }
+
+ return Task.FromException(new InvalidOperationException("The Task returned by function cannot be null."));
+ }
+ catch (Exception e)
+ {
+ return Task.FromException(e);
+ }
+ }
+ */
+
+ var taskCompletionSource = new TaskCompletionSource();
+
+ _ = dispatcher?.TryEnqueue(priority, async () =>
+ {
+ try
+ {
+ if (function() is Task awaitableResult)
+ {
+ await awaitableResult.ConfigureAwait(false);
+
+ taskCompletionSource.SetResult(null);
+ }
+ else
+ {
+ taskCompletionSource.SetException(new InvalidOperationException("The Task returned by function cannot be null."));
+ }
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ }
+ });
+
+ return taskCompletionSource.Task;
+ }
+
+ ///
+ /// Extension method for . Offering an actual awaitable with optional result that will be executed on the given dispatcher.
+ ///
+ /// Returned data type of the function.
+ /// DispatcherQueue of a thread to run .
+ /// Asynchronous function to be executed asynchronously on the given dispatcher.
+ /// DispatcherQueue execution priority, default is normal.
+ /// An awaitable for the operation.
+ /// If the current thread has UI access, will be invoked directly.
+ public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Func> function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
+ {
+ if (function is null)
+ {
+ throw new ArgumentNullException(nameof(function));
+ }
+
+ // Skip the dispatch, if possible
+ // Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
+ /*
+ if (dispatcher.HasThreadAccess)
+ {
+ try
+ {
+ if (function() is Task awaitableResult)
+ {
+ return awaitableResult;
+ }
+
+ return Task.FromException(new InvalidOperationException("The Task returned by function cannot be null."));
+ }
+ catch (Exception e)
+ {
+ return Task.FromException(e);
+ }
+ }
+ */
+
+ var taskCompletionSource = new TaskCompletionSource();
+
+ _ = dispatcher?.TryEnqueue(priority, async () =>
+ {
+ try
+ {
+ if (function() is Task awaitableResult)
+ {
+ var result = await awaitableResult.ConfigureAwait(false);
+
+ taskCompletionSource.SetResult(result);
+ }
+ else
+ {
+ taskCompletionSource.SetException(new InvalidOperationException("The Task returned by function cannot be null."));
+ }
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ }
+ });
+
+ return taskCompletionSource.Task;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Helpers/ThemeListener.cs b/src/src/Notepads.Controls/Helpers/ThemeListener.cs
new file mode 100644
index 0000000..c05b5c0
--- /dev/null
+++ b/src/src/Notepads.Controls/Helpers/ThemeListener.cs
@@ -0,0 +1,144 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/8464f8e5263686c1484732bdea86ebba3f30a075/Microsoft.Toolkit.Uwp.UI/Helpers/ThemeListener.cs
+
+namespace Notepads.Controls.Helpers
+{
+ using System;
+ using System.Threading.Tasks;
+ using Windows.Foundation.Metadata;
+ using Windows.System;
+ using Windows.UI.ViewManagement;
+ using Windows.UI.Xaml;
+
+ ///
+ /// The Delegate for a ThemeChanged Event.
+ ///
+ /// Sender ThemeListener
+ public delegate void ThemeChangedEvent(ThemeListener sender);
+
+ ///
+ /// Class which listens for changes to Application Theme or High Contrast Modes
+ /// and Signals an Event when they occur.
+ ///
+ [AllowForWeb]
+ public sealed class ThemeListener : IDisposable
+ {
+ ///
+ /// Gets the Name of the Current Theme.
+ ///
+ public string CurrentThemeName => CurrentTheme.ToString();
+
+ ///
+ /// Gets or sets the Current Theme.
+ ///
+ public ApplicationTheme CurrentTheme { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the current theme is high contrast.
+ ///
+ public bool IsHighContrast { get; set; }
+
+ ///
+ /// Gets or sets which DispatcherQueue is used to dispatch UI updates.
+ ///
+ public DispatcherQueue DispatcherQueue { get; set; }
+
+ ///
+ /// An event that fires if the Theme changes.
+ ///
+ public event ThemeChangedEvent ThemeChanged;
+
+ private readonly AccessibilitySettings _accessible = new AccessibilitySettings();
+ private readonly UISettings _settings = new UISettings();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The DispatcherQueue that should be used to dispatch UI updates, or null if this is being called from the UI thread.
+ public ThemeListener(DispatcherQueue dispatcherQueue = null)
+ {
+ CurrentTheme = Application.Current.RequestedTheme;
+ IsHighContrast = _accessible.HighContrast;
+
+ DispatcherQueue = dispatcherQueue ?? DispatcherQueue.GetForCurrentThread();
+
+ _accessible.HighContrastChanged += Accessible_HighContrastChanged;
+ _settings.ColorValuesChanged += Settings_ColorValuesChanged;
+
+ // Fallback in case either of the above fail, we'll check when we get activated next.
+ if (Window.Current != null)
+ {
+ Window.Current.CoreWindow.Activated += CoreWindow_Activated;
+ }
+ }
+
+ private async void Accessible_HighContrastChanged(AccessibilitySettings sender, object args)
+ {
+ await DispatcherQueue.ExecuteOnUIThreadAsync(UpdateProperties, DispatcherQueuePriority.Normal);
+ }
+
+ // Note: This can get called multiple times during HighContrast switch, do we care?
+ private async void Settings_ColorValuesChanged(UISettings sender, object args)
+ {
+ await OnColorValuesChanged();
+ }
+
+ internal Task OnColorValuesChanged()
+ {
+ // Getting called off thread, so we need to dispatch to request value.
+ return DispatcherQueue.ExecuteOnUIThreadAsync(
+ () =>
+ {
+ // TODO: This doesn't stop the multiple calls if we're in our faked 'White' HighContrast Mode below.
+ if (CurrentTheme != Application.Current.RequestedTheme ||
+ IsHighContrast != _accessible.HighContrast)
+ {
+ UpdateProperties();
+ }
+ }, DispatcherQueuePriority.Normal);
+ }
+
+ private void CoreWindow_Activated(Windows.UI.Core.CoreWindow sender, Windows.UI.Core.WindowActivatedEventArgs args)
+ {
+ if (CurrentTheme != Application.Current.RequestedTheme ||
+ IsHighContrast != _accessible.HighContrast)
+ {
+ UpdateProperties();
+ }
+ }
+
+ ///
+ /// Set our current properties and fire a change notification.
+ ///
+ private void UpdateProperties()
+ {
+ // TODO: Not sure if HighContrastScheme names are localized?
+ if (_accessible.HighContrast && _accessible.HighContrastScheme.IndexOf("white", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ IsHighContrast = false;
+ CurrentTheme = ApplicationTheme.Light;
+ }
+ else
+ {
+ // Otherwise, we just set to what's in the system as we'd expect.
+ IsHighContrast = _accessible.HighContrast;
+ CurrentTheme = Application.Current.RequestedTheme;
+ }
+
+ ThemeChanged?.Invoke(this);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _accessible.HighContrastChanged -= Accessible_HighContrastChanged;
+ _settings.ColorValuesChanged -= Settings_ColorValuesChanged;
+ if (Window.Current != null)
+ {
+ Window.Current.CoreWindow.Activated -= CoreWindow_Activated;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.AttachedProperties.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotification.AttachedProperties.cs
new file mode 100644
index 0000000..c8effb5
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.AttachedProperties.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media.Animation;
+
+ ///
+ /// In App Notification defines a control to show local notification in the app.
+ ///
+ public partial class InAppNotification
+ {
+ ///
+ /// Gets the value of the KeyFrameDuration attached Property
+ ///
+ /// the KeyFrame where the duration is set
+ /// Value of KeyFrameDuration
+ public static TimeSpan GetKeyFrameDuration(DependencyObject obj)
+ {
+ return (TimeSpan)obj.GetValue(KeyFrameDurationProperty);
+ }
+
+ ///
+ /// Sets the value of the KeyFrameDuration attached property
+ ///
+ /// The KeyFrame object where the property is attached
+ /// The TimeSpan value to be set as duration
+ public static void SetKeyFrameDuration(DependencyObject obj, TimeSpan value)
+ {
+ obj.SetValue(KeyFrameDurationProperty, value);
+ }
+
+ ///
+ /// Using a DependencyProperty as the backing store for KeyFrameDuration. This enables animation, styling, binding, etc
+ ///
+ public static readonly DependencyProperty KeyFrameDurationProperty =
+ DependencyProperty.RegisterAttached("KeyFrameDuration", typeof(TimeSpan), typeof(InAppNotification), new PropertyMetadata(0, OnKeyFrameAnimationChanged));
+
+ private static void OnKeyFrameAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is TimeSpan ts)
+ {
+ if (d is DoubleKeyFrame dkf)
+ {
+ dkf.KeyTime = KeyTime.FromTimeSpan(ts);
+ }
+ else if (d is ObjectKeyFrame okf)
+ {
+ okf.KeyTime = KeyTime.FromTimeSpan(ts);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.Constants.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Constants.cs
new file mode 100644
index 0000000..d1e4dd9
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Constants.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ ///
+ /// In App Notification defines a control to show local notification in the app.
+ ///
+ public partial class InAppNotification
+ {
+ ///
+ /// Key of the VisualStateGroup that show/dismiss content
+ ///
+ private const string GroupContent = "State";
+
+ ///
+ /// Key of the VisualState when content is showed
+ ///
+ private const string StateContentVisible = "Visible";
+
+ ///
+ /// Key of the VisualState when content is dismissed
+ ///
+ private const string StateContentCollapsed = "Collapsed";
+
+ ///
+ /// Key of the UI Element that dismiss the control
+ ///
+ private const string DismissButtonPart = "PART_DismissButton";
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.Events.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Events.cs
new file mode 100644
index 0000000..ae6cbfa
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Events.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Automation;
+ using Windows.UI.Xaml.Automation.Peers;
+
+ ///
+ /// In App Notification defines a control to show local notification in the app.
+ ///
+ public partial class InAppNotification
+ {
+ ///
+ /// Event raised when the notification is opening
+ ///
+ public event InAppNotificationOpeningEventHandler Opening;
+
+ ///
+ /// Event raised when the notification is opened
+ ///
+ public event EventHandler Opened;
+
+ ///
+ /// Event raised when the notification is closing
+ ///
+ public event InAppNotificationClosingEventHandler Closing;
+
+ ///
+ /// Event raised when the notification is closed
+ ///
+ public event InAppNotificationClosedEventHandler Closed;
+
+ private AutomationPeer peer;
+
+ private void DismissButton_Click(object sender, RoutedEventArgs e)
+ {
+ Dismiss(InAppNotificationDismissKind.User);
+ }
+
+ private void DismissTimer_Tick(object sender, object e)
+ {
+ Dismiss(InAppNotificationDismissKind.Timeout);
+ }
+
+ private void OpenAnimationTimer_Tick(object sender, object e)
+ {
+ lock (_openAnimationTimer)
+ {
+ _openAnimationTimer.Stop();
+ Opened?.Invoke(this, EventArgs.Empty);
+ SetValue(AutomationProperties.NameProperty, "Notification");
+ peer = FrameworkElementAutomationPeer.CreatePeerForElement(ContentTemplateRoot);
+ if (Content?.GetType() == typeof(string))
+ {
+ AutomateTextNotification(Content.ToString());
+ }
+ }
+ }
+
+ private void AutomateTextNotification(string message)
+ {
+ if (peer != null)
+ {
+ peer.SetFocus();
+ peer.RaiseNotificationEvent(
+ AutomationNotificationKind.Other,
+ AutomationNotificationProcessing.ImportantMostRecent,
+ "New notification" + message,
+ Guid.NewGuid().ToString());
+ }
+ }
+
+ private void ClosingAnimationTimer_Tick(object sender, object e)
+ {
+ lock (_closingAnimationTimer)
+ {
+ _closingAnimationTimer.Stop();
+ Closed?.Invoke(this, new InAppNotificationClosedEventArgs(_lastDismissKind));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.Properties.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Properties.cs
new file mode 100644
index 0000000..3f7431a
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.Properties.cs
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.UI.Xaml;
+
+ ///
+ /// In App Notification defines a control to show local notification in the app.
+ ///
+ public partial class InAppNotification
+ {
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ShowDismissButtonProperty =
+ DependencyProperty.Register(nameof(ShowDismissButton), typeof(bool), typeof(InAppNotification), new PropertyMetadata(true, OnShowDismissButtonChanged));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty AnimationDurationProperty =
+ DependencyProperty.Register(nameof(AnimationDuration), typeof(TimeSpan), typeof(InAppNotification), new PropertyMetadata(TimeSpan.FromMilliseconds(100)));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty VerticalOffsetProperty =
+ DependencyProperty.Register(nameof(VerticalOffset), typeof(double), typeof(InAppNotification), new PropertyMetadata(100));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty HorizontalOffsetProperty =
+ DependencyProperty.Register(nameof(HorizontalOffset), typeof(double), typeof(InAppNotification), new PropertyMetadata(0));
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty StackModeProperty =
+ DependencyProperty.Register(nameof(StackMode), typeof(StackMode), typeof(InAppNotification), new PropertyMetadata(StackMode.Replace));
+
+ ///
+ /// Gets or sets a value indicating whether to show the Dismiss button of the control.
+ ///
+ public bool ShowDismissButton
+ {
+ get => (bool)GetValue(ShowDismissButtonProperty);
+ set => SetValue(ShowDismissButtonProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating the duration of the popup animation (in milliseconds).
+ ///
+ public TimeSpan AnimationDuration
+ {
+ get => (TimeSpan)GetValue(AnimationDurationProperty);
+ set => SetValue(AnimationDurationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating the vertical offset of the popup animation.
+ ///
+ public double VerticalOffset
+ {
+ get => (double)GetValue(VerticalOffsetProperty);
+ set => SetValue(VerticalOffsetProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating the horizontal offset of the popup animation.
+ ///
+ public double HorizontalOffset
+ {
+ get => (double)GetValue(HorizontalOffsetProperty);
+ set => SetValue(HorizontalOffsetProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating the stack mode of the notifications.
+ ///
+ public StackMode StackMode
+ {
+ get => (StackMode)GetValue(StackModeProperty);
+ set => SetValue(StackModeProperty, value);
+ }
+
+ private static void OnShowDismissButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var inApNotification = d as InAppNotification;
+
+ if (inApNotification._dismissButton != null)
+ {
+ bool showDismissButton = (bool)e.NewValue;
+ inApNotification._dismissButton.Visibility = showDismissButton ? Visibility.Visible : Visibility.Collapsed;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotification.cs
new file mode 100644
index 0000000..667b465
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.cs
@@ -0,0 +1,274 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+
+ ///
+ /// In App Notification defines a control to show local notification in the app.
+ ///
+ [TemplateVisualState(Name = StateContentVisible, GroupName = GroupContent)]
+ [TemplateVisualState(Name = StateContentCollapsed, GroupName = GroupContent)]
+ [TemplatePart(Name = DismissButtonPart, Type = typeof(Button))]
+ public partial class InAppNotification : ContentControl
+ {
+ private InAppNotificationDismissKind _lastDismissKind;
+ private readonly DispatcherTimer _openAnimationTimer = new DispatcherTimer();
+ private readonly DispatcherTimer _closingAnimationTimer = new DispatcherTimer();
+ private readonly DispatcherTimer _dismissTimer = new DispatcherTimer();
+ private Button _dismissButton;
+ //private VisualStateGroup _visualStateGroup;
+ private readonly List _stackedNotificationOptions = new List();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InAppNotification()
+ {
+ DefaultStyleKey = typeof(InAppNotification);
+
+ _dismissTimer.Tick += DismissTimer_Tick;
+ _openAnimationTimer.Tick += OpenAnimationTimer_Tick;
+ _closingAnimationTimer.Tick += ClosingAnimationTimer_Tick;
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ if (_dismissButton != null)
+ {
+ _dismissButton.Click -= DismissButton_Click;
+ }
+
+ _dismissButton = (Button)GetTemplateChild(DismissButtonPart);
+ //_visualStateGroup = (VisualStateGroup)GetTemplateChild(GroupContent);
+
+ if (_dismissButton != null)
+ {
+ _dismissButton.Visibility = ShowDismissButton ? Visibility.Visible : Visibility.Collapsed;
+ _dismissButton.Click += DismissButton_Click;
+ }
+
+ if (Visibility == Visibility.Visible)
+ {
+ VisualStateManager.GoToState(this, StateContentVisible, true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, StateContentCollapsed, true);
+ }
+
+ base.OnApplyTemplate();
+ }
+
+ ///
+ /// Show notification using the current template
+ ///
+ /// Displayed duration of the notification in ms (less or equal 0 means infinite duration)
+ public void Show(int duration = 0)
+ {
+ lock (_openAnimationTimer)
+ lock (_closingAnimationTimer)
+ lock (_dismissTimer)
+ {
+ _openAnimationTimer.Stop();
+ _closingAnimationTimer.Stop();
+ _dismissTimer.Stop();
+
+ var eventArgs = new InAppNotificationOpeningEventArgs();
+ Opening?.Invoke(this, eventArgs);
+
+ if (eventArgs.Cancel)
+ {
+ return;
+ }
+
+ Visibility = Visibility.Visible;
+ VisualStateManager.GoToState(this, StateContentVisible, true);
+
+ _openAnimationTimer.Interval = AnimationDuration;
+ _openAnimationTimer.Start();
+
+ if (duration > 0)
+ {
+ _dismissTimer.Interval = TimeSpan.FromMilliseconds(duration);
+ _dismissTimer.Start();
+ }
+ }
+ }
+
+ ///
+ /// Show notification using text as the content of the notification
+ ///
+ /// Text used as the content of the notification
+ /// Displayed duration of the notification in ms (less or equal 0 means infinite duration)
+ public void Show(string text, int duration = 0)
+ {
+ var notificationOptions = new NotificationOptions
+ {
+ Duration = duration,
+ Content = text
+ };
+ Show(notificationOptions);
+ }
+
+ ///
+ /// Show notification using UIElement as the content of the notification
+ ///
+ /// UIElement used as the content of the notification
+ /// Displayed duration of the notification in ms (less or equal 0 means infinite duration)
+ public void Show(UIElement element, int duration = 0)
+ {
+ var notificationOptions = new NotificationOptions
+ {
+ Duration = duration,
+ Content = element
+ };
+ Show(notificationOptions);
+ }
+
+ ///
+ /// Show notification using DataTemplate as the content of the notification
+ ///
+ /// DataTemplate used as the content of the notification
+ /// Displayed duration of the notification in ms (less or equal 0 means infinite duration)
+ public void Show(DataTemplate dataTemplate, int duration = 0)
+ {
+ var notificationOptions = new NotificationOptions
+ {
+ Duration = duration,
+ Content = dataTemplate
+ };
+ Show(notificationOptions);
+ }
+
+ ///
+ /// Dismiss the notification
+ ///
+ public void Dismiss()
+ {
+ Dismiss(InAppNotificationDismissKind.Programmatic);
+ }
+
+ ///
+ /// Dismiss the notification
+ ///
+ /// Kind of action that triggered dismiss event
+ private void Dismiss(InAppNotificationDismissKind dismissKind)
+ {
+ lock (_openAnimationTimer)
+ lock (_closingAnimationTimer)
+ lock (_dismissTimer)
+ {
+ if (Visibility == Visibility.Visible)
+ {
+ _dismissTimer.Stop();
+
+ // Continue to display notification if on remaining stacked notification
+ if (_stackedNotificationOptions.Any())
+ {
+ _stackedNotificationOptions.RemoveAt(0);
+
+ if (_stackedNotificationOptions.Any())
+ {
+ var notificationOptions = _stackedNotificationOptions[0];
+
+ UpdateContent(notificationOptions);
+
+ if (notificationOptions.Duration > 0)
+ {
+ _dismissTimer.Interval = TimeSpan.FromMilliseconds(notificationOptions.Duration);
+ _dismissTimer.Start();
+ }
+
+ return;
+ }
+ }
+
+ _openAnimationTimer.Stop();
+ _closingAnimationTimer.Stop();
+
+ var closingEventArgs = new InAppNotificationClosingEventArgs(dismissKind);
+ Closing?.Invoke(this, closingEventArgs);
+
+ if (closingEventArgs.Cancel)
+ {
+ return;
+ }
+
+ VisualStateManager.GoToState(this, StateContentCollapsed, true);
+
+ _lastDismissKind = dismissKind;
+
+ _closingAnimationTimer.Interval = AnimationDuration;
+ _closingAnimationTimer.Start();
+ }
+ }
+ }
+
+ ///
+ /// Informs if the notification should be displayed immediately (based on the StackMode)
+ ///
+ /// True if notification should be displayed immediately
+ private bool ShouldDisplayImmediately()
+ {
+ return StackMode != StackMode.QueueBehind ||
+ (StackMode == StackMode.QueueBehind && _stackedNotificationOptions.Count == 0);
+ }
+
+ ///
+ /// Update the Content of the notification
+ ///
+ /// Information about the notification to display
+ private void UpdateContent(NotificationOptions notificationOptions)
+ {
+ switch (notificationOptions.Content)
+ {
+ case string text:
+ ContentTemplate = null;
+ Content = text;
+ break;
+ case UIElement element:
+ ContentTemplate = null;
+ Content = element;
+ break;
+ case DataTemplate dataTemplate:
+ ContentTemplate = dataTemplate;
+ Content = null;
+ break;
+ }
+ }
+
+ ///
+ /// Handle the display of the notification based on the current StackMode
+ ///
+ /// Information about the notification to display
+ private void Show(NotificationOptions notificationOptions)
+ {
+ bool shouldDisplayImmediately = ShouldDisplayImmediately();
+
+ if (StackMode == StackMode.QueueBehind)
+ {
+ _stackedNotificationOptions.Add(notificationOptions);
+ }
+
+ if (StackMode == StackMode.StackInFront)
+ {
+ _stackedNotificationOptions.Insert(0, notificationOptions);
+ }
+
+ if (shouldDisplayImmediately)
+ {
+ UpdateContent(notificationOptions);
+ Show(notificationOptions.Duration);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotification.xaml b/src/src/Notepads.Controls/InAppNotification/InAppNotification.xaml
new file mode 100644
index 0000000..459ff02
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotification.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosedEventArgs.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosedEventArgs.cs
new file mode 100644
index 0000000..bfe6fe0
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosedEventArgs.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// A delegate for dismissing.
+ ///
+ /// The sender.
+ /// The event arguments.
+ public delegate void InAppNotificationClosedEventHandler(object sender, InAppNotificationClosedEventArgs e);
+
+ ///
+ /// Provides data for the Dismissing event.
+ ///
+ public class InAppNotificationClosedEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dismiss kind that triggered the closing event
+ public InAppNotificationClosedEventArgs(InAppNotificationDismissKind dismissKind)
+ {
+ DismissKind = dismissKind;
+ }
+
+ ///
+ /// Gets the kind of action for the closing event.
+ ///
+ public InAppNotificationDismissKind DismissKind { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosingEventArgs.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosingEventArgs.cs
new file mode 100644
index 0000000..b2ba760
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotificationClosingEventArgs.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// A delegate for dismissing.
+ ///
+ /// The sender.
+ /// The event arguments.
+ public delegate void InAppNotificationClosingEventHandler(object sender, InAppNotificationClosingEventArgs e);
+
+ ///
+ /// Provides data for the Dismissing event.
+ ///
+ public class InAppNotificationClosingEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dismiss kind that triggered the closing event
+ public InAppNotificationClosingEventArgs(InAppNotificationDismissKind dismissKind)
+ {
+ DismissKind = dismissKind;
+ }
+
+ ///
+ /// Gets the kind of action for the closing event.
+ ///
+ public InAppNotificationDismissKind DismissKind { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether the notification should be closed.
+ ///
+ public bool Cancel { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotificationDismissKind.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotificationDismissKind.cs
new file mode 100644
index 0000000..f6d3355
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotificationDismissKind.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ ///
+ /// Enumeration to describe how an InAppNotification was dismissed
+ ///
+ public enum InAppNotificationDismissKind
+ {
+ ///
+ /// When the system dismissed the notification.
+ ///
+ Programmatic,
+
+ ///
+ /// When user explicitly dismissed the notification.
+ ///
+ User,
+
+ ///
+ /// When the system dismissed the notification after timeout.
+ ///
+ Timeout
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/InAppNotificationOpeningEventArgs.cs b/src/src/Notepads.Controls/InAppNotification/InAppNotificationOpeningEventArgs.cs
new file mode 100644
index 0000000..78e39cc
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/InAppNotificationOpeningEventArgs.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// A delegate for opening.
+ ///
+ /// The sender.
+ /// The event arguments.
+ public delegate void InAppNotificationOpeningEventHandler(object sender, InAppNotificationOpeningEventArgs e);
+
+ ///
+ /// Provides data for the Dismissing event.
+ ///
+ public class InAppNotificationOpeningEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InAppNotificationOpeningEventArgs()
+ {
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the notification should be opened.
+ ///
+ public bool Cancel { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/NotificationOptions.cs b/src/src/Notepads.Controls/InAppNotification/NotificationOptions.cs
new file mode 100644
index 0000000..ca67a8d
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/NotificationOptions.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+
+ ///
+ /// Base class that contains options of notification
+ ///
+ internal class NotificationOptions
+ {
+ ///
+ /// Gets or sets duration of the stacked notification
+ ///
+ public int Duration { get; set; }
+
+ ///
+ /// Gets or sets Content of the notification
+ /// Could be either a or a or a
+ ///
+ public object Content { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/StackMode.cs b/src/src/Notepads.Controls/InAppNotification/StackMode.cs
new file mode 100644
index 0000000..e619a10
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/StackMode.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
+
+namespace Notepads.Controls
+{
+ ///
+ /// The Stack mode of an in-app notification.
+ ///
+ public enum StackMode
+ {
+ ///
+ /// Each notification will replace the previous one
+ ///
+ Replace,
+
+ ///
+ /// Opening a notification will display it immediately, remaining notifications will appear when a notification is dismissed
+ ///
+ StackInFront,
+
+ ///
+ /// Dismissing a notification will show the next one in the queue
+ ///
+ QueueBehind
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/InAppNotification/Styles/MSEdgeNotificationStyle.xaml b/src/src/Notepads.Controls/InAppNotification/Styles/MSEdgeNotificationStyle.xaml
new file mode 100644
index 0000000..a560dc3
--- /dev/null
+++ b/src/src/Notepads.Controls/InAppNotification/Styles/MSEdgeNotificationStyle.xaml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Visible
+
+
+
+
+ Collapsed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/CodeBlockResolvingEventArgs.cs b/src/src/Notepads.Controls/MarkdownTextBlock/CodeBlockResolvingEventArgs.cs
new file mode 100644
index 0000000..ef01363
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/CodeBlockResolvingEventArgs.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.UI.Xaml.Documents;
+
+ ///
+ /// Arguments for the event when a Code Block is being rendered.
+ ///
+ public class CodeBlockResolvingEventArgs : EventArgs
+ {
+ internal CodeBlockResolvingEventArgs(InlineCollection inlineCollection, string text, string codeLanguage)
+ {
+ InlineCollection = inlineCollection;
+ Text = text;
+ CodeLanguage = codeLanguage;
+ }
+
+ ///
+ /// Gets the language of the Code Block, as specified by ```{Language} on the first line of the block,
+ /// e.g.
+ /// ```C#
+ /// public void Method();
+ /// ```
+ ///
+ public string CodeLanguage { get; }
+
+ ///
+ /// Gets the raw code block text
+ ///
+ public string Text { get; }
+
+ ///
+ /// Gets Collection to add formatted Text to.
+ ///
+ public InlineCollection InlineCollection { get; }
+
+ ///
+ /// Gets or sets a value indicating whether this event was handled successfully.
+ ///
+ public bool Handled { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/ImageResolvingEventArgs.cs b/src/src/Notepads.Controls/MarkdownTextBlock/ImageResolvingEventArgs.cs
new file mode 100644
index 0000000..601b77a
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/ImageResolvingEventArgs.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Windows.Foundation;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Arguments for the event which is called when a url needs to be resolved to a .
+ ///
+ public class ImageResolvingEventArgs : EventArgs
+ {
+ private readonly IList> _deferrals;
+
+ internal ImageResolvingEventArgs(string url, string tooltip)
+ {
+ _deferrals = new List>();
+ Url = url;
+ Tooltip = tooltip;
+ }
+
+ ///
+ /// Gets the url of the image in the markdown document.
+ ///
+ public string Url { get; }
+
+ ///
+ /// Gets the tooltip of the image in the markdown document.
+ ///
+ public string Tooltip { get; }
+
+ ///
+ /// Gets or sets a value indicating whether this event was handled successfully.
+ ///
+ public bool Handled { get; set; }
+
+ ///
+ /// Gets or sets the image to display in the .
+ ///
+ public ImageSource Image { get; set; }
+
+ ///
+ /// Informs the that the event handler might run asynchronously.
+ ///
+ /// Deferral
+ public Deferral GetDeferral()
+ {
+ var task = new TaskCompletionSource();
+ _deferrals.Add(task);
+
+ return new Deferral(() =>
+ {
+ task.SetResult(null);
+ });
+ }
+
+ ///
+ /// Returns a that completes when all s have completed.
+ ///
+ /// A representing the asynchronous operation.
+ internal Task WaitForDeferrals()
+ {
+ return Task.WhenAll(_deferrals.Select(f => f.Task));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/LinkClickedEventArgs.cs b/src/src/Notepads.Controls/MarkdownTextBlock/LinkClickedEventArgs.cs
new file mode 100644
index 0000000..8a83e2b
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/LinkClickedEventArgs.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// Arguments for the OnLinkClicked event which is fired then the user presses a link.
+ ///
+ public class LinkClickedEventArgs : EventArgs
+ {
+ internal LinkClickedEventArgs(string link)
+ {
+ Link = link;
+ }
+
+ ///
+ /// Gets the link that was tapped.
+ ///
+ public string Link { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/CodeBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/CodeBlock.cs
new file mode 100644
index 0000000..79abd14
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/CodeBlock.cs
@@ -0,0 +1,196 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Text;
+
+ ///
+ /// Represents a block of text that is displayed in a fixed-width font. Inline elements and
+ /// escape sequences are ignored inside the code block.
+ ///
+ public class CodeBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CodeBlock()
+ : base(MarkdownBlockType.Code)
+ {
+ }
+
+ ///
+ /// Gets or sets the source code to display.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets the Language specified in prefix, e.g. ```c# (Github Style Parsing).
+ /// This does not guarantee that the Code Block has a language, or no language, some valid code might not have been prefixed, and this will still return null.
+ /// To ensure all Code is Highlighted (If desired), you might have to determine the language from the provided string, such as looking for key words.
+ ///
+ public string CodeLanguage { get; set; }
+
+ ///
+ /// Parses a code block.
+ ///
+ /// The markdown text.
+ /// The location of the first character in the block.
+ /// The location to stop parsing.
+ /// The current nesting level for block quoting.
+ /// Set to the end of the block when the return value is non-null.
+ /// A parsed code block, or null if this is not a code block.
+ internal static CodeBlock Parse(string markdown, int start, int maxEnd, int quoteDepth, out int actualEnd)
+ {
+ StringBuilder code = null;
+ actualEnd = start;
+ bool insideCodeBlock = false;
+ string codeLanguage = string.Empty;
+
+ /*
+ Two options here:
+ Either every line starts with a tab character or at least 4 spaces
+ Or the code block starts and ends with ```
+ */
+
+ foreach (var lineInfo in Common.ParseLines(markdown, start, maxEnd, quoteDepth))
+ {
+ int pos = lineInfo.StartOfLine;
+ if (pos < maxEnd && markdown[pos] == '`')
+ {
+ var backTickCount = 0;
+ while (pos < maxEnd && backTickCount < 3)
+ {
+ if (markdown[pos] == '`')
+ {
+ backTickCount++;
+ }
+ else
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ if (backTickCount == 3)
+ {
+ insideCodeBlock = !insideCodeBlock;
+
+ if (!insideCodeBlock)
+ {
+ actualEnd = lineInfo.StartOfNextLine;
+ break;
+ }
+ else
+ {
+ // Collects the Programming Language from the end of the starting ticks.
+ while (pos < lineInfo.EndOfLine)
+ {
+ codeLanguage += markdown[pos];
+ pos++;
+ }
+ }
+ }
+ }
+
+ if (!insideCodeBlock)
+ {
+ // Add every line that starts with a tab character or at least 4 spaces.
+ if (pos < maxEnd && markdown[pos] == '\t')
+ {
+ pos++;
+ }
+ else
+ {
+ int spaceCount = 0;
+ while (pos < maxEnd && spaceCount < 4)
+ {
+ if (markdown[pos] == ' ')
+ {
+ spaceCount++;
+ }
+ else if (markdown[pos] == '\t')
+ {
+ spaceCount += 4;
+ }
+ else
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ if (spaceCount < 4)
+ {
+ // We found a line that doesn't start with a tab or 4 spaces.
+ // But don't end the code block until we find a non-blank line.
+ if (lineInfo.IsLineBlank == false)
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ // Separate each line of the code text.
+ if (code == null)
+ {
+ code = new StringBuilder();
+ }
+ else
+ {
+ code.AppendLine();
+ }
+
+ if (lineInfo.IsLineBlank == false)
+ {
+ // Append the code text, excluding the first tab/4 spaces, and convert tab characters into spaces.
+ string lineText = markdown.Substring(pos, lineInfo.EndOfLine - pos);
+ int startOfLinePos = code.Length;
+ for (int i = 0; i < lineText.Length; i++)
+ {
+ char c = lineText[i];
+ if (c == '\t')
+ {
+ code.Append(' ', 4 - ((code.Length - startOfLinePos) % 4));
+ }
+ else
+ {
+ code.Append(c);
+ }
+ }
+ }
+
+ // Update the end position.
+ actualEnd = lineInfo.StartOfNextLine;
+ }
+
+ if (code == null)
+ {
+ // Not a valid code block.
+ actualEnd = start;
+ return null;
+ }
+
+ // Blank lines should be trimmed from the start and end.
+ return new CodeBlock()
+ {
+ Text = code.ToString().Trim('\r', '\n'),
+ CodeLanguage = !string.IsNullOrWhiteSpace(codeLanguage) ? codeLanguage.Trim() : null
+ };
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ return Text;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HeaderBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HeaderBlock.cs
new file mode 100644
index 0000000..a4dd874
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HeaderBlock.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a heading.
+ /// Single-Line Header CommonMark Spec
+ /// Two-Line Header CommonMark Spec
+ ///
+ public class HeaderBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HeaderBlock()
+ : base(MarkdownBlockType.Header)
+ {
+ }
+
+ private int _headerLevel;
+
+ ///
+ /// Gets or sets the header level (1-6). 1 is the most important header, 6 is the least important.
+ ///
+ public int HeaderLevel
+ {
+ get => _headerLevel;
+
+ set
+ {
+ if (value < 1 || value > 6)
+ {
+ throw new ArgumentOutOfRangeException("HeaderLevel", "The header level must be between 1 and 6 (inclusive).");
+ }
+
+ _headerLevel = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the contents of the block.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Parses a header that starts with a hash.
+ ///
+ /// The markdown text.
+ /// The location of the first hash character.
+ /// The location of the end of the line.
+ /// A parsed header block, or null if this is not a header.
+ internal static HeaderBlock ParseHashPrefixedHeader(string markdown, int start, int end)
+ {
+ // This type of header starts with one or more '#' characters, followed by the header
+ // text, optionally followed by any number of hash characters.
+ var result = new HeaderBlock();
+
+ // Figure out how many consecutive hash characters there are.
+ int pos = start;
+ while (pos < end && markdown[pos] == '#' && pos - start < 6)
+ {
+ pos++;
+ }
+
+ result.HeaderLevel = pos - start;
+ if (result.HeaderLevel == 0)
+ {
+ return null;
+ }
+
+ // Ignore any hashes at the end of the line.
+ while (pos < end && markdown[end - 1] == '#')
+ {
+ end--;
+ }
+
+ // Parse the inline content.
+ result.Inlines = Common.ParseInlineChildren(markdown, pos, end);
+ return result;
+ }
+
+ ///
+ /// Parses a two-line header.
+ ///
+ /// The markdown text.
+ /// The location of the start of the first line.
+ /// The location of the end of the first line.
+ /// The location of the start of the second line.
+ /// The location of the end of the second line.
+ /// A parsed header block, or null if this is not a header.
+ internal static HeaderBlock ParseUnderlineStyleHeader(string markdown, int firstLineStart, int firstLineEnd, int secondLineStart, int secondLineEnd)
+ {
+ // This type of header starts with some text on the first line, followed by one or more
+ // underline characters ('=' or '-') on the second line.
+ // The second line can have whitespace after the underline characters, but not before
+ // or between each character.
+
+ // Check the second line is valid.
+ if (secondLineEnd <= secondLineStart)
+ {
+ return null;
+ }
+
+ // Figure out what the underline character is ('=' or '-').
+ char underlineChar = markdown[secondLineStart];
+ if (underlineChar != '=' && underlineChar != '-')
+ {
+ return null;
+ }
+
+ // Read past consecutive underline characters.
+ int pos = secondLineStart + 1;
+ for (; pos < secondLineEnd; pos++)
+ {
+ char c = markdown[pos];
+ if (c != underlineChar)
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ // The rest of the line must be whitespace.
+ for (; pos < secondLineEnd; pos++)
+ {
+ char c = markdown[pos];
+ if (c != ' ' && c != '\t')
+ {
+ return null;
+ }
+
+ pos++;
+ }
+
+ var result = new HeaderBlock
+ {
+ HeaderLevel = underlineChar == '=' ? 1 : 2,
+
+ // Parse the inline content.
+ Inlines = Common.ParseInlineChildren(markdown, firstLineStart, firstLineEnd)
+ };
+ return result;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return string.Join(string.Empty, Inlines);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HorizontalRuleBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HorizontalRuleBlock.cs
new file mode 100644
index 0000000..1dd9a35
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/HorizontalRuleBlock.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Represents a horizontal line.
+ ///
+ public class HorizontalRuleBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HorizontalRuleBlock()
+ : base(MarkdownBlockType.HorizontalRule)
+ {
+ }
+
+ ///
+ /// Parses a horizontal rule.
+ ///
+ /// The markdown text.
+ /// The location of the start of the line.
+ /// The location of the end of the line.
+ /// A parsed horizontal rule block, or null if this is not a horizontal rule.
+ internal static HorizontalRuleBlock Parse(string markdown, int start, int end)
+ {
+ // A horizontal rule is a line with at least 3 stars, optionally separated by spaces
+ // OR a line with at least 3 dashes, optionally separated by spaces
+ // OR a line with at least 3 underscores, optionally separated by spaces.
+ char hrChar = '\0';
+ int hrCharCount = 0;
+ int pos = start;
+ while (pos < end)
+ {
+ char c = markdown[pos++];
+ if (c == '*' || c == '-' || c == '_')
+ {
+ // All of the non-whitespace characters on the line must match.
+ if (hrCharCount > 0 && c != hrChar)
+ {
+ return null;
+ }
+
+ hrChar = c;
+ hrCharCount++;
+ }
+ else if (c == '\n')
+ {
+ break;
+ }
+ else if (!ParseHelpers.IsMarkdownWhiteSpace(c))
+ {
+ return null;
+ }
+ }
+
+ // Hopefully there were at least 3 stars/dashes/underscores.
+ return hrCharCount >= 3 ? new HorizontalRuleBlock() : null;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ return "---";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/LinkReferenceBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/LinkReferenceBlock.cs
new file mode 100644
index 0000000..98c1bb4
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/LinkReferenceBlock.cs
@@ -0,0 +1,170 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Represents the target of a reference ([ref][]).
+ ///
+ public class LinkReferenceBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LinkReferenceBlock()
+ : base(MarkdownBlockType.LinkReference)
+ {
+ }
+
+ ///
+ /// Gets or sets the reference ID.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Gets or sets the link URL.
+ ///
+ public string Url { get; set; }
+
+ ///
+ /// Gets or sets a tooltip to display on hover.
+ ///
+ public string Tooltip { get; set; }
+
+ ///
+ /// Attempts to parse a reference e.g. "[example]: http://www.reddit.com 'title'".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed markdown link, or null if this is not a markdown link.
+ internal static LinkReferenceBlock Parse(string markdown, int start, int end)
+ {
+ // Expect a '[' character.
+ if (start >= end || markdown[start] != '[')
+ {
+ return null;
+ }
+
+ // Find the ']' character
+ int pos = start + 1;
+ while (pos < end)
+ {
+ if (markdown[pos] == ']')
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ if (pos == end)
+ {
+ return null;
+ }
+
+ // Extract the ID.
+ string id = markdown.Substring(start + 1, pos - (start + 1));
+
+ // Expect the ':' character.
+ pos++;
+ if (pos == end || markdown[pos] != ':')
+ {
+ return null;
+ }
+
+ // Skip whitespace
+ pos++;
+ while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ if (pos == end)
+ {
+ return null;
+ }
+
+ // Extract the URL.
+ int urlStart = pos;
+ while (pos < end && !ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ string url = TextRunInline.ResolveEscapeSequences(markdown, urlStart, pos);
+
+ // Ignore leading '<' and trailing '>'.
+ url = url.TrimStart('<').TrimEnd('>');
+
+ // Skip whitespace.
+ pos++;
+ while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ string tooltip = null;
+ if (pos < end)
+ {
+ // Extract the tooltip.
+ char tooltipEndChar;
+ switch (markdown[pos])
+ {
+ case '(':
+ tooltipEndChar = ')';
+ break;
+
+ case '"':
+ case '\'':
+ tooltipEndChar = markdown[pos];
+ break;
+
+ default:
+ return null;
+ }
+
+ pos++;
+ int tooltipStart = pos;
+ while (pos < end && markdown[pos] != tooltipEndChar)
+ {
+ pos++;
+ }
+
+ if (pos == end)
+ {
+ return null; // No end character.
+ }
+
+ tooltip = markdown.Substring(tooltipStart, pos - tooltipStart);
+
+ // Check there isn't any trailing text.
+ pos++;
+ while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ if (pos < end)
+ {
+ return null;
+ }
+ }
+
+ // We found something!
+ var result = new LinkReferenceBlock { Id = id, Url = url, Tooltip = tooltip };
+ return result;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ return $"[{Id}]: {Url} {Tooltip}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBlock.cs
new file mode 100644
index 0000000..a65adc6
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBlock.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// This specifies the Content of the List element.
+ ///
+ public class ListItemBlock
+ {
+ ///
+ /// Gets or sets the contents of the list item.
+ ///
+ public IList Blocks { get; set; }
+
+ internal ListItemBlock()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBuilder.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBuilder.cs
new file mode 100644
index 0000000..93c9fe9
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemBuilder.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Text;
+
+ internal class ListItemBuilder : MarkdownBlock
+ {
+ public StringBuilder Builder { get; } = new StringBuilder();
+
+ public ListItemBuilder()
+ : base(MarkdownBlockType.ListItemBuilder)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemPreamble.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemPreamble.cs
new file mode 100644
index 0000000..cef7a13
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/ListItemPreamble.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
+
+namespace Notepads.Controls.Markdown
+{
+ internal class ListItemPreamble
+ {
+ public ListStyle Style { get; set; }
+
+ public int ContentStartPos { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/NestedListInfo.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/NestedListInfo.cs
new file mode 100644
index 0000000..e1f88ba
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/List/NestedListInfo.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
+
+namespace Notepads.Controls.Markdown
+{
+ internal class NestedListInfo
+ {
+ public ListBlock List { get; set; }
+
+ // The number of spaces at the start of the line the list first appeared.
+ public int SpaceCount { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ListBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ListBlock.cs
new file mode 100644
index 0000000..241701c
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ListBlock.cs
@@ -0,0 +1,402 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Text.RegularExpressions;
+
+ ///
+ /// Represents a list, with each list item proceeded by either a number or a bullet.
+ ///
+ public class ListBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ListBlock()
+ : base(MarkdownBlockType.List)
+ {
+ }
+
+ ///
+ /// Gets or sets the list items.
+ ///
+ public IList Items { get; set; }
+
+ ///
+ /// Gets or sets the style of the list, either numbered or bulleted.
+ ///
+ public ListStyle Style { get; set; }
+
+ ///
+ /// Parses a list block.
+ ///
+ /// The markdown text.
+ /// The location of the first character in the block.
+ /// The location to stop parsing.
+ /// The current nesting level for block quoting.
+ /// Set to the end of the block when the return value is non-null.
+ /// A parsed list block, or null if this is not a list block.
+ internal static ListBlock Parse(string markdown, int start, int maxEnd, int quoteDepth, out int actualEnd)
+ {
+ var russianDolls = new List();
+ int russianDollIndex = -1;
+ bool previousLineWasBlank = false;
+ bool inCodeBlock = false;
+ ListItemBlock currentListItem = null;
+ actualEnd = start;
+
+ foreach (var lineInfo in Common.ParseLines(markdown, start, maxEnd, quoteDepth))
+ {
+ // Is this line blank?
+ if (lineInfo.IsLineBlank)
+ {
+ // The line is blank, which means the next line which contains text may end the list (or it may not...).
+ previousLineWasBlank = true;
+ }
+ else
+ {
+ // Does the line contain a list item?
+ ListItemPreamble listItemPreamble = null;
+ if (lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine < (russianDollIndex + 2) * 4)
+ {
+ listItemPreamble = ParseItemPreamble(markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine);
+ }
+
+ if (listItemPreamble != null)
+ {
+ // Yes, this line contains a list item.
+
+ // Determining the nesting level is done as follows:
+ // 1. If this is the first line, then the list is not nested.
+ // 2. If the number of spaces at the start of the line is equal to that of
+ // an existing list, then the nesting level is the same as that list.
+ // 3. Otherwise, if the number of spaces is 0-4, then the nesting level
+ // is one level deep.
+ // 4. Otherwise, if the number of spaces is 5-8, then the nesting level
+ // is two levels deep (but no deeper than one level more than the
+ // previous list item).
+ // 5. Etcetera.
+ ListBlock listToAddTo = null;
+ int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine;
+ russianDollIndex = russianDolls.FindIndex(rd => rd.SpaceCount == spaceCount);
+ if (russianDollIndex >= 0)
+ {
+ // Add the new list item to an existing list.
+ listToAddTo = russianDolls[russianDollIndex].List;
+
+ // Don't add new list items to items higher up in the list.
+ russianDolls.RemoveRange(russianDollIndex + 1, russianDolls.Count - (russianDollIndex + 1));
+ }
+ else
+ {
+ russianDollIndex = Math.Max(1, 1 + ((spaceCount - 1) / 4));
+ if (russianDollIndex < russianDolls.Count)
+ {
+ // Add the new list item to an existing list.
+ listToAddTo = russianDolls[russianDollIndex].List;
+
+ // Don't add new list items to items higher up in the list.
+ russianDolls.RemoveRange(russianDollIndex + 1, russianDolls.Count - (russianDollIndex + 1));
+ }
+ else
+ {
+ // Create a new list.
+ listToAddTo = new ListBlock { Style = listItemPreamble.Style, Items = new List() };
+ if (russianDolls.Count > 0)
+ {
+ currentListItem.Blocks.Add(listToAddTo);
+ }
+
+ russianDollIndex = russianDolls.Count;
+ russianDolls.Add(new NestedListInfo { List = listToAddTo, SpaceCount = spaceCount });
+ }
+ }
+
+ // Add a new list item.
+ currentListItem = new ListItemBlock() { Blocks = new List() };
+ listToAddTo.Items.Add(currentListItem);
+
+ // Add the rest of the line to the builder.
+ AppendTextToListItem(currentListItem, markdown, listItemPreamble.ContentStartPos, lineInfo.EndOfLine);
+ }
+ else
+ {
+ // No, this line contains text.
+
+ // Is there even a list in progress?
+ if (currentListItem == null)
+ {
+ actualEnd = start;
+ return null;
+ }
+
+ // This is the start of a new paragraph.
+ int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine;
+ if (spaceCount == 0)
+ {
+ break;
+ }
+
+ russianDollIndex = Math.Min(russianDollIndex, (spaceCount - 1) / 4);
+ int lineStart = Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + ((russianDollIndex + 1) * 4));
+
+ // 0 spaces = end of the list.
+ // 1-4 spaces = first level.
+ // 5-8 spaces = second level, etc.
+ if (previousLineWasBlank)
+ {
+ ListBlock listToAddTo = russianDolls[russianDollIndex].List;
+ currentListItem = listToAddTo.Items[listToAddTo.Items.Count - 1];
+
+ ListItemBuilder builder;
+
+ // Prevents new Block creation if still in a Code Block.
+ if (!inCodeBlock)
+ {
+ builder = new ListItemBuilder();
+ currentListItem.Blocks.Add(builder);
+ }
+ else
+ {
+ // This can only ever be a ListItemBuilder, so it is not a null reference.
+ builder = currentListItem.Blocks.Last() as ListItemBuilder;
+
+ // Make up for the escaped NewLines.
+ builder.Builder.AppendLine();
+ builder.Builder.AppendLine();
+ }
+
+ AppendTextToListItem(currentListItem, markdown, lineStart, lineInfo.EndOfLine);
+ }
+ else
+ {
+ // Inline text. Ignores the 4 spaces that are used to continue the list.
+ AppendTextToListItem(currentListItem, markdown, lineStart, lineInfo.EndOfLine, true);
+ }
+ }
+
+ // Check for Closing Code Blocks.
+ if (currentListItem.Blocks.Last() is ListItemBuilder currentBlock)
+ {
+ var blockmatchcount = Regex.Matches(currentBlock.Builder.ToString(), "```").Count;
+ if (blockmatchcount > 0 && blockmatchcount % 2 != 0)
+ {
+ inCodeBlock = true;
+ }
+ else
+ {
+ inCodeBlock = false;
+ }
+ }
+
+ // The line was not blank.
+ previousLineWasBlank = false;
+ }
+
+ // Go to the next line.
+ actualEnd = lineInfo.EndOfLine;
+ }
+
+ var result = russianDolls[0].List;
+ ReplaceStringBuilders(result);
+ return result;
+ }
+
+ ///
+ /// Parsing helper method.
+ ///
+ /// Returns a ListItemPreamble
+ private static ListItemPreamble ParseItemPreamble(string markdown, int start, int maxEnd)
+ {
+ // There are two types of lists.
+ // A numbered list starts with a number, then a period ('.'), then a space.
+ // A bulleted list starts with a star ('*'), dash ('-') or plus ('+'), then a period, then a space.
+ ListStyle style;
+ if (markdown[start] == '*' || markdown[start] == '-' || markdown[start] == '+')
+ {
+ style = ListStyle.Bulleted;
+ start++;
+ }
+ else if (markdown[start] >= '0' && markdown[start] <= '9')
+ {
+ style = ListStyle.Numbered;
+ start++;
+
+ // Skip any other digits.
+ while (start < maxEnd)
+ {
+ char c = markdown[start];
+ if (c < '0' || c > '9')
+ {
+ break;
+ }
+
+ start++;
+ }
+
+ // Next should be a period ('.').
+ if (start == maxEnd || markdown[start] != '.')
+ {
+ return null;
+ }
+
+ start++;
+ }
+ else
+ {
+ return null;
+ }
+
+ // Next should be a space.
+ if (start == maxEnd || (markdown[start] != ' ' && markdown[start] != '\t'))
+ {
+ return null;
+ }
+
+ start++;
+
+ // This is a valid list item.
+ return new ListItemPreamble { Style = style, ContentStartPos = start };
+ }
+
+ ///
+ /// Parsing helper method.
+ ///
+ private static void AppendTextToListItem(ListItemBlock listItem, string markdown, int start, int end, bool newLine = false)
+ {
+ ListItemBuilder listItemBuilder = null;
+ if (listItem.Blocks.Count > 0)
+ {
+ listItemBuilder = listItem.Blocks[listItem.Blocks.Count - 1] as ListItemBuilder;
+ }
+
+ if (listItemBuilder == null)
+ {
+ // Add a new block.
+ listItemBuilder = new ListItemBuilder();
+ listItem.Blocks.Add(listItemBuilder);
+ }
+
+ var builder = listItemBuilder.Builder;
+ if (builder.Length >= 2 &&
+ ParseHelpers.IsMarkdownWhiteSpace(builder[builder.Length - 2]) &&
+ ParseHelpers.IsMarkdownWhiteSpace(builder[builder.Length - 1]))
+ {
+ builder.Length -= 2;
+ builder.AppendLine();
+ }
+ else if (builder.Length > 0)
+ {
+ builder.Append(' ');
+ }
+
+ if (newLine)
+ {
+ builder.Append(Environment.NewLine);
+ }
+
+ builder.Append(markdown.Substring(start, end - start));
+ }
+
+ ///
+ /// Parsing helper.
+ ///
+ /// true if any of the list items were parsed using the block parser.
+ private static bool ReplaceStringBuilders(ListBlock list)
+ {
+ bool usedBlockParser = false;
+ foreach (var listItem in list.Items)
+ {
+ // Use the inline parser if there is one paragraph, use the block parser otherwise.
+ var useBlockParser = listItem.Blocks.Count(block => block.Type == MarkdownBlockType.ListItemBuilder) > 1;
+
+ // Recursively replace any child lists.
+ foreach (var block in listItem.Blocks)
+ {
+ if (block is ListBlock listBlock && ReplaceStringBuilders(listBlock))
+ {
+ useBlockParser = true;
+ }
+ }
+
+ // Parse the text content of the list items.
+ var newBlockList = new List();
+ foreach (var block in listItem.Blocks)
+ {
+ if (block is ListItemBuilder)
+ {
+ var blockText = ((ListItemBuilder)block).Builder.ToString();
+ if (useBlockParser)
+ {
+ // Parse the list item as a series of blocks.
+ newBlockList.AddRange(MarkdownDocument.Parse(blockText, 0, blockText.Length, quoteDepth: 0, actualEnd: out var actualEnd));
+ usedBlockParser = true;
+ }
+ else
+ {
+ // Don't allow blocks.
+ var paragraph = new ParagraphBlock
+ {
+ Inlines = Common.ParseInlineChildren(blockText, 0, blockText.Length)
+ };
+ newBlockList.Add(paragraph);
+ }
+ }
+ else
+ {
+ newBlockList.Add(block);
+ }
+ }
+
+ listItem.Blocks = newBlockList;
+ }
+
+ return usedBlockParser;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Items == null)
+ {
+ return base.ToString();
+ }
+
+ var result = new StringBuilder();
+ for (int i = 0; i < Items.Count; i++)
+ {
+ if (result.Length > 0)
+ {
+ result.AppendLine();
+ }
+
+ switch (Style)
+ {
+ case ListStyle.Bulleted:
+ result.Append("* ");
+ break;
+
+ case ListStyle.Numbered:
+ result.Append(i + 1);
+ result.Append(".");
+ break;
+ }
+
+ result.Append(" ");
+ result.Append(string.Join("\r\n", Items[i].Blocks));
+ }
+
+ return result.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ParagraphBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ParagraphBlock.cs
new file mode 100644
index 0000000..310c846
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/ParagraphBlock.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a block of text that is displayed as a single paragraph.
+ ///
+ public class ParagraphBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ParagraphBlock()
+ : base(MarkdownBlockType.Paragraph)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the block.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Parses paragraph text.
+ ///
+ /// The markdown text.
+ /// A parsed paragraph.
+ internal static ParagraphBlock Parse(string markdown)
+ {
+ return new ParagraphBlock { Inlines = Common.ParseInlineChildren(markdown, 0, markdown.Length) };
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return string.Join(string.Empty, Inlines);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/QuoteBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/QuoteBlock.cs
new file mode 100644
index 0000000..c541fbf
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/QuoteBlock.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a block that is displayed using a quote style. Quotes are used to indicate
+ /// that the text originated elsewhere (e.g. a previous comment).
+ ///
+ public class QuoteBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public QuoteBlock()
+ : base(MarkdownBlockType.Quote)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the block.
+ ///
+ public IList Blocks { get; set; }
+
+ ///
+ /// Parses a quote block.
+ ///
+ /// The markdown text.
+ /// The location of the start of the line.
+ /// The location to stop parsing.
+ /// The current nesting level of quotes.
+ /// Set to the end of the block when the return value is non-null.
+ /// A parsed quote block.
+ internal static QuoteBlock Parse(string markdown, int startOfLine, int maxEnd, int quoteDepth, out int actualEnd)
+ {
+ var result = new QuoteBlock
+ {
+ // Recursively call into the markdown block parser.
+ Blocks = MarkdownDocument.Parse(markdown, startOfLine, maxEnd, quoteDepth: quoteDepth + 1, actualEnd: out actualEnd)
+ };
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/TableBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/TableBlock.cs
new file mode 100644
index 0000000..ea73e13
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/TableBlock.cs
@@ -0,0 +1,329 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a block which contains tabular data.
+ ///
+ public class TableBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TableBlock()
+ : base(MarkdownBlockType.Table)
+ {
+ }
+
+ ///
+ /// Gets or sets the table rows.
+ ///
+ public IList Rows { get; set; }
+
+ ///
+ /// Gets or sets describes the columns in the table. Rows can have more or less cells than the number
+ /// of columns. Rows with fewer cells should be padded with empty cells. For rows with
+ /// more cells, the extra cells should be hidden.
+ ///
+ public IList ColumnDefinitions { get; set; }
+
+ ///
+ /// Parses a table block.
+ ///
+ /// The markdown text.
+ /// The location of the first character in the block.
+ /// The location of the end of the first line.
+ /// The location to stop parsing.
+ /// The current nesting level for block quoting.
+ /// Set to the end of the block when the return value is non-null.
+ /// A parsed table block, or null if this is not a table block.
+ internal static TableBlock Parse(string markdown, int start, int endOfFirstLine, int maxEnd, int quoteDepth, out int actualEnd)
+ {
+ // A table is a line of text, with at least one vertical bar (|), followed by a line of
+ // of text that consists of alternating dashes (-) and vertical bars (|) and optionally
+ // vertical bars at the start and end. The second line must have at least as many
+ // interior vertical bars as there are interior vertical bars on the first line.
+ actualEnd = start;
+
+ // First thing to do is to check if there is a vertical bar on the line.
+ var barSections = markdown.Substring(start, endOfFirstLine - start).Split('|');
+
+ var allBarsEscaped = true;
+
+ // we can skip the last section, because there is no bar at the end of it
+ for (var i = 0; i < barSections.Length - 1; i++)
+ {
+ var barSection = barSections[i];
+ if (!barSection.EndsWith("\\", StringComparison.Ordinal))
+ {
+ allBarsEscaped = false;
+ break;
+ }
+ }
+
+ if (allBarsEscaped)
+ {
+ return null;
+ }
+
+ var rows = new List();
+
+ // Parse the first row.
+ var firstRow = new TableRow();
+ start = firstRow.Parse(markdown, start, maxEnd, quoteDepth);
+ rows.Add(firstRow);
+
+ // Parse the contents of the second row.
+ var secondRowContents = new List();
+ start = TableRow.ParseContents(
+ markdown,
+ start,
+ maxEnd,
+ quoteDepth,
+ requireVerticalBar: false,
+ contentParser: (start2, end2) => secondRowContents.Add(markdown.Substring(start2, end2 - start2)));
+
+ // There must be at least as many columns in the second row as in the first row.
+ if (secondRowContents.Count < firstRow.Cells.Count)
+ {
+ return null;
+ }
+
+ // Check each column definition.
+ // Note: excess columns past firstRowColumnCount are ignored and can contain anything.
+ var columnDefinitions = new List(firstRow.Cells.Count);
+ for (int i = 0; i < firstRow.Cells.Count; i++)
+ {
+ var cellContent = secondRowContents[i];
+ if (cellContent.Length == 0)
+ {
+ return null;
+ }
+
+ // The first and last characters can be '-' or ':'.
+ if (cellContent[0] != ':' && cellContent[0] != '-')
+ {
+ return null;
+ }
+
+ if (cellContent[cellContent.Length - 1] != ':' && cellContent[cellContent.Length - 1] != '-')
+ {
+ return null;
+ }
+
+ // Every other character must be '-'.
+ for (int j = 1; j < cellContent.Length - 1; j++)
+ {
+ if (cellContent[j] != '-')
+ {
+ return null;
+ }
+ }
+
+ // Record the alignment.
+ var columnDefinition = new TableColumnDefinition();
+ if (cellContent.Length > 1 && cellContent[0] == ':' && cellContent[cellContent.Length - 1] == ':')
+ {
+ columnDefinition.Alignment = ColumnAlignment.Center;
+ }
+ else if (cellContent[0] == ':')
+ {
+ columnDefinition.Alignment = ColumnAlignment.Left;
+ }
+ else if (cellContent[cellContent.Length - 1] == ':')
+ {
+ columnDefinition.Alignment = ColumnAlignment.Right;
+ }
+
+ columnDefinitions.Add(columnDefinition);
+ }
+
+ // Parse additional rows.
+ while (start < maxEnd)
+ {
+ var row = new TableRow();
+ start = row.Parse(markdown, start, maxEnd, quoteDepth);
+ if (row.Cells.Count == 0)
+ {
+ break;
+ }
+
+ rows.Add(row);
+ }
+
+ actualEnd = start;
+ return new TableBlock { ColumnDefinitions = columnDefinitions, Rows = rows };
+ }
+
+ ///
+ /// Describes a column in the markdown table.
+ ///
+ public class TableColumnDefinition
+ {
+ ///
+ /// Gets or sets the alignment of content in a table column.
+ ///
+ public ColumnAlignment Alignment { get; set; }
+ }
+
+ ///
+ /// Represents a single row in the table.
+ ///
+ public class TableRow
+ {
+ ///
+ /// Gets or sets the table cells.
+ ///
+ public IList Cells { get; set; }
+
+ ///
+ /// Parses the contents of the row, ignoring whitespace at the beginning and end of each cell.
+ ///
+ /// The markdown text.
+ /// The position of the start of the row.
+ /// The maximum position of the end of the row
+ /// The current nesting level for block quoting.
+ /// Indicates whether the line must contain a vertical bar.
+ /// Called for each cell.
+ /// The position of the start of the next line.
+ internal static int ParseContents(string markdown, int startingPos, int maxEndingPos, int quoteDepth, bool requireVerticalBar, Action contentParser)
+ {
+ // Skip quote characters.
+ int pos = Common.SkipQuoteCharacters(markdown, startingPos, maxEndingPos, quoteDepth);
+
+ // If the line starts with a '|' character, skip it.
+ bool lineHasVerticalBar = false;
+ if (pos < maxEndingPos && markdown[pos] == '|')
+ {
+ lineHasVerticalBar = true;
+ pos++;
+ }
+
+ while (pos < maxEndingPos)
+ {
+ // Ignore any whitespace at the start of the cell (except for a newline character).
+ while (pos < maxEndingPos && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]) && markdown[pos] != '\n' && markdown[pos] != '\r')
+ {
+ pos++;
+ }
+
+ int startOfCellContent = pos;
+
+ // Find the end of the cell.
+ bool endOfLineFound = true;
+ while (pos < maxEndingPos)
+ {
+ char c = markdown[pos];
+ if (c == '|' && (pos == 0 || markdown[pos - 1] != '\\'))
+ {
+ lineHasVerticalBar = true;
+ endOfLineFound = false;
+ break;
+ }
+
+ if (c == '\n')
+ {
+ break;
+ }
+
+ if (c == '\r')
+ {
+ if (pos < maxEndingPos && markdown[pos + 1] == '\n')
+ {
+ pos++; // Swallow the complete linefeed.
+ }
+
+ break;
+ }
+
+ pos++;
+ }
+
+ int endOfCell = pos;
+
+ // If a vertical bar is required, and none was found, then exit early.
+ if (endOfLineFound && !lineHasVerticalBar && requireVerticalBar)
+ {
+ return startingPos;
+ }
+
+ // Ignore any whitespace at the end of the cell.
+ if (endOfCell > startOfCellContent)
+ {
+ while (ParseHelpers.IsMarkdownWhiteSpace(markdown[pos - 1]))
+ {
+ pos--;
+ }
+ }
+
+ int endOfCellContent = pos;
+
+ if (endOfLineFound == false || endOfCellContent > startOfCellContent)
+ {
+ // Parse the contents of the cell.
+ contentParser(startOfCellContent, endOfCellContent);
+ }
+
+ // End of input?
+ if (pos == maxEndingPos)
+ {
+ break;
+ }
+
+ // Move to the next cell, or the next line.
+ pos = endOfCell + 1;
+
+ // End of the line?
+ if (endOfLineFound)
+ {
+ break;
+ }
+ }
+
+ return pos;
+ }
+
+ ///
+ /// Called when this block type should parse out the goods. Given the markdown, a starting point, and a max ending point
+ /// the block should find the start of the block, find the end and parse out the middle. The end most of the time will not be
+ /// the max ending pos, but it sometimes can be. The function will return where it ended parsing the block in the markdown.
+ ///
+ /// the postiion parsed to
+ internal int Parse(string markdown, int startingPos, int maxEndingPos, int quoteDepth)
+ {
+ Cells = new List();
+ return ParseContents(
+ markdown,
+ startingPos,
+ maxEndingPos,
+ quoteDepth,
+ requireVerticalBar: true,
+ contentParser: (startingPos2, maxEndingPos2) =>
+ {
+ var cell = new TableCell
+ {
+ Inlines = Common.ParseInlineChildren(markdown, startingPos2, maxEndingPos2)
+ };
+ Cells.Add(cell);
+ });
+ }
+ }
+
+ ///
+ /// Represents a cell in the table.
+ ///
+ public class TableCell
+ {
+ ///
+ /// Gets or sets the cell contents.
+ ///
+ public IList Inlines { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/YamlHeaderBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/YamlHeaderBlock.cs
new file mode 100644
index 0000000..d4e9790
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Blocks/YamlHeaderBlock.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Yaml Header. use for blog.
+ /// e.g.
+ /// ---
+ /// title: something
+ /// tag: something
+ /// ---
+ ///
+ public class YamlHeaderBlock : MarkdownBlock
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public YamlHeaderBlock()
+ : base(MarkdownBlockType.YamlHeader)
+ {
+ }
+
+ ///
+ /// Gets or sets yaml header properties
+ ///
+ public Dictionary Children { get; set; }
+
+ ///
+ /// Parse yaml header
+ ///
+ /// The markdown text.
+ /// The location of the first hash character.
+ /// The location of the end of the line.
+ /// The location of the actual end of the aprse.
+ /// Parsed class
+ internal static YamlHeaderBlock Parse(string markdown, int start, int end, out int realEndIndex)
+ {
+ // As yaml header, must be start a line with "---"
+ // and end with a line "---"
+ realEndIndex = start;
+ int lineStart = start;
+ if (end - start < 3)
+ {
+ return null;
+ }
+
+ if (lineStart != 0 || markdown.Substring(start, 3) != "---")
+ {
+ return null;
+ }
+
+ int startUnderlineIndex = Common.FindNextSingleNewLine(markdown, lineStart, end, out int startOfNextLine);
+ if (startUnderlineIndex - lineStart != 3)
+ {
+ return null;
+ }
+
+ bool lockedFinalUnderline = false;
+
+ // if current line not contain the ": ", check it is end of parse, if not, exit
+ // if next line is the end, exit
+ int pos = startOfNextLine;
+ List elements = new List();
+ while (pos < end)
+ {
+ int nextUnderLineIndex = Common.FindNextSingleNewLine(markdown, pos, end, out startOfNextLine);
+ bool haveSeparator = markdown.Substring(pos, nextUnderLineIndex - pos).Contains(": ", StringComparison.Ordinal);
+ if (haveSeparator)
+ {
+ elements.Add(markdown.Substring(pos, nextUnderLineIndex - pos));
+ }
+ else if (end - pos >= 3 && markdown.Substring(pos, 3) == "---")
+ {
+ lockedFinalUnderline = true;
+ realEndIndex = pos + 3;
+ break;
+ }
+ else if (startOfNextLine == pos + 1)
+ {
+ pos = startOfNextLine;
+ continue;
+ }
+ else
+ {
+ return null;
+ }
+
+ pos = startOfNextLine;
+ }
+
+ // if not have the end, return
+ if (!lockedFinalUnderline)
+ {
+ return null;
+ }
+
+ // parse yaml header properties
+ if (elements.Count < 1)
+ {
+ return null;
+ }
+
+ var result = new YamlHeaderBlock
+ {
+ Children = new Dictionary()
+ };
+ foreach (var item in elements)
+ {
+ string[] splits = item.Split(new string[] { ": " }, StringSplitOptions.None);
+ if (splits.Length < 2)
+ {
+ continue;
+ }
+ else
+ {
+ string key = splits[0];
+ string value = splits[1];
+ if (key.Trim().Length == 0)
+ {
+ continue;
+ }
+
+ value = string.IsNullOrEmpty(value.Trim()) ? string.Empty : value;
+ result.Children.Add(key, value);
+ }
+ }
+
+ if (result.Children == null)
+ {
+ return null;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Children == null)
+ {
+ return base.ToString();
+ }
+ else
+ {
+ string result = string.Empty;
+ foreach (KeyValuePair item in Children)
+ {
+ result += item.Key + ": " + item.Value + "\n";
+ }
+
+ return result.TrimEnd('\n');
+ }
+ }
+ }
+}
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/IParser.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/IParser.cs
new file mode 100644
index 0000000..c438754
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/IParser.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/master/Microsoft.Toolkit.Parsers/Core
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Parser interface.
+ ///
+ /// Type to parse into.
+ public interface IParser
+ where T : SchemaBase
+ {
+ ///
+ /// Parse method which all classes must implement.
+ ///
+ /// Data to parse.
+ /// Strong typed parsed data.
+ IEnumerable Parse(string data);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/ParseHelpers.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/ParseHelpers.cs
new file mode 100644
index 0000000..5c9e63f
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/ParseHelpers.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/master/Microsoft.Toolkit.Parsers/Core
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// This class offers helpers for Parsing.
+ ///
+ public static class ParseHelpers
+ {
+ ///
+ /// Determines if a Markdown string is blank or comprised entirely of whitespace characters.
+ ///
+ /// true if blank or white space
+ public static bool IsMarkdownBlankOrWhiteSpace(string str)
+ {
+ for (int i = 0; i < str.Length; i++)
+ {
+ if (!IsMarkdownWhiteSpace(str[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Determines if a character is a Markdown whitespace character.
+ ///
+ /// true if is white space
+ public static bool IsMarkdownWhiteSpace(char c)
+ {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/SchemaBase.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/SchemaBase.cs
new file mode 100644
index 0000000..84a3baa
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/SchemaBase.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/master/Microsoft.Toolkit.Parsers/Core
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Strong typed schema base class.
+ ///
+ public abstract class SchemaBase
+ {
+ ///
+ /// Gets or sets identifier for strong typed record.
+ ///
+ public string InternalID { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/StringValueAttribute.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/StringValueAttribute.cs
new file mode 100644
index 0000000..766dfc0
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Core/StringValueAttribute.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/master/Microsoft.Toolkit.Parsers/Core
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+
+ ///
+ /// The StringValue attribute is used as a helper to decorate enum values with string representations.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public sealed class StringValueAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructor accepting string value.
+ ///
+ /// String value
+ public StringValueAttribute(string value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets property for string value.
+ ///
+ public string Value { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ColumnAlignment.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ColumnAlignment.cs
new file mode 100644
index 0000000..216a590
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ColumnAlignment.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// The alignment of content in a table column.
+ ///
+ public enum ColumnAlignment
+ {
+ ///
+ /// The alignment was not specified.
+ ///
+ Unspecified,
+
+ ///
+ /// Content should be left aligned.
+ ///
+ Left,
+
+ ///
+ /// Content should be right aligned.
+ ///
+ Right,
+
+ ///
+ /// Content should be centered.
+ ///
+ Center,
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/HyperlinkType.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/HyperlinkType.cs
new file mode 100644
index 0000000..de1ce31
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/HyperlinkType.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Specifies the type of Hyperlink that is used.
+ ///
+ public enum HyperlinkType
+ {
+ ///
+ /// A hyperlink surrounded by angle brackets (e.g. "http://www.reddit.com").
+ ///
+ BracketedUrl,
+
+ ///
+ /// A fully qualified hyperlink (e.g. "http://www.reddit.com").
+ ///
+ FullUrl,
+
+ ///
+ /// A URL without a scheme (e.g. "www.reddit.com").
+ ///
+ PartialUrl,
+
+ ///
+ /// An email address (e.g. "test@reddit.com").
+ ///
+ Email,
+
+ ///
+ /// A subreddit link (e.g. "/r/news").
+ ///
+ Subreddit,
+
+ ///
+ /// A user link (e.g. "/u/quinbd").
+ ///
+ User,
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/InlineParseMethod.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/InlineParseMethod.cs
new file mode 100644
index 0000000..d3c7fd8
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/InlineParseMethod.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ internal enum InlineParseMethod
+ {
+ ///
+ /// A Comment text
+ ///
+ Comment,
+
+ ///
+ /// A Link Reference
+ ///
+ LinkReference,
+
+ ///
+ /// A bold element
+ ///
+ Bold,
+
+ ///
+ /// An bold and italic block
+ ///
+ BoldItalic,
+
+ ///
+ /// A code element
+ ///
+ Code,
+
+ ///
+ /// An italic block
+ ///
+ Italic,
+
+ ///
+ /// A link block
+ ///
+ MarkdownLink,
+
+ ///
+ /// An angle bracket link.
+ ///
+ AngleBracketLink,
+
+ ///
+ /// A url block
+ ///
+ Url,
+
+ ///
+ /// A reddit style link
+ ///
+ RedditLink,
+
+ ///
+ /// An in line text link
+ ///
+ PartialLink,
+
+ ///
+ /// An email element
+ ///
+ Email,
+
+ ///
+ /// strike through element
+ ///
+ Strikethrough,
+
+ ///
+ /// Super script element.
+ ///
+ Superscript,
+
+ ///
+ /// Sub script element.
+ ///
+ Subscript,
+
+ ///
+ /// Image element.
+ ///
+ Image,
+
+ ///
+ /// Emoji element.
+ ///
+ Emoji
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ListStyle.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ListStyle.cs
new file mode 100644
index 0000000..9bbaf11
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/ListStyle.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// This specifies the type of style the List will be.
+ ///
+ public enum ListStyle
+ {
+ ///
+ /// A list with bullets
+ ///
+ Bulleted,
+
+ ///
+ /// A numbered list
+ ///
+ Numbered,
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownBlockType.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownBlockType.cs
new file mode 100644
index 0000000..439fa93
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownBlockType.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Determines the type of Block the Block element is.
+ ///
+ public enum MarkdownBlockType
+ {
+ ///
+ /// The root element
+ ///
+ Root,
+
+ ///
+ /// A paragraph element.
+ ///
+ Paragraph,
+
+ ///
+ /// A quote block
+ ///
+ Quote,
+
+ ///
+ /// A code block
+ ///
+ Code,
+
+ ///
+ /// A header block
+ ///
+ Header,
+
+ ///
+ /// A list block
+ ///
+ List,
+
+ ///
+ /// A list item block
+ ///
+ ListItemBuilder,
+
+ ///
+ /// a horizontal rule block
+ ///
+ HorizontalRule,
+
+ ///
+ /// A table block
+ ///
+ Table,
+
+ ///
+ /// A link block
+ ///
+ LinkReference,
+
+ ///
+ /// A Yaml header block
+ ///
+ YamlHeader
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownInlineType.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownInlineType.cs
new file mode 100644
index 0000000..ee2427e
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Enums/MarkdownInlineType.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Enums
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Determines the type of Inline the Inline Element is.
+ ///
+ public enum MarkdownInlineType
+ {
+ ///
+ /// A comment
+ ///
+ Comment,
+
+ ///
+ /// A text run
+ ///
+ TextRun,
+
+ ///
+ /// A bold run
+ ///
+ Bold,
+
+ ///
+ /// An italic run
+ ///
+ Italic,
+
+ ///
+ /// A link in markdown syntax
+ ///
+ MarkdownLink,
+
+ ///
+ /// A raw hyper link
+ ///
+ RawHyperlink,
+
+ ///
+ /// A raw subreddit link
+ ///
+ RawSubreddit,
+
+ ///
+ /// A strike through run
+ ///
+ Strikethrough,
+
+ ///
+ /// A superscript run
+ ///
+ Superscript,
+
+ ///
+ /// A subscript run
+ ///
+ Subscript,
+
+ ///
+ /// A code run
+ ///
+ Code,
+
+ ///
+ /// An image
+ ///
+ Image,
+
+ ///
+ /// Emoji
+ ///
+ Emoji,
+
+ ///
+ /// Link Reference
+ ///
+ LinkReference
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/Common.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/Common.cs
new file mode 100644
index 0000000..04afb13
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/Common.cs
@@ -0,0 +1,535 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Helpers
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Helpers for Markdown.
+ ///
+ internal class Common
+ {
+ private static readonly List _triggerList = new List();
+ private static readonly char[] _tripCharacters;
+
+ static Common()
+ {
+ BoldItalicTextInline.AddTripChars(_triggerList);
+ BoldTextInline.AddTripChars(_triggerList);
+ ItalicTextInline.AddTripChars(_triggerList);
+ MarkdownLinkInline.AddTripChars(_triggerList);
+ HyperlinkInline.AddTripChars(_triggerList);
+ CommentInline.AddTripChars(_triggerList);
+ StrikethroughTextInline.AddTripChars(_triggerList);
+ SuperscriptTextInline.AddTripChars(_triggerList);
+ SubscriptTextInline.AddTripChars(_triggerList);
+ CodeInline.AddTripChars(_triggerList);
+ ImageInline.AddTripChars(_triggerList);
+ EmojiInline.AddTripChars(_triggerList);
+ LinkAnchorInline.AddTripChars(_triggerList);
+
+ // Create an array of characters to search against using IndexOfAny.
+ _tripCharacters = _triggerList.Select(trigger => trigger.FirstChar).Distinct().ToArray();
+ }
+
+ ///
+ /// This function can be called by any element parsing. Given a start and stopping point this will
+ /// parse all found elements out of the range.
+ ///
+ /// A list of parsed inlines.
+ public static List ParseInlineChildren(string markdown, int startingPos, int maxEndingPos, bool ignoreLinks = false)
+ {
+ int currentParsePosition = startingPos;
+
+ var inlines = new List();
+ while (currentParsePosition < maxEndingPos)
+ {
+ // Find the next inline element.
+ var parseResult = FindNextInlineElement(markdown, currentParsePosition, maxEndingPos, ignoreLinks);
+
+ // If the element we found doesn't start at the position we are looking for there
+ // is text between the element and the start of the parsed element. We need to wrap
+ // it into a text run.
+ if (parseResult.Start != currentParsePosition)
+ {
+ var textRun = TextRunInline.Parse(markdown, currentParsePosition, parseResult.Start);
+ inlines.Add(textRun);
+ }
+
+ // Add the parsed element.
+ inlines.Add(parseResult.ParsedElement);
+
+ // Update the current position.
+ currentParsePosition = parseResult.End;
+ }
+
+ return inlines;
+ }
+
+ ///
+ /// Finds the next inline element by matching trip chars and verifying the match.
+ ///
+ /// The markdown text to parse.
+ /// The position to start parsing.
+ /// The position to stop parsing.
+ /// Indicates whether to parse links.
+ /// Returns the next element
+ private static InlineParseResult FindNextInlineElement(string markdown, int start, int end, bool ignoreLinks)
+ {
+ // Search for the next inline sequence.
+ for (int pos = start; pos < end; pos++)
+ {
+ // IndexOfAny should be the fastest way to skip characters we don't care about.
+ pos = markdown.IndexOfAny(_tripCharacters, pos, end - pos);
+ if (pos < 0)
+ {
+ break;
+ }
+
+ // Find the trigger(s) that matched.
+ char currentChar = markdown[pos];
+ foreach (InlineTripCharHelper currentTripChar in _triggerList)
+ {
+ // Check if our current char matches the suffix char.
+ if (currentChar == currentTripChar.FirstChar)
+ {
+ // Don't match if the previous character was a backslash.
+ if (pos > start && markdown[pos - 1] == '\\')
+ {
+ continue;
+ }
+
+ // If we are here we have a possible match. Call into the inline class to verify.
+ InlineParseResult parseResult = null;
+ switch (currentTripChar.Method)
+ {
+ case InlineParseMethod.BoldItalic:
+ parseResult = BoldItalicTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Comment:
+ parseResult = CommentInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.LinkReference:
+ parseResult = LinkAnchorInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Bold:
+ parseResult = BoldTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Italic:
+ parseResult = ItalicTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.MarkdownLink:
+ if (!ignoreLinks)
+ {
+ parseResult = MarkdownLinkInline.Parse(markdown, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.AngleBracketLink:
+ if (!ignoreLinks)
+ {
+ parseResult = HyperlinkInline.ParseAngleBracketLink(markdown, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.Url:
+ if (!ignoreLinks)
+ {
+ parseResult = HyperlinkInline.ParseUrl(markdown, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.RedditLink:
+ if (!ignoreLinks)
+ {
+ parseResult = HyperlinkInline.ParseRedditLink(markdown, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.PartialLink:
+ if (!ignoreLinks)
+ {
+ parseResult = HyperlinkInline.ParsePartialLink(markdown, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.Email:
+ if (!ignoreLinks)
+ {
+ parseResult = HyperlinkInline.ParseEmailAddress(markdown, start, pos, end);
+ }
+
+ break;
+
+ case InlineParseMethod.Strikethrough:
+ parseResult = StrikethroughTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Superscript:
+ parseResult = SuperscriptTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Subscript:
+ parseResult = SubscriptTextInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Code:
+ parseResult = CodeInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Image:
+ parseResult = ImageInline.Parse(markdown, pos, end);
+ break;
+
+ case InlineParseMethod.Emoji:
+ parseResult = EmojiInline.Parse(markdown, pos, end);
+ break;
+ }
+
+ if (parseResult != null)
+ {
+ return parseResult;
+ }
+ }
+ }
+ }
+
+ // If we didn't find any elements we have a normal text block.
+ // Let us consume the entire range.
+ return new InlineParseResult(TextRunInline.Parse(markdown, start, end), start, end);
+ }
+
+ ///
+ /// Returns the next \n or \r\n in the markdown.
+ ///
+ /// the next single line
+ public static int FindNextSingleNewLine(string markdown, int startingPos, int endingPos, out int startOfNextLine)
+ {
+ // A line can end with CRLF (\r\n) or just LF (\n).
+ int lineFeedPos = markdown.IndexOf('\n', startingPos);
+ if (lineFeedPos == -1)
+ {
+ // Trying with /r now
+ lineFeedPos = markdown.IndexOf('\r', startingPos);
+ if (lineFeedPos == -1)
+ {
+ startOfNextLine = endingPos;
+ return endingPos;
+ }
+ }
+
+ startOfNextLine = lineFeedPos + 1;
+
+ // Check if it was a CRLF.
+ if (lineFeedPos > startingPos && markdown[lineFeedPos - 1] == '\r')
+ {
+ return lineFeedPos - 1;
+ }
+
+ return lineFeedPos;
+ }
+
+ ///
+ /// Helper function for index of with a start and an ending.
+ ///
+ /// Pos of the searched for item
+ public static int IndexOf(string markdown, string search, int startingPos, int endingPos, bool reverseSearch = false)
+ {
+ // Check the ending isn't out of bounds.
+ if (endingPos > markdown.Length)
+ {
+ endingPos = markdown.Length;
+ DebuggingReporter.ReportCriticalError("IndexOf endingPos > string length");
+ }
+
+ // Figure out how long to go
+ int count = endingPos - startingPos;
+ if (count < 0)
+ {
+ return -1;
+ }
+
+ // Make sure we don't go too far.
+ int remainingCount = markdown.Length - startingPos;
+ if (count > remainingCount)
+ {
+ DebuggingReporter.ReportCriticalError("IndexOf count > remaing count");
+ count = remainingCount;
+ }
+
+ // Check the ending. Since we use inclusive ranges we need to -1 from this for
+ // reverses searches.
+ if (reverseSearch && endingPos > 0)
+ {
+ endingPos -= 1;
+ }
+
+ return reverseSearch ? markdown.LastIndexOf(search, endingPos, count, StringComparison.OrdinalIgnoreCase) : markdown.IndexOf(search, startingPos, count, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Helper function for index of with a start and an ending.
+ ///
+ /// Pos of the searched for item
+ public static int IndexOf(string markdown, char search, int startingPos, int endingPos, bool reverseSearch = false)
+ {
+ // Check the ending isn't out of bounds.
+ if (endingPos > markdown.Length)
+ {
+ endingPos = markdown.Length;
+ DebuggingReporter.ReportCriticalError("IndexOf endingPos > string length");
+ }
+
+ // Figure out how long to go
+ int count = endingPos - startingPos;
+ if (count < 0)
+ {
+ return -1;
+ }
+
+ // Make sure we don't go too far.
+ int remainingCount = markdown.Length - startingPos;
+ if (count > remainingCount)
+ {
+ DebuggingReporter.ReportCriticalError("IndexOf count > remaing count");
+ count = remainingCount;
+ }
+
+ // Check the ending. Since we use inclusive ranges we need to -1 from this for
+ // reverses searches.
+ if (reverseSearch && endingPos > 0)
+ {
+ endingPos -= 1;
+ }
+
+ return reverseSearch ? markdown.LastIndexOf(search, endingPos, count) : markdown.IndexOf(search, startingPos, count);
+ }
+
+ ///
+ /// Finds the next whitespace in a range.
+ ///
+ /// pos of the white space
+ public static int FindNextWhiteSpace(string markdown, int startingPos, int endingPos, bool ifNotFoundReturnLength)
+ {
+ int currentPos = startingPos;
+ while (currentPos < markdown.Length && currentPos < endingPos)
+ {
+ if (char.IsWhiteSpace(markdown[currentPos]))
+ {
+ return currentPos;
+ }
+
+ currentPos++;
+ }
+
+ return ifNotFoundReturnLength ? endingPos : -1;
+ }
+
+ ///
+ /// Parses lines.
+ ///
+ /// LineInfo
+ public static IEnumerable ParseLines(string markdown, int start, int end, int quoteDepth)
+ {
+ int pos = start;
+ bool lineStartsNewParagraph = true;
+
+ while (pos < end)
+ {
+ int startOfLine = pos;
+ int expectedQuotesRemaining = quoteDepth;
+ int nonSpacePos = pos;
+ char nonSpaceChar = '\0';
+ while (true)
+ {
+ // Find the next non-space char.
+ while (nonSpacePos < end)
+ {
+ char c = markdown[nonSpacePos];
+ if (c == '\r' || c == '\n')
+ {
+ // The line is either entirely whitespace, or is empty.
+ break;
+ }
+
+ if (c != ' ' && c != '\t')
+ {
+ // The line has content.
+ nonSpaceChar = c;
+ break;
+ }
+
+ nonSpacePos++;
+ }
+
+ // When parsing blocks in a blockquote context, we need to count the number of
+ // quote characters ('>'). If there are less than expected AND this is the
+ // start of a new paragraph, then stop parsing.
+ if (expectedQuotesRemaining == 0)
+ {
+ break;
+ }
+
+ if (nonSpaceChar == '>')
+ {
+ // Expected block quote characters should be ignored.
+ expectedQuotesRemaining--;
+ nonSpacePos++;
+ nonSpaceChar = '\0';
+ startOfLine = nonSpacePos;
+
+ // Ignore the first space after the quote character, if there is one.
+ if (startOfLine < end && markdown[startOfLine] == ' ')
+ {
+ startOfLine++;
+ nonSpacePos++;
+ }
+ }
+ else
+ {
+ // There were less block quote characters than expected.
+ // But it doesn't matter if this is not the start of a new paragraph.
+ if (!lineStartsNewParagraph || nonSpaceChar == '\0')
+ {
+ break;
+ }
+
+ // This must be the end of the blockquote. End the current paragraph, if any.
+ yield break;
+ }
+ }
+
+ // Find the end of the current line.
+ int endOfLine = FindNextSingleNewLine(markdown, nonSpacePos, end, out int startOfNextLine);
+
+ // Return the line info to the caller.
+ yield return new LineInfo
+ {
+ StartOfLine = startOfLine,
+ FirstNonWhitespaceChar = nonSpacePos,
+ EndOfLine = endOfLine,
+ StartOfNextLine = startOfNextLine,
+ };
+
+ if (nonSpaceChar == '\0')
+ {
+ // The line is empty or nothing but whitespace.
+ lineStartsNewParagraph = true;
+ }
+
+ // Repeat.
+ pos = startOfNextLine;
+ }
+ }
+
+ ///
+ /// Skips a certain number of quote characters (>).
+ ///
+ /// Skip Quote Chars
+ public static int SkipQuoteCharacters(string markdown, int start, int end, int quoteDepth)
+ {
+ if (quoteDepth == 0)
+ {
+ return start;
+ }
+
+ int startOfLine = start;
+ int nonSpacePos = start;
+ char nonSpaceChar = '\0';
+
+ while (true)
+ {
+ // Find the next non-space char.
+ while (nonSpacePos < end)
+ {
+ char c = markdown[nonSpacePos];
+ if (c == '\r' || c == '\n')
+ {
+ // The line is either entirely whitespace, or is empty.
+ break;
+ }
+
+ if (c != ' ' && c != '\t')
+ {
+ // The line has content.
+ nonSpaceChar = c;
+ break;
+ }
+
+ nonSpacePos++;
+ }
+
+ // When parsing blocks in a blockquote context, we need to count the number of
+ // quote characters ('>'). If there are less than expected AND this is the
+ // start of a new paragraph, then stop parsing.
+ if (quoteDepth == 0)
+ {
+ break;
+ }
+
+ if (nonSpaceChar == '>')
+ {
+ // Expected block quote characters should be ignored.
+ quoteDepth--;
+ nonSpacePos++;
+ nonSpaceChar = '\0';
+ startOfLine = nonSpacePos;
+
+ // Ignore the first space after the quote character, if there is one.
+ if (startOfLine < end && markdown[startOfLine] == ' ')
+ {
+ startOfLine++;
+ nonSpacePos++;
+ }
+ }
+ else
+ {
+ // There were less block quote characters than expected.
+ break;
+ }
+ }
+
+ return startOfLine;
+ }
+
+ ///
+ /// Checks if the given URL is allowed in a markdown link.
+ ///
+ /// The URL to check.
+ /// true if the URL is valid; false otherwise.
+ public static bool IsUrlValid(string url)
+ {
+ // URLs can be relative.
+ if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result))
+ {
+ return true;
+ }
+
+ // Check the scheme is allowed.
+ foreach (var scheme in MarkdownDocument.KnownSchemes)
+ {
+ if (result.Scheme.Equals(scheme, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/DebuggingReporter.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/DebuggingReporter.cs
new file mode 100644
index 0000000..6717174
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/DebuggingReporter.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Helpers
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Diagnostics;
+
+ ///
+ /// Reports an error during debugging.
+ ///
+ internal class DebuggingReporter
+ {
+ ///
+ /// Reports a critical error.
+ ///
+ public static void ReportCriticalError(string errorText)
+ {
+ Debug.WriteLine(errorText);
+ if (Debugger.IsAttached)
+ {
+ Debugger.Break();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineParseResult.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineParseResult.cs
new file mode 100644
index 0000000..1a799dd
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineParseResult.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Helpers
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Represents the result of parsing an inline element.
+ ///
+ internal class InlineParseResult
+ {
+ public InlineParseResult(MarkdownInline parsedElement, int start, int end)
+ {
+ ParsedElement = parsedElement;
+ Start = start;
+ End = end;
+ }
+
+ ///
+ /// Gets the element that was parsed (can be null ).
+ ///
+ public MarkdownInline ParsedElement { get; }
+
+ ///
+ /// Gets the position of the first character in the parsed element.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Gets the position of the character after the last character in the parsed element.
+ ///
+ public int End { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineTripCharHelper.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineTripCharHelper.cs
new file mode 100644
index 0000000..3aff1f7
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/InlineTripCharHelper.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Helpers
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// A helper class for the trip chars. This is an optimization. If we ask each class to go
+ /// through the rage and look for itself we end up looping through the range n times, once
+ /// for each inline. This class represent a character that an inline needs to have a
+ /// possible match. We will go through the range once and look for everyone's trip chars,
+ /// and if they can make a match from the trip char then we will commit to them.
+ ///
+ internal class InlineTripCharHelper
+ {
+ // Note! Everything in first char and suffix should be lower case!
+ public char FirstChar { get; set; }
+
+ public InlineParseMethod Method { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/LineInfo.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/LineInfo.cs
new file mode 100644
index 0000000..914250b
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Helpers/LineInfo.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Helpers
+
+namespace Notepads.Controls.Markdown
+{
+ internal class LineInfo
+ {
+ public int StartOfLine { get; set; }
+
+ public int FirstNonWhitespaceChar { get; set; }
+
+ public int EndOfLine { get; set; }
+
+ public bool IsLineBlank => FirstNonWhitespaceChar == EndOfLine;
+
+ public int StartOfNextLine { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldItalicTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldItalicTextInline.cs
new file mode 100644
index 0000000..2937346
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldItalicTextInline.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing bold italic text.
+ ///
+ internal class BoldItalicTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BoldItalicTextInline()
+ : base(MarkdownInlineType.Bold)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '*', Method = InlineParseMethod.BoldItalic });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '_', Method = InlineParseMethod.BoldItalic });
+ }
+
+ ///
+ /// Attempts to parse a bold text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed bold text span, or null if this is not a bold text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ if (start >= maxEnd - 1)
+ {
+ return null;
+ }
+
+ if (markdown == null || markdown.Length < 6 || start + 6 >= maxEnd)
+ {
+ return null;
+ }
+
+ // Check the start sequence.
+ string startSequence = markdown.Substring(start, 3);
+ if (startSequence != "***" && startSequence != "___")
+ {
+ return null;
+ }
+
+ // Find the end of the span. The end sequence (either '***' or '___') must be the same
+ // as the start sequence.
+ var innerStart = start + 3;
+ int innerEnd = Common.IndexOf(markdown, startSequence, innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // The first character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]))
+ {
+ return null;
+ }
+
+ // The last character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ var bold = new BoldTextInline
+ {
+ Inlines = new List
+ {
+ new ItalicTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ }
+ }
+ };
+ return new InlineParseResult(bold, start, innerEnd + 3);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "***" + string.Join(string.Empty, Inlines) + "***";
+ }
+ }
+}
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldTextInline.cs
new file mode 100644
index 0000000..314d6d2
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/BoldTextInline.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span that contains bold text.
+ ///
+ public class BoldTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BoldTextInline()
+ : base(MarkdownInlineType.Bold)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '*', Method = InlineParseMethod.Bold });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '_', Method = InlineParseMethod.Bold });
+ }
+
+ ///
+ /// Attempts to parse a bold text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed bold text span, or null if this is not a bold text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ if (start >= maxEnd - 1)
+ {
+ return null;
+ }
+
+ // Check the start sequence.
+ string startSequence = markdown.Substring(start, 2);
+ if (startSequence != "**" && startSequence != "__")
+ {
+ return null;
+ }
+
+ // Find the end of the span. The end sequence (either '**' or '__') must be the same
+ // as the start sequence.
+ var innerStart = start + 2;
+ int innerEnd = Common.IndexOf(markdown, startSequence, innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // The first character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]))
+ {
+ return null;
+ }
+
+ // The last character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new BoldTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, innerEnd + 2);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "**" + string.Join(string.Empty, Inlines) + "**";
+ }
+ }
+}
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CodeInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CodeInline.cs
new file mode 100644
index 0000000..c5b8ffa
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CodeInline.cs
@@ -0,0 +1,112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing code, or other text that is to be displayed using a
+ /// fixed-width font.
+ ///
+ public class CodeInline : MarkdownInline, IInlineLeaf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CodeInline()
+ : base(MarkdownInlineType.Code)
+ {
+ }
+
+ ///
+ /// Gets or sets the text to display as code.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '`', Method = InlineParseMethod.Code });
+ }
+
+ ///
+ /// Attempts to parse an inline code span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed inline code span, or null if this is not an inline code span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Check the first char.
+ if (start == maxEnd || markdown[start] != '`')
+ {
+ return null;
+ }
+
+ // There is an alternate syntax that starts and ends with two backticks.
+ // e.g. ``sdf`sdf`` would be "sdf`sdf".
+ int innerStart = start + 1;
+ int innerEnd, end;
+ if (innerStart < maxEnd && markdown[innerStart] == '`')
+ {
+ // Alternate double back-tick syntax.
+ innerStart++;
+
+ // Find the end of the span.
+ innerEnd = Common.IndexOf(markdown, "``", innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ end = innerEnd + 2;
+ }
+ else
+ {
+ // Standard single backtick syntax.
+
+ // Find the end of the span.
+ innerEnd = Common.IndexOf(markdown, '`', innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ end = innerEnd + 1;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new CodeInline
+ {
+ Text = markdown.Substring(innerStart, innerEnd - innerStart).Trim(' ', '\t', '\r', '\n')
+ };
+ return new InlineParseResult(result, start, end);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Text == null)
+ {
+ return base.ToString();
+ }
+
+ return "`" + Text + "`";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CommentInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CommentInline.cs
new file mode 100644
index 0000000..c2cb6b3
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/CommentInline.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span that contains comment.
+ ///
+ internal class CommentInline : MarkdownInline, IInlineLeaf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommentInline()
+ : base(MarkdownInlineType.Comment)
+ {
+ }
+
+ ///
+ /// Gets or sets the Content of the Comment.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '<', Method = InlineParseMethod.Comment });
+ }
+
+ ///
+ /// Attempts to parse a comment span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed bold text span, or null if this is not a bold text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ if (start >= maxEnd - 1)
+ {
+ return null;
+ }
+
+ string startSequence = markdown.Substring(start);
+ if (!startSequence.StartsWith("')
+ var innerStart = start + 4;
+ int innerEnd = Common.IndexOf(markdown, "-->", innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ var length = innerEnd - innerStart;
+ var contents = markdown.Substring(innerStart, length);
+
+ var result = new CommentInline
+ {
+ Text = contents
+ };
+
+ return new InlineParseResult(result, start, innerEnd + 3);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ return "";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.EmojiCodes.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.EmojiCodes.cs
new file mode 100644
index 0000000..40d08da
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.EmojiCodes.cs
@@ -0,0 +1,846 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing emoji symbol.
+ ///
+ public partial class EmojiInline
+ {
+ // Codes taken from https://gist.github.com/rxaviers/7360908
+ // Ignoring not implented symbols in Segoe UI Emoji font (e.g. :bowtie:)
+ private static readonly Dictionary _emojiCodesDictionary = new Dictionary
+ {
+ { "smile", 0x1f604 },
+ { "laughing", 0x1f606 },
+ { "blush", 0x1f60a },
+ { "smiley", 0x1f603 },
+ { "relaxed", 0x263a },
+ { "smirk", 0x1f60f },
+ { "heart_eyes", 0x1f60d },
+ { "kissing_heart", 0x1f618 },
+ { "kissing_closed_eyes", 0x1f61a },
+ { "flushed", 0x1f633 },
+ { "relieved", 0x1f60c },
+ { "satisfied", 0x1f606 },
+ { "grin", 0x1f601 },
+ { "wink", 0x1f609 },
+ { "stuck_out_tongue_winking_eye", 0x1f61c },
+ { "stuck_out_tongue_closed_eyes", 0x1f61d },
+ { "grinning", 0x1f600 },
+ { "kissing", 0x1f617 },
+ { "kissing_smiling_eyes", 0x1f619 },
+ { "stuck_out_tongue", 0x1f61b },
+ { "sleeping", 0x1f634 },
+ { "worried", 0x1f61f },
+ { "frowning", 0x1f626 },
+ { "anguished", 0x1f627 },
+ { "open_mouth", 0x1f62e },
+ { "grimacing", 0x1f62c },
+ { "confused", 0x1f615 },
+ { "hushed", 0x1f62f },
+ { "expressionless", 0x1f611 },
+ { "unamused", 0x1f612 },
+ { "sweat_smile", 0x1f605 },
+ { "sweat", 0x1f613 },
+ { "disappointed_relieved", 0x1f625 },
+ { "weary", 0x1f629 },
+ { "pensive", 0x1f614 },
+ { "disappointed", 0x1f61e },
+ { "confounded", 0x1f616 },
+ { "fearful", 0x1f628 },
+ { "cold_sweat", 0x1f630 },
+ { "persevere", 0x1f623 },
+ { "cry", 0x1f622 },
+ { "sob", 0x1f62d },
+ { "joy", 0x1f602 },
+ { "astonished", 0x1f632 },
+ { "scream", 0x1f631 },
+ { "tired_face", 0x1f62b },
+ { "angry", 0x1f620 },
+ { "rage", 0x1f621 },
+ { "triumph", 0x1f624 },
+ { "sleepy", 0x1f62a },
+ { "yum", 0x1f60b },
+ { "mask", 0x1f637 },
+ { "sunglasses", 0x1f60e },
+ { "dizzy_face", 0x1f635 },
+ { "imp", 0x1f47f },
+ { "smiling_imp", 0x1f608 },
+ { "neutral_face", 0x1f610 },
+ { "no_mouth", 0x1f636 },
+ { "innocent", 0x1f607 },
+ { "alien", 0x1f47d },
+ { "yellow_heart", 0x1f49b },
+ { "blue_heart", 0x1f499 },
+ { "purple_heart", 0x1f49c },
+ { "heart", 0x2764 },
+ { "green_heart", 0x1f49a },
+ { "broken_heart", 0x1f494 },
+ { "heartbeat", 0x1f493 },
+ { "heartpulse", 0x1f497 },
+ { "two_hearts", 0x1f495 },
+ { "revolving_hearts", 0x1f49e },
+ { "cupid", 0x1f498 },
+ { "sparkling_heart", 0x1f496 },
+ { "sparkles", 0x2728 },
+ { "star", 0x2b50 },
+ { "star2", 0x1f31f },
+ { "dizzy", 0x1f4ab },
+ { "boom", 0x1f4a5 },
+ { "collision", 0x1f4a5 },
+ { "anger", 0x1f4a2 },
+ { "exclamation", 0x2757 },
+ { "question", 0x2753 },
+ { "grey_exclamation", 0x2755 },
+ { "grey_question", 0x2754 },
+ { "zzz", 0x1f4a4 },
+ { "dash", 0x1f4a8 },
+ { "sweat_drops", 0x1f4a6 },
+ { "notes", 0x1f3b6 },
+ { "musical_note", 0x1f3b5 },
+ { "fire", 0x1f525 },
+ { "hankey", 0x1f4a9 },
+ { "poop", 0x1f4a9 },
+ { "+1", 0x1f44d },
+ { "thumbsup", 0x1f44d },
+ { "-1", 0x1f44e },
+ { "thumbsdown", 0x1f44e },
+ { "ok_hand", 0x1f44c },
+ { "punch", 0x1f44a },
+ { "facepunch", 0x1f44a },
+ { "fist", 0x270a },
+ { "v", 0x270c },
+ { "wave", 0x1f44b },
+ { "hand", 0x270b },
+ { "raised_hand", 0x270b },
+ { "open_hands", 0x1f450 },
+ { "point_up", 0x261d },
+ { "point_down", 0x1f447 },
+ { "point_left", 0x1f448 },
+ { "point_right", 0x1f449 },
+ { "raised_hands", 0x1f64c },
+ { "pray", 0x1f64f },
+ { "point_up_2", 0x1f446 },
+ { "clap", 0x1f44f },
+ { "muscle", 0x1f4aa },
+ { "metal", 0x1f918 },
+ { "walking", 0x1f6b6 },
+ { "runner", 0x1f3c3 },
+ { "running", 0x1f3c3 },
+ { "couple", 0x1f46b },
+ { "family", 0x1f46a },
+ { "two_men_holding_hands", 0x1f46c },
+ { "two_women_holding_hands", 0x1f46d },
+ { "dancer", 0x1f483 },
+ { "dancers", 0x1f46f },
+ { "ok_woman", 0x1f646 },
+ { "no_good", 0x1f645 },
+ { "information_desk_person", 0x1f481 },
+ { "raising_hand", 0x1f64b },
+ { "bride_with_veil", 0x1f470 },
+ { "person_with_pouting_face", 0x1f64e },
+ { "person_frowning", 0x1f64d },
+ { "bow", 0x1f647 },
+ { "couple_with_heart", 0x1f491 },
+ { "massage", 0x1f486 },
+ { "haircut", 0x1f487 },
+ { "nail_care", 0x1f485 },
+ { "boy", 0x1f466 },
+ { "girl", 0x1f467 },
+ { "woman", 0x1f469 },
+ { "man", 0x1f468 },
+ { "baby", 0x1f476 },
+ { "older_woman", 0x1f475 },
+ { "older_man", 0x1f474 },
+ { "person_with_blond_hair", 0x1f471 },
+ { "man_with_gua_pi_mao", 0x1f472 },
+ { "man_with_turban", 0x1f473 },
+ { "construction_worker", 0x1f477 },
+ { "cop", 0x1f46e },
+ { "angel", 0x1f47c },
+ { "princess", 0x1f478 },
+ { "smiley_cat", 0x1f63a },
+ { "smile_cat", 0x1f638 },
+ { "heart_eyes_cat", 0x1f63b },
+ { "kissing_cat", 0x1f63d },
+ { "smirk_cat", 0x1f63c },
+ { "scream_cat", 0x1f640 },
+ { "crying_cat_face", 0x1f63f },
+ { "joy_cat", 0x1f639 },
+ { "pouting_cat", 0x1f63e },
+ { "japanese_ogre", 0x1f479 },
+ { "japanese_goblin", 0x1f47a },
+ { "see_no_evil", 0x1f648 },
+ { "hear_no_evil", 0x1f649 },
+ { "speak_no_evil", 0x1f64a },
+ { "guardsman", 0x1f482 },
+ { "skull", 0x1f480 },
+ { "feet", 0x1f43e },
+ { "lips", 0x1f444 },
+ { "kiss", 0x1f48b },
+ { "droplet", 0x1f4a7 },
+ { "ear", 0x1f442 },
+ { "eyes", 0x1f440 },
+ { "nose", 0x1f443 },
+ { "tongue", 0x1f445 },
+ { "love_letter", 0x1f48c },
+ { "bust_in_silhouette", 0x1f464 },
+ { "busts_in_silhouette", 0x1f465 },
+ { "speech_balloon", 0x1f4ac },
+ { "thought_balloon", 0x1f4ad },
+ { "sunny", 0x2600 },
+ { "umbrella", 0x2614 },
+ { "cloud", 0x2601 },
+ { "snowflake", 0x2744 },
+ { "snowman", 0x26c4 },
+ { "zap", 0x26a1 },
+ { "cyclone", 0x1f300 },
+ { "foggy", 0x1f301 },
+ { "ocean", 0x1f30a },
+ { "cat", 0x1f431 },
+ { "dog", 0x1f436 },
+ { "mouse", 0x1f42d },
+ { "hamster", 0x1f439 },
+ { "rabbit", 0x1f430 },
+ { "wolf", 0x1f43a },
+ { "frog", 0x1f438 },
+ { "tiger", 0x1f42f },
+ { "koala", 0x1f428 },
+ { "bear", 0x1f43b },
+ { "pig", 0x1f437 },
+ { "pig_nose", 0x1f43d },
+ { "cow", 0x1f42e },
+ { "boar", 0x1f417 },
+ { "monkey_face", 0x1f435 },
+ { "monkey", 0x1f412 },
+ { "horse", 0x1f434 },
+ { "racehorse", 0x1f40e },
+ { "camel", 0x1f42b },
+ { "sheep", 0x1f411 },
+ { "elephant", 0x1f418 },
+ { "panda_face", 0x1f43c },
+ { "snake", 0x1f40d },
+ { "bird", 0x1f426 },
+ { "baby_chick", 0x1f424 },
+ { "hatched_chick", 0x1f425 },
+ { "hatching_chick", 0x1f423 },
+ { "chicken", 0x1f414 },
+ { "penguin", 0x1f427 },
+ { "turtle", 0x1f422 },
+ { "bug", 0x1f41b },
+ { "honeybee", 0x1f41d },
+ { "ant", 0x1f41c },
+ { "beetle", 0x1f41e },
+ { "snail", 0x1f40c },
+ { "octopus", 0x1f419 },
+ { "tropical_fish", 0x1f420 },
+ { "fish", 0x1f41f },
+ { "whale", 0x1f433 },
+ { "whale2", 0x1f40b },
+ { "dolphin", 0x1f42c },
+ { "cow2", 0x1f404 },
+ { "ram", 0x1f40f },
+ { "rat", 0x1f400 },
+ { "water_buffalo", 0x1f403 },
+ { "tiger2", 0x1f405 },
+ { "rabbit2", 0x1f407 },
+ { "dragon", 0x1f409 },
+ { "goat", 0x1f410 },
+ { "rooster", 0x1f413 },
+ { "dog2", 0x1f415 },
+ { "pig2", 0x1f416 },
+ { "mouse2", 0x1f401 },
+ { "ox", 0x1f402 },
+ { "dragon_face", 0x1f432 },
+ { "blowfish", 0x1f421 },
+ { "crocodile", 0x1f40a },
+ { "dromedary_camel", 0x1f42a },
+ { "leopard", 0x1f406 },
+ { "cat2", 0x1f408 },
+ { "poodle", 0x1f429 },
+ { "paw_prints", 0x1f43e },
+ { "bouquet", 0x1f490 },
+ { "cherry_blossom", 0x1f338 },
+ { "tulip", 0x1f337 },
+ { "four_leaf_clover", 0x1f340 },
+ { "rose", 0x1f339 },
+ { "sunflower", 0x1f33b },
+ { "hibiscus", 0x1f33a },
+ { "maple_leaf", 0x1f341 },
+ { "leaves", 0x1f343 },
+ { "fallen_leaf", 0x1f342 },
+ { "herb", 0x1f33f },
+ { "mushroom", 0x1f344 },
+ { "cactus", 0x1f335 },
+ { "palm_tree", 0x1f334 },
+ { "evergreen_tree", 0x1f332 },
+ { "deciduous_tree", 0x1f333 },
+ { "chestnut", 0x1f330 },
+ { "seedling", 0x1f331 },
+ { "blossom", 0x1f33c },
+ { "ear_of_rice", 0x1f33e },
+ { "shell", 0x1f41a },
+ { "globe_with_meridians", 0x1f310 },
+ { "sun_with_face", 0x1f31e },
+ { "full_moon_with_face", 0x1f31d },
+ { "new_moon_with_face", 0x1f31a },
+ { "new_moon", 0x1f311 },
+ { "waxing_crescent_moon", 0x1f312 },
+ { "first_quarter_moon", 0x1f313 },
+ { "waxing_gibbous_moon", 0x1f314 },
+ { "full_moon", 0x1f315 },
+ { "waning_gibbous_moon", 0x1f316 },
+ { "last_quarter_moon", 0x1f317 },
+ { "waning_crescent_moon", 0x1f318 },
+ { "last_quarter_moon_with_face", 0x1f31c },
+ { "first_quarter_moon_with_face", 0x1f31b },
+ { "moon", 0x1f314 },
+ { "earth_africa", 0x1f30d },
+ { "earth_americas", 0x1f30e },
+ { "earth_asia", 0x1f30f },
+ { "volcano", 0x1f30b },
+ { "milky_way", 0x1f30c },
+ { "partly_sunny", 0x26c5 },
+ { "bamboo", 0x1f38d },
+ { "gift_heart", 0x1f49d },
+ { "dolls", 0x1f38e },
+ { "school_satchel", 0x1f392 },
+ { "mortar_board", 0x1f393 },
+ { "flags", 0x1f38f },
+ { "fireworks", 0x1f386 },
+ { "sparkler", 0x1f387 },
+ { "wind_chime", 0x1f390 },
+ { "rice_scene", 0x1f391 },
+ { "jack_o_lantern", 0x1f383 },
+ { "ghost", 0x1f47b },
+ { "santa", 0x1f385 },
+ { "christmas_tree", 0x1f384 },
+ { "gift", 0x1f381 },
+ { "bell", 0x1f514 },
+ { "no_bell", 0x1f515 },
+ { "tanabata_tree", 0x1f38b },
+ { "tada", 0x1f389 },
+ { "confetti_ball", 0x1f38a },
+ { "balloon", 0x1f388 },
+ { "crystal_ball", 0x1f52e },
+ { "cd", 0x1f4bf },
+ { "dvd", 0x1f4c0 },
+ { "floppy_disk", 0x1f4be },
+ { "camera", 0x1f4f7 },
+ { "video_camera", 0x1f4f9 },
+ { "movie_camera", 0x1f3a5 },
+ { "computer", 0x1f4bb },
+ { "tv", 0x1f4fa },
+ { "iphone", 0x1f4f1 },
+ { "phone", 0x260e },
+ { "telephone", 0x260e },
+ { "telephone_receiver", 0x1f4de },
+ { "pager", 0x1f4df },
+ { "fax", 0x1f4e0 },
+ { "minidisc", 0x1f4bd },
+ { "vhs", 0x1f4fc },
+ { "sound", 0x1f509 },
+ { "speaker", 0x1f508 },
+ { "mute", 0x1f507 },
+ { "loudspeaker", 0x1f4e2 },
+ { "mega", 0x1f4e3 },
+ { "hourglass", 0x231b },
+ { "hourglass_flowing_sand", 0x23f3 },
+ { "alarm_clock", 0x23f0 },
+ { "watch", 0x231a },
+ { "radio", 0x1f4fb },
+ { "satellite", 0x1f4e1 },
+ { "loop", 0x27bf },
+ { "mag", 0x1f50d },
+ { "mag_right", 0x1f50e },
+ { "unlock", 0x1f513 },
+ { "lock", 0x1f512 },
+ { "lock_with_ink_pen", 0x1f50f },
+ { "closed_lock_with_key", 0x1f510 },
+ { "key", 0x1f511 },
+ { "bulb", 0x1f4a1 },
+ { "flashlight", 0x1f526 },
+ { "high_brightness", 0x1f506 },
+ { "low_brightness", 0x1f505 },
+ { "electric_plug", 0x1f50c },
+ { "battery", 0x1f50b },
+ { "calling", 0x1f4f2 },
+ { "email", 0x2709 },
+ { "mailbox", 0x1f4eb },
+ { "postbox", 0x1f4ee },
+ { "bath", 0x1f6c0 },
+ { "bathtub", 0x1f6c1 },
+ { "shower", 0x1f6bf },
+ { "toilet", 0x1f6bd },
+ { "wrench", 0x1f527 },
+ { "nut_and_bolt", 0x1f529 },
+ { "hammer", 0x1f528 },
+ { "seat", 0x1f4ba },
+ { "moneybag", 0x1f4b0 },
+ { "yen", 0x1f4b4 },
+ { "dollar", 0x1f4b5 },
+ { "pound", 0x1f4b7 },
+ { "euro", 0x1f4b6 },
+ { "credit_card", 0x1f4b3 },
+ { "money_with_wings", 0x1f4b8 },
+ { "e-mail", 0x1f4e7 },
+ { "inbox_tray", 0x1f4e5 },
+ { "outbox_tray", 0x1f4e4 },
+ { "envelope", 0x2709 },
+ { "incoming_envelope", 0x1f4e8 },
+ { "postal_horn", 0x1f4ef },
+ { "mailbox_closed", 0x1f4ea },
+ { "mailbox_with_mail", 0x1f4ec },
+ { "mailbox_with_no_mail", 0x1f4ed },
+ { "door", 0x1f6aa },
+ { "smoking", 0x1f6ac },
+ { "bomb", 0x1f4a3 },
+ { "gun", 0x1f52b },
+ { "hocho", 0x1f52a },
+ { "pill", 0x1f48a },
+ { "syringe", 0x1f489 },
+ { "page_facing_up", 0x1f4c4 },
+ { "page_with_curl", 0x1f4c3 },
+ { "bookmark_tabs", 0x1f4d1 },
+ { "bar_chart", 0x1f4ca },
+ { "chart_with_upwards_trend", 0x1f4c8 },
+ { "chart_with_downwards_trend", 0x1f4c9 },
+ { "scroll", 0x1f4dc },
+ { "clipboard", 0x1f4cb },
+ { "calendar", 0x1f4c6 },
+ { "date", 0x1f4c5 },
+ { "card_index", 0x1f4c7 },
+ { "file_folder", 0x1f4c1 },
+ { "open_file_folder", 0x1f4c2 },
+ { "scissors", 0x2702 },
+ { "pushpin", 0x1f4cc },
+ { "paperclip", 0x1f4ce },
+ { "black_nib", 0x2712 },
+ { "pencil2", 0x270f },
+ { "straight_ruler", 0x1f4cf },
+ { "triangular_ruler", 0x1f4d0 },
+ { "closed_book", 0x1f4d5 },
+ { "green_book", 0x1f4d7 },
+ { "blue_book", 0x1f4d8 },
+ { "orange_book", 0x1f4d9 },
+ { "notebook", 0x1f4d3 },
+ { "notebook_with_decorative_cover", 0x1f4d4 },
+ { "ledger", 0x1f4d2 },
+ { "books", 0x1f4da },
+ { "bookmark", 0x1f516 },
+ { "name_badge", 0x1f4db },
+ { "microscope", 0x1f52c },
+ { "telescope", 0x1f52d },
+ { "newspaper", 0x1f4f0 },
+ { "football", 0x1f3c8 },
+ { "basketball", 0x1f3c0 },
+ { "soccer", 0x26bd },
+ { "baseball", 0x26be },
+ { "tennis", 0x1f3be },
+ { "8ball", 0x1f3b1 },
+ { "rugby_football", 0x1f3c9 },
+ { "bowling", 0x1f3b3 },
+ { "golf", 0x26f3 },
+ { "mountain_bicyclist", 0x1f6b5 },
+ { "bicyclist", 0x1f6b4 },
+ { "horse_racing", 0x1f3c7 },
+ { "snowboarder", 0x1f3c2 },
+ { "swimmer", 0x1f3ca },
+ { "surfer", 0x1f3c4 },
+ { "ski", 0x1f3bf },
+ { "spades", 0x2660 },
+ { "hearts", 0x2665 },
+ { "clubs", 0x2663 },
+ { "diamonds", 0x2666 },
+ { "gem", 0x1f48e },
+ { "ring", 0x1f48d },
+ { "trophy", 0x1f3c6 },
+ { "musical_score", 0x1f3bc },
+ { "musical_keyboard", 0x1f3b9 },
+ { "violin", 0x1f3bb },
+ { "space_invader", 0x1f47e },
+ { "video_game", 0x1f3ae },
+ { "black_joker", 0x1f0cf },
+ { "flower_playing_cards", 0x1f3b4 },
+ { "game_die", 0x1f3b2 },
+ { "dart", 0x1f3af },
+ { "mahjong", 0x1f004 },
+ { "clapper", 0x1f3ac },
+ { "memo", 0x1f4dd },
+ { "pencil", 0x1f4dd },
+ { "book", 0x1f4d6 },
+ { "art", 0x1f3a8 },
+ { "microphone", 0x1f3a4 },
+ { "headphones", 0x1f3a7 },
+ { "trumpet", 0x1f3ba },
+ { "saxophone", 0x1f3b7 },
+ { "guitar", 0x1f3b8 },
+ { "shoe", 0x1f45e },
+ { "sandal", 0x1f461 },
+ { "high_heel", 0x1f460 },
+ { "lipstick", 0x1f484 },
+ { "boot", 0x1f462 },
+ { "shirt", 0x1f455 },
+ { "tshirt", 0x1f455 },
+ { "necktie", 0x1f454 },
+ { "womans_clothes", 0x1f45a },
+ { "dress", 0x1f457 },
+ { "running_shirt_with_sash", 0x1f3bd },
+ { "jeans", 0x1f456 },
+ { "kimono", 0x1f458 },
+ { "bikini", 0x1f459 },
+ { "ribbon", 0x1f380 },
+ { "tophat", 0x1f3a9 },
+ { "crown", 0x1f451 },
+ { "womans_hat", 0x1f452 },
+ { "mans_shoe", 0x1f45e },
+ { "closed_umbrella", 0x1f302 },
+ { "briefcase", 0x1f4bc },
+ { "handbag", 0x1f45c },
+ { "pouch", 0x1f45d },
+ { "purse", 0x1f45b },
+ { "eyeglasses", 0x1f453 },
+ { "fishing_pole_and_fish", 0x1f3a3 },
+ { "coffee", 0x2615 },
+ { "tea", 0x1f375 },
+ { "sake", 0x1f376 },
+ { "baby_bottle", 0x1f37c },
+ { "beer", 0x1f37a },
+ { "beers", 0x1f37b },
+ { "cocktail", 0x1f378 },
+ { "tropical_drink", 0x1f379 },
+ { "wine_glass", 0x1f377 },
+ { "fork_and_knife", 0x1f374 },
+ { "pizza", 0x1f355 },
+ { "hamburger", 0x1f354 },
+ { "fries", 0x1f35f },
+ { "poultry_leg", 0x1f357 },
+ { "meat_on_bone", 0x1f356 },
+ { "spaghetti", 0x1f35d },
+ { "curry", 0x1f35b },
+ { "fried_shrimp", 0x1f364 },
+ { "bento", 0x1f371 },
+ { "sushi", 0x1f363 },
+ { "fish_cake", 0x1f365 },
+ { "rice_ball", 0x1f359 },
+ { "rice_cracker", 0x1f358 },
+ { "rice", 0x1f35a },
+ { "ramen", 0x1f35c },
+ { "stew", 0x1f372 },
+ { "oden", 0x1f362 },
+ { "dango", 0x1f361 },
+ { "egg", 0x1f95a },
+ { "bread", 0x1f35e },
+ { "doughnut", 0x1f369 },
+ { "custard", 0x1f36e },
+ { "icecream", 0x1f366 },
+ { "ice_cream", 0x1f368 },
+ { "shaved_ice", 0x1f367 },
+ { "birthday", 0x1f382 },
+ { "cake", 0x1f370 },
+ { "cookie", 0x1f36a },
+ { "chocolate_bar", 0x1f36b },
+ { "candy", 0x1f36c },
+ { "lollipop", 0x1f36d },
+ { "honey_pot", 0x1f36f },
+ { "apple", 0x1f34e },
+ { "green_apple", 0x1f34f },
+ { "tangerine", 0x1f34a },
+ { "lemon", 0x1f34b },
+ { "cherries", 0x1f352 },
+ { "grapes", 0x1f347 },
+ { "watermelon", 0x1f349 },
+ { "strawberry", 0x1f353 },
+ { "peach", 0x1f351 },
+ { "melon", 0x1f348 },
+ { "banana", 0x1f34c },
+ { "pear", 0x1f350 },
+ { "pineapple", 0x1f34d },
+ { "sweet_potato", 0x1f360 },
+ { "eggplant", 0x1f346 },
+ { "tomato", 0x1f345 },
+ { "corn", 0x1f33d },
+ { "house", 0x1f3e0 },
+ { "house_with_garden", 0x1f3e1 },
+ { "school", 0x1f3eb },
+ { "office", 0x1f3e2 },
+ { "post_office", 0x1f3e3 },
+ { "hospital", 0x1f3e5 },
+ { "bank", 0x1f3e6 },
+ { "convenience_store", 0x1f3ea },
+ { "love_hotel", 0x1f3e9 },
+ { "hotel", 0x1f3e8 },
+ { "wedding", 0x1f492 },
+ { "church", 0x26ea },
+ { "department_store", 0x1f3ec },
+ { "european_post_office", 0x1f3e4 },
+ { "city_sunrise", 0x1f307 },
+ { "city_sunset", 0x1f306 },
+ { "japanese_castle", 0x1f3ef },
+ { "european_castle", 0x1f3f0 },
+ { "tent", 0x26fa },
+ { "factory", 0x1f3ed },
+ { "tokyo_tower", 0x1f5fc },
+ { "japan", 0x1f5fe },
+ { "mount_fuji", 0x1f5fb },
+ { "sunrise_over_mountains", 0x1f304 },
+ { "sunrise", 0x1f305 },
+ { "stars", 0x1f320 },
+ { "statue_of_liberty", 0x1f5fd },
+ { "bridge_at_night", 0x1f309 },
+ { "carousel_horse", 0x1f3a0 },
+ { "rainbow", 0x1f308 },
+ { "ferris_wheel", 0x1f3a1 },
+ { "fountain", 0x26f2 },
+ { "roller_coaster", 0x1f3a2 },
+ { "ship", 0x1f6a2 },
+ { "speedboat", 0x1f6a4 },
+ { "boat", 0x26f5 },
+ { "sailboat", 0x26f5 },
+ { "rowboat", 0x1f6a3 },
+ { "anchor", 0x2693 },
+ { "rocket", 0x1f680 },
+ { "airplane", 0x2708 },
+ { "helicopter", 0x1f681 },
+ { "steam_locomotive", 0x1f682 },
+ { "tram", 0x1f68a },
+ { "mountain_railway", 0x1f69e },
+ { "bike", 0x1f6b2 },
+ { "aerial_tramway", 0x1f6a1 },
+ { "suspension_railway", 0x1f69f },
+ { "mountain_cableway", 0x1f6a0 },
+ { "tractor", 0x1f69c },
+ { "blue_car", 0x1f699 },
+ { "oncoming_automobile", 0x1f698 },
+ { "car", 0x1f697 },
+ { "red_car", 0x1f697 },
+ { "taxi", 0x1f695 },
+ { "oncoming_taxi", 0x1f696 },
+ { "articulated_lorry", 0x1f69b },
+ { "bus", 0x1f68c },
+ { "oncoming_bus", 0x1f68d },
+ { "rotating_light", 0x1f6a8 },
+ { "police_car", 0x1f693 },
+ { "oncoming_police_car", 0x1f694 },
+ { "fire_engine", 0x1f692 },
+ { "ambulance", 0x1f691 },
+ { "minibus", 0x1f690 },
+ { "truck", 0x1f69a },
+ { "train", 0x1f68b },
+ { "station", 0x1f689 },
+ { "train2", 0x1f686 },
+ { "bullettrain_front", 0x1f685 },
+ { "bullettrain_side", 0x1f684 },
+ { "light_rail", 0x1f688 },
+ { "monorail", 0x1f69d },
+ { "railway_car", 0x1f683 },
+ { "trolleybus", 0x1f68e },
+ { "ticket", 0x1f3ab },
+ { "fuelpump", 0x26fd },
+ { "vertical_traffic_light", 0x1f6a6 },
+ { "traffic_light", 0x1f6a5 },
+ { "warning", 0x26a0 },
+ { "construction", 0x1f6a7 },
+ { "beginner", 0x1f530 },
+ { "atm", 0x1f3e7 },
+ { "slot_machine", 0x1f3b0 },
+ { "busstop", 0x1f68f },
+ { "barber", 0x1f488 },
+ { "hotsprings", 0x2668 },
+ { "checkered_flag", 0x1f3c1 },
+ { "crossed_flags", 0x1f38c },
+ { "izakaya_lantern", 0x1f3ee },
+ { "moyai", 0x1f5ff },
+ { "circus_tent", 0x1f3aa },
+ { "performing_arts", 0x1f3ad },
+ { "round_pushpin", 0x1f4cd },
+ { "triangular_flag_on_post", 0x1f6a9 },
+ { "keycap_ten", 0x1f51f },
+ { "1234", 0x1f522 },
+ { "symbols", 0x1f523 },
+ { "arrow_backward", 0x25c0 },
+ { "arrow_down", 0x2b07 },
+ { "arrow_forward", 0x25b6 },
+ { "arrow_left", 0x2b05 },
+ { "capital_abcd", 0x1f520 },
+ { "abcd", 0x1f521 },
+ { "abc", 0x1f524 },
+ { "arrow_lower_left", 0x2199 },
+ { "arrow_lower_right", 0x2198 },
+ { "arrow_right", 0x27a1 },
+ { "arrow_up", 0x2b06 },
+ { "arrow_upper_left", 0x2196 },
+ { "arrow_upper_right", 0x2197 },
+ { "arrow_double_down", 0x23ec },
+ { "arrow_double_up", 0x23eb },
+ { "arrow_down_small", 0x1f53d },
+ { "arrow_heading_down", 0x2935 },
+ { "arrow_heading_up", 0x2934 },
+ { "leftwards_arrow_with_hook", 0x21a9 },
+ { "arrow_right_hook", 0x21aa },
+ { "left_right_arrow", 0x2194 },
+ { "arrow_up_down", 0x2195 },
+ { "arrow_up_small", 0x1f53c },
+ { "arrows_clockwise", 0x1f503 },
+ { "arrows_counterclockwise", 0x1f504 },
+ { "rewind", 0x23ea },
+ { "fast_forward", 0x23e9 },
+ { "information_source", 0x2139 },
+ { "ok", 0x1f197 },
+ { "twisted_rightwards_arrows", 0x1f500 },
+ { "repeat", 0x1f501 },
+ { "repeat_one", 0x1f502 },
+ { "new", 0x1f195 },
+ { "top", 0x1f51d },
+ { "up", 0x1f199 },
+ { "cool", 0x1f192 },
+ { "free", 0x1f193 },
+ { "ng", 0x1f196 },
+ { "cinema", 0x1f3a6 },
+ { "koko", 0x1f201 },
+ { "signal_strength", 0x1f4f6 },
+ { "u5272", 0x1f239 },
+ { "u5408", 0x1f234 },
+ { "u55b6", 0x1f23a },
+ { "u6307", 0x1f22f },
+ { "u6708", 0x1f237 },
+ { "u6709", 0x1f236 },
+ { "u6e80", 0x1f235 },
+ { "u7121", 0x1f21a },
+ { "u7533", 0x1f238 },
+ { "u7a7a", 0x1f233 },
+ { "u7981", 0x1f232 },
+ { "sa", 0x1f202 },
+ { "restroom", 0x1f6bb },
+ { "mens", 0x1f6b9 },
+ { "womens", 0x1f6ba },
+ { "baby_symbol", 0x1f6bc },
+ { "no_smoking", 0x1f6ad },
+ { "parking", 0x1f17f },
+ { "wheelchair", 0x267f },
+ { "metro", 0x1f687 },
+ { "baggage_claim", 0x1f6c4 },
+ { "accept", 0x1f251 },
+ { "wc", 0x1f6be },
+ { "potable_water", 0x1f6b0 },
+ { "put_litter_in_its_place", 0x1f6ae },
+ { "secret", 0x3299 },
+ { "congratulations", 0x3297 },
+ { "m", 0x24c2 },
+ { "passport_control", 0x1f6c2 },
+ { "left_luggage", 0x1f6c5 },
+ { "customs", 0x1f6c3 },
+ { "ideograph_advantage", 0x1f250 },
+ { "cl", 0x1f191 },
+ { "sos", 0x1f198 },
+ { "id", 0x1f194 },
+ { "no_entry_sign", 0x1f6ab },
+ { "underage", 0x1f51e },
+ { "no_mobile_phones", 0x1f4f5 },
+ { "do_not_litter", 0x1f6af },
+ { "non-potable_water", 0x1f6b1 },
+ { "no_bicycles", 0x1f6b3 },
+ { "no_pedestrians", 0x1f6b7 },
+ { "children_crossing", 0x1f6b8 },
+ { "no_entry", 0x26d4 },
+ { "eight_spoked_asterisk", 0x2733 },
+ { "eight_pointed_black_star", 0x2734 },
+ { "heart_decoration", 0x1f49f },
+ { "vs", 0x1f19a },
+ { "vibration_mode", 0x1f4f3 },
+ { "mobile_phone_off", 0x1f4f4 },
+ { "chart", 0x1f4b9 },
+ { "currency_exchange", 0x1f4b1 },
+ { "aries", 0x2648 },
+ { "taurus", 0x2649 },
+ { "gemini", 0x264a },
+ { "cancer", 0x264b },
+ { "leo", 0x264c },
+ { "virgo", 0x264d },
+ { "libra", 0x264e },
+ { "scorpius", 0x264f },
+ { "sagittarius", 0x2650 },
+ { "capricorn", 0x2651 },
+ { "aquarius", 0x2652 },
+ { "pisces", 0x2653 },
+ { "ophiuchus", 0x26ce },
+ { "six_pointed_star", 0x1f52f },
+ { "negative_squared_cross_mark", 0x274e },
+ { "a", 0x1f170 },
+ { "b", 0x1f171 },
+ { "ab", 0x1f18e },
+ { "o2", 0x1f17e },
+ { "diamond_shape_with_a_dot_inside", 0x1f4a0 },
+ { "recycle", 0x267b },
+ { "end", 0x1f51a },
+ { "on", 0x1f51b },
+ { "soon", 0x1f51c },
+ { "clock1", 0x1f550 },
+ { "clock130", 0x1f55c },
+ { "clock10", 0x1f559 },
+ { "clock1030", 0x1f565 },
+ { "clock11", 0x1f55a },
+ { "clock1130", 0x1f566 },
+ { "clock12", 0x1f55b },
+ { "clock1230", 0x1f567 },
+ { "clock2", 0x1f551 },
+ { "clock230", 0x1f55d },
+ { "clock3", 0x1f552 },
+ { "clock330", 0x1f55e },
+ { "clock4", 0x1f553 },
+ { "clock430", 0x1f55f },
+ { "clock5", 0x1f554 },
+ { "clock530", 0x1f560 },
+ { "clock6", 0x1f555 },
+ { "clock630", 0x1f561 },
+ { "clock7", 0x1f556 },
+ { "clock730", 0x1f562 },
+ { "clock8", 0x1f557 },
+ { "clock830", 0x1f563 },
+ { "clock9", 0x1f558 },
+ { "clock930", 0x1f564 },
+ { "heavy_dollar_sign", 0x1f4b2 },
+ { "copyright", 0x00a9 },
+ { "registered", 0x00ae },
+ { "tm", 0x2122 },
+ { "x", 0x274c },
+ { "heavy_exclamation_mark", 0x2757 },
+ { "bangbang", 0x203c },
+ { "interrobang", 0x2049 },
+ { "o", 0x2b55 },
+ { "heavy_multiplication_x", 0x2716 },
+ { "heavy_plus_sign", 0x2795 },
+ { "heavy_minus_sign", 0x2796 },
+ { "heavy_division_sign", 0x2797 },
+ { "white_flower", 0x1f4ae },
+ { "100", 0x1f4af },
+ { "heavy_check_mark", 0x2714 },
+ { "ballot_box_with_check", 0x2611 },
+ { "radio_button", 0x1f518 },
+ { "link", 0x1f517 },
+ { "curly_loop", 0x27b0 },
+ { "wavy_dash", 0x3030 },
+ { "part_alternation_mark", 0x303d },
+ { "trident", 0x1f531 },
+ { "white_check_mark", 0x2705 },
+ { "black_square_button", 0x1f532 },
+ { "white_square_button", 0x1f533 },
+ { "black_circle", 0x26ab },
+ { "white_circle", 0x26aa },
+ { "red_circle", 0x1f534 },
+ { "large_blue_circle", 0x1f535 },
+ { "large_blue_diamond", 0x1f537 },
+ { "large_orange_diamond", 0x1f536 },
+ { "small_blue_diamond", 0x1f539 },
+ { "small_orange_diamond", 0x1f538 },
+ { "small_red_triangle", 0x1f53a },
+ { "small_red_triangle_down", 0x1f53b },
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.cs
new file mode 100644
index 0000000..e790111
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/EmojiInline.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing emoji symbol.
+ ///
+ public partial class EmojiInline : MarkdownInline, IInlineLeaf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EmojiInline()
+ : base(MarkdownInlineType.Emoji)
+ {
+ }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = ':', Method = InlineParseMethod.Emoji });
+ }
+
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ if (start >= maxEnd - 1)
+ {
+ return null;
+ }
+
+ // Check the start sequence.
+ string startSequence = markdown.Substring(start, 1);
+ if (startSequence != ":")
+ {
+ return null;
+ }
+
+ // Find the end of the span.
+ var innerStart = start + 1;
+ int innerEnd = Common.IndexOf(markdown, startSequence, innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // The first character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]))
+ {
+ return null;
+ }
+
+ // The last character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ var emojiName = markdown.Substring(innerStart, innerEnd - innerStart);
+
+ if (_emojiCodesDictionary.TryGetValue(emojiName, out var emojiCode))
+ {
+ var result = new EmojiInline { Text = char.ConvertFromUtf32(emojiCode), Type = MarkdownInlineType.Emoji };
+ return new InlineParseResult(result, start, innerEnd + 1);
+ }
+
+ return null;
+ }
+
+ ///
+ public string Text { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/HyperlinkInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/HyperlinkInline.cs
new file mode 100644
index 0000000..9ccb5b4
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/HyperlinkInline.cs
@@ -0,0 +1,477 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Represents a type of hyperlink where the text and the target URL cannot be controlled
+ /// independently.
+ ///
+ public class HyperlinkInline : MarkdownInline, IInlineLeaf, ILinkElement
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HyperlinkInline()
+ : base(MarkdownInlineType.RawHyperlink)
+ {
+ }
+
+ ///
+ /// Gets or sets the text to display.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets the URL to link to.
+ ///
+ public string Url { get; set; }
+
+ ///
+ /// Gets this type of hyperlink does not have a tooltip.
+ ///
+ string ILinkElement.Tooltip => null;
+
+ ///
+ /// Gets or sets the type of hyperlink.
+ ///
+ public HyperlinkType LinkType { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '<', Method = InlineParseMethod.AngleBracketLink });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = ':', Method = InlineParseMethod.Url });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '/', Method = InlineParseMethod.RedditLink });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '.', Method = InlineParseMethod.PartialLink });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '@', Method = InlineParseMethod.Email });
+ }
+
+ ///
+ /// Attempts to parse a URL within angle brackets e.g. "http://www.reddit.com".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed URL, or null if this is not a URL.
+ internal static InlineParseResult ParseAngleBracketLink(string markdown, int start, int maxEnd)
+ {
+ int innerStart = start + 1;
+
+ // Check for a known scheme e.g. "https://".
+ int pos = -1;
+ foreach (var scheme in MarkdownDocument.KnownSchemes)
+ {
+ if (maxEnd - innerStart >= scheme.Length && string.Equals(markdown.Substring(innerStart, scheme.Length), scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ // URL scheme found.
+ pos = innerStart + scheme.Length;
+ break;
+ }
+ }
+
+ if (pos == -1)
+ {
+ return null;
+ }
+
+ // Angle bracket links should not have any whitespace.
+ int innerEnd = markdown.IndexOfAny(new char[] { ' ', '\t', '\r', '\n', '>' }, pos, maxEnd - pos);
+ if (innerEnd == -1 || markdown[innerEnd] != '>')
+ {
+ return null;
+ }
+
+ // There should be at least one character after the http://.
+ if (innerEnd == pos)
+ {
+ return null;
+ }
+
+ var url = markdown.Substring(innerStart, innerEnd - innerStart);
+ return new InlineParseResult(new HyperlinkInline { Url = url, Text = url, LinkType = HyperlinkType.BracketedUrl }, start, innerEnd + 1);
+ }
+
+ ///
+ /// Attempts to parse a URL e.g. "http://www.reddit.com".
+ ///
+ /// The markdown text.
+ /// The location of the colon character.
+ /// The location to stop parsing.
+ /// A parsed URL, or null if this is not a URL.
+ internal static InlineParseResult ParseUrl(string markdown, int tripPos, int maxEnd)
+ {
+ int start = -1;
+
+ // Check for a known scheme e.g. "https://".
+ foreach (var scheme in MarkdownDocument.KnownSchemes)
+ {
+ int schemeStart = tripPos - scheme.Length;
+ if (schemeStart >= 0 && schemeStart <= maxEnd - scheme.Length && string.Equals(markdown.Substring(schemeStart, scheme.Length), scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ // URL scheme found.
+ start = schemeStart;
+ break;
+ }
+ }
+
+ if (start == -1)
+ {
+ return null;
+ }
+
+ // The previous character must be non-alphanumeric i.e. "ahttp://t.co" is not a valid URL.
+ if (start > 0 && char.IsLetter(markdown[start - 1]))
+ {
+ return null;
+ }
+
+ // The URL must have at least one character after the http:// and at least one dot.
+ int pos = tripPos + 3;
+ if (pos > maxEnd)
+ {
+ return null;
+ }
+
+ int dotIndex = markdown.IndexOf('.', pos, maxEnd - pos);
+ if (dotIndex == -1 || dotIndex == pos)
+ {
+ return null;
+ }
+
+ // Find the end of the URL.
+ int end = FindUrlEnd(markdown, dotIndex + 1, maxEnd);
+
+ var url = markdown.Substring(start, end - start);
+ return new InlineParseResult(new HyperlinkInline { Url = url, Text = url, LinkType = HyperlinkType.FullUrl }, start, end);
+ }
+
+ ///
+ /// Attempts to parse a subreddit link e.g. "/r/news" or "r/news".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed subreddit or user link, or null if this is not a subreddit link.
+ internal static InlineParseResult ParseRedditLink(string markdown, int start, int maxEnd)
+ {
+ var result = ParseDoubleSlashLink(markdown, start, maxEnd);
+ if (result != null)
+ {
+ return result;
+ }
+
+ return ParseSingleSlashLink(markdown, start, maxEnd);
+ }
+
+ ///
+ /// Parse a link of the form "/r/news" or "/u/quinbd".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed subreddit or user link, or null if this is not a subreddit link.
+ private static InlineParseResult ParseDoubleSlashLink(string markdown, int start, int maxEnd)
+ {
+ // The minimum length is 4 characters ("/u/u").
+ if (start > maxEnd - 4)
+ {
+ return null;
+ }
+
+ // Determine the type of link (subreddit or user).
+ HyperlinkType linkType;
+ if (markdown[start + 1] == 'r')
+ {
+ linkType = HyperlinkType.Subreddit;
+ }
+ else if (markdown[start + 1] == 'u')
+ {
+ linkType = HyperlinkType.User;
+ }
+ else
+ {
+ return null;
+ }
+
+ // Check that there is another slash.
+ if (markdown[start + 2] != '/')
+ {
+ return null;
+ }
+
+ // Find the end of the link.
+ int end = FindEndOfRedditLink(markdown, start + 3, maxEnd);
+
+ // Subreddit names must be at least two characters long, users at least one.
+ if (end - start < (linkType == HyperlinkType.User ? 4 : 5))
+ {
+ return null;
+ }
+
+ // We found something!
+ var text = markdown.Substring(start, end - start);
+ return new InlineParseResult(new HyperlinkInline { Text = text, Url = text, LinkType = linkType }, start, end);
+ }
+
+ ///
+ /// Parse a link of the form "r/news" or "u/quinbd".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed subreddit or user link, or null if this is not a subreddit link.
+ private static InlineParseResult ParseSingleSlashLink(string markdown, int start, int maxEnd)
+ {
+ // The minimum length is 3 characters ("u/u").
+ start--;
+ if (start < 0 || start > maxEnd - 3)
+ {
+ return null;
+ }
+
+ // Determine the type of link (subreddit or user).
+ HyperlinkType linkType;
+ if (markdown[start] == 'r')
+ {
+ linkType = HyperlinkType.Subreddit;
+ }
+ else if (markdown[start] == 'u')
+ {
+ linkType = HyperlinkType.User;
+ }
+ else
+ {
+ return null;
+ }
+
+ // If the link doesn't start with '/', then the previous character must be
+ // non-alphanumeric i.e. "bear/trap" is not a valid subreddit link.
+ if (start >= 1 && (char.IsLetterOrDigit(markdown[start - 1]) || markdown[start - 1] == '/'))
+ {
+ return null;
+ }
+
+ // Find the end of the link.
+ int end = FindEndOfRedditLink(markdown, start + 2, maxEnd);
+
+ // Subreddit names must be at least two characters long, users at least one.
+ if (end - start < (linkType == HyperlinkType.User ? 3 : 4))
+ {
+ return null;
+ }
+
+ // We found something!
+ var text = markdown.Substring(start, end - start);
+ return new InlineParseResult(new HyperlinkInline { Text = text, Url = "/" + text, LinkType = linkType }, start, end);
+ }
+
+ ///
+ /// Attempts to parse a URL without a scheme e.g. "www.reddit.com".
+ ///
+ /// The markdown text.
+ /// The location of the dot character.
+ /// The location to stop parsing.
+ /// A parsed URL, or null if this is not a URL.
+ internal static InlineParseResult ParsePartialLink(string markdown, int tripPos, int maxEnd)
+ {
+ int start = tripPos - 3;
+ if (start < 0 || markdown[start] != 'w' || markdown[start + 1] != 'w' || markdown[start + 2] != 'w')
+ {
+ return null;
+ }
+
+ // The character before the "www" must be non-alphanumeric i.e. "bwww.reddit.com" is not a valid URL.
+ if (start >= 1 && (char.IsLetterOrDigit(markdown[start - 1]) || markdown[start - 1] == '<'))
+ {
+ return null;
+ }
+
+ // The URL must have at least one character after the www.
+ if (start >= maxEnd - 4)
+ {
+ return null;
+ }
+
+ // Find the end of the URL.
+ int end = FindUrlEnd(markdown, start + 4, maxEnd);
+
+ var url = markdown.Substring(start, end - start);
+ return new InlineParseResult(new HyperlinkInline { Url = "http://" + url, Text = url, LinkType = HyperlinkType.PartialUrl }, start, end);
+ }
+
+ ///
+ /// Attempts to parse an email address e.g. "test@reddit.com".
+ ///
+ /// The markdown text.
+ /// The minimum start position to return.
+ /// The location of the at character.
+ /// The location to stop parsing.
+ /// A parsed URL, or null if this is not a URL.
+ internal static InlineParseResult ParseEmailAddress(string markdown, int minStart, int tripPos, int maxEnd)
+ {
+ // Search backwards until we find a character which is not a letter, digit, or one of
+ // these characters: '+', '-', '_', '.'.
+ // Note: it is intended that this code match the reddit.com markdown parser; there are
+ // many characters which are legal in email addresses but which aren't picked up by
+ // reddit (for example: '$' and '!').
+
+ // Special characters as per https://en.wikipedia.org/wiki/Email_address#Local-part allowed
+ char[] allowedchars = new char[] { '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~' };
+
+ int start = tripPos;
+ while (start > minStart)
+ {
+ char c = markdown[start - 1];
+ if ((c < 'a' || c > 'z') &&
+ (c < 'A' || c > 'Z') &&
+ (c < '0' || c > '9') &&
+ !allowedchars.Contains(c))
+ {
+ break;
+ }
+
+ start--;
+ }
+
+ // There must be at least one character before the '@'.
+ if (start == tripPos)
+ {
+ return null;
+ }
+
+ // Search forwards until we find a character which is not a letter, digit, or one of
+ // these characters: '-', '_'.
+ // Note: it is intended that this code match the reddit.com markdown parser;
+ // technically underscores ('_') aren't allowed in a host name.
+ int dotIndex = tripPos + 1;
+ while (dotIndex < maxEnd)
+ {
+ char c = markdown[dotIndex];
+ if ((c < 'a' || c > 'z') &&
+ (c < 'A' || c > 'Z') &&
+ (c < '0' || c > '9') &&
+ c != '-' && c != '_')
+ {
+ break;
+ }
+
+ dotIndex++;
+ }
+
+ // We are expecting a dot.
+ if (dotIndex == maxEnd || markdown[dotIndex] != '.')
+ {
+ return null;
+ }
+
+ // Search forwards until we find a character which is not a letter, digit, or one of
+ // these characters: '.', '-', '_'.
+ // Note: it is intended that this code match the reddit.com markdown parser;
+ // technically underscores ('_') aren't allowed in a host name.
+ int end = dotIndex + 1;
+ while (end < maxEnd)
+ {
+ char c = markdown[end];
+ if ((c < 'a' || c > 'z') &&
+ (c < 'A' || c > 'Z') &&
+ (c < '0' || c > '9') &&
+ c != '.' && c != '-' && c != '_')
+ {
+ break;
+ }
+
+ end++;
+ }
+
+ // There must be at least one character after the dot.
+ if (end == dotIndex + 1)
+ {
+ return null;
+ }
+
+ // We found an email address!
+ var emailAddress = markdown.Substring(start, end - start);
+ return new InlineParseResult(new HyperlinkInline { Url = "mailto:" + emailAddress, Text = emailAddress, LinkType = HyperlinkType.Email }, start, end);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Text == null)
+ {
+ return base.ToString();
+ }
+
+ return Text;
+ }
+
+ ///
+ /// Finds the next character that is not a letter, digit or underscore in a range.
+ ///
+ /// The markdown text.
+ /// The location to start searching.
+ /// The location to stop searching.
+ /// The location of the next character that is not a letter, digit or underscore.
+ private static int FindEndOfRedditLink(string markdown, int start, int end)
+ {
+ int pos = start;
+ while (pos < markdown.Length && pos < end)
+ {
+ char c = markdown[pos];
+ if ((c < 'a' || c > 'z') &&
+ (c < 'A' || c > 'Z') &&
+ (c < '0' || c > '9') &&
+ c != '_' && c != '/')
+ {
+ return pos;
+ }
+
+ pos++;
+ }
+
+ return end;
+ }
+
+ ///
+ /// Finds the end of a URL.
+ ///
+ /// The markdown text.
+ /// The location to start searching.
+ /// The location to stop searching.
+ /// The location of the end of the URL.
+ private static int FindUrlEnd(string markdown, int start, int maxEnd)
+ {
+ // For some reason a less than character ends a URL...
+ int end = markdown.IndexOfAny(new char[] { ' ', '\t', '\r', '\n', '<' }, start, maxEnd - start);
+ if (end == -1)
+ {
+ end = maxEnd;
+ }
+
+ // URLs can't end on a punctuation character.
+ while (end - 1 > start)
+ {
+ if (Array.IndexOf(new char[] { ')', '}', ']', '!', ';', '.', '?', ',' }, markdown[end - 1]) < 0)
+ {
+ break;
+ }
+
+ end--;
+ }
+
+ return end;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineContainer.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineContainer.cs
new file mode 100644
index 0000000..88fa2e3
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineContainer.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public interface IInlineContainer
+ {
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ IList Inlines { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineLeaf.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineLeaf.cs
new file mode 100644
index 0000000..adeaea0
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/IInlineLeaf.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public interface IInlineLeaf
+ {
+ ///
+ /// Gets or sets the text for this run.
+ ///
+ string Text { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ILinkElement.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ILinkElement.cs
new file mode 100644
index 0000000..5dabbad
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ILinkElement.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Implemented by all inline link elements.
+ ///
+ internal interface ILinkElement
+ {
+ ///
+ /// Gets the link URL. This can be a relative URL, but note that subreddit links will always
+ /// have the leading slash (i.e. the Url will be "/r/baconit" even if the text is
+ /// "r/baconit").
+ ///
+ string Url { get; }
+
+ ///
+ /// Gets a tooltip to display on hover. Can be null .
+ ///
+ string Tooltip { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ImageInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ImageInline.cs
new file mode 100644
index 0000000..bec1687
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ImageInline.cs
@@ -0,0 +1,258 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents an embedded image.
+ ///
+ public class ImageInline : MarkdownInline, IInlineLeaf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ImageInline()
+ : base(MarkdownInlineType.Image)
+ {
+ }
+
+ ///
+ /// Gets or sets the image URL.
+ ///
+ public string Url { get; set; }
+
+ ///
+ /// Gets or sets the image Render URL.
+ ///
+ public string RenderUrl { get; set; }
+
+ ///
+ /// Gets or sets a text to display on hover.
+ ///
+ public string Tooltip { get; set; }
+
+ ///
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the ID of a reference, if this is a reference-style link.
+ ///
+ public string ReferenceId { get; set; }
+
+ ///
+ /// Gets image width
+ /// If value is greater than 0, ImageStretch is set to UniformToFill
+ /// If both ImageWidth and ImageHeight are greater than 0, ImageStretch is set to Fill
+ ///
+ public int ImageWidth { get; internal set; }
+
+ ///
+ /// Gets image height
+ /// If value is greater than 0, ImageStretch is set to UniformToFill
+ /// If both ImageWidth and ImageHeight are greater than 0, ImageStretch is set to Fill
+ ///
+ public int ImageHeight { get; internal set; }
+
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '!', Method = InlineParseMethod.Image });
+ }
+
+ ///
+ /// Attempts to parse an image e.g. "".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed markdown image, or null if this is not a markdown image.
+ internal static InlineParseResult Parse(string markdown, int start, int end)
+ {
+ // Expect a '!' character.
+ if (start >= end || markdown[start] != '!')
+ {
+ return null;
+ }
+
+ int pos = start + 1;
+
+ // Then a '[' character
+ if (pos >= end || markdown[pos] != '[')
+ {
+ return null;
+ }
+
+ pos++;
+
+ // Find the ']' character
+ while (pos < end)
+ {
+ if (markdown[pos] == ']')
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ if (pos == end)
+ {
+ return null;
+ }
+
+ // Extract the alt.
+ string tooltip = markdown.Substring(start + 2, pos - (start + 2));
+
+ // Expect the '(' character.
+ pos++;
+
+ string reference = string.Empty;
+ string url = string.Empty;
+ int imageWidth = 0;
+ int imageHeight = 0;
+
+ if (pos < end && markdown[pos] == '[')
+ {
+ int refstart = pos;
+
+ // Find the reference ']' character
+ while (pos < end)
+ {
+ if (markdown[pos] == ']')
+ {
+ break;
+ }
+
+ pos++;
+ }
+
+ reference = markdown.Substring(refstart + 1, pos - refstart - 1);
+ }
+ else if (pos < end && markdown[pos] == '(')
+ {
+ while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ // Extract the URL.
+ int urlStart = pos;
+ while (pos < end && markdown[pos] != ')')
+ {
+ pos++;
+ }
+
+ var imageDimensionsPos = markdown.IndexOf(" =", urlStart, pos - urlStart, StringComparison.Ordinal);
+
+ url = imageDimensionsPos > 0
+ ? TextRunInline.ResolveEscapeSequences(markdown, urlStart + 1, imageDimensionsPos)
+ : TextRunInline.ResolveEscapeSequences(markdown, urlStart + 1, pos);
+
+ if (imageDimensionsPos > 0)
+ {
+ // trying to find 'x' which separates image width and height
+ var dimensionsSepatorPos = markdown.IndexOf("x", imageDimensionsPos + 2, pos - imageDimensionsPos - 1, StringComparison.Ordinal);
+
+ // didn't find separator, trying to parse value as imageWidth
+ if (dimensionsSepatorPos == -1)
+ {
+ var imageWidthStr = markdown.Substring(imageDimensionsPos + 2, pos - imageDimensionsPos - 2);
+
+ _ = int.TryParse(imageWidthStr, out imageWidth);
+ }
+ else
+ {
+ var dimensions = markdown.Substring(imageDimensionsPos + 2, pos - imageDimensionsPos - 2).Split('x');
+
+ // got width and height
+ if (dimensions.Length == 2)
+ {
+ _ = int.TryParse(dimensions[0], out imageWidth);
+ _ = int.TryParse(dimensions[1], out imageHeight);
+ }
+ }
+ }
+ }
+
+ if (pos == end)
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new ImageInline
+ {
+ Tooltip = tooltip,
+ RenderUrl = url,
+ ReferenceId = reference,
+ Url = url,
+ Text = markdown.Substring(start, pos + 1 - start),
+ ImageWidth = imageWidth,
+ ImageHeight = imageHeight
+ };
+ return new InlineParseResult(result, start, pos + 1);
+ }
+
+ ///
+ /// If this is a reference-style link, attempts to converts it to a regular link.
+ ///
+ /// The document containing the list of references.
+ internal void ResolveReference(MarkdownDocument document)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (ReferenceId == null)
+ {
+ return;
+ }
+
+ // Look up the reference ID.
+ var reference = document.LookUpReference(ReferenceId);
+ if (reference == null)
+ {
+ return;
+ }
+
+ // The reference was found. Check the URL is valid.
+ if (!Common.IsUrlValid(reference.Url))
+ {
+ return;
+ }
+
+ // Everything is cool when you're part of a team.
+ RenderUrl = reference.Url;
+ ReferenceId = null;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (ImageWidth > 0 && ImageHeight > 0)
+ {
+ return $"![{Tooltip}]: {Url} (Width: {ImageWidth}, Height: {ImageHeight})";
+ }
+
+ if (ImageWidth > 0)
+ {
+ return $"![{Tooltip}]: {Url} (Width: {ImageWidth})";
+ }
+
+ if (ImageHeight > 0)
+ {
+ return $"![{Tooltip}]: {Url} (Height: {ImageHeight})";
+ }
+
+ return $"![{Tooltip}]: {Url}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ItalicTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ItalicTextInline.cs
new file mode 100644
index 0000000..fa7a26b
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/ItalicTextInline.cs
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing italic text.
+ ///
+ public class ItalicTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ItalicTextInline()
+ : base(MarkdownInlineType.Italic)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '*', Method = InlineParseMethod.Italic });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '_', Method = InlineParseMethod.Italic });
+ }
+
+ ///
+ /// Attempts to parse a italic text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed italic text span, or null if this is not a italic text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Check the first char.
+ char startChar = markdown[start];
+ if (start == maxEnd || (startChar != '*' && startChar != '_'))
+ {
+ return null;
+ }
+
+ // Find the end of the span. The end character (either '*' or '_') must be the same as
+ // the start character.
+ var innerStart = start + 1;
+ int innerEnd = Common.IndexOf(markdown, startChar, start + 1, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // The first character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]))
+ {
+ return null;
+ }
+
+ // The last character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new ItalicTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, innerEnd + 1);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "*" + string.Join(string.Empty, Inlines) + "*";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/LinkAnchorInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/LinkAnchorInline.cs
new file mode 100644
index 0000000..30310d0
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/LinkAnchorInline.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+ using System.Xml.Linq;
+
+ ///
+ /// Represents a span that contains a reference for links to point to.
+ ///
+ public class LinkAnchorInline : MarkdownInline
+ {
+ internal LinkAnchorInline()
+ : base(MarkdownInlineType.LinkReference)
+ {
+ }
+
+ ///
+ /// Gets or sets the Name of this Link Reference.
+ ///
+ public string Link { get; set; }
+
+ ///
+ /// Gets or sets the raw Link Reference.
+ ///
+ public string Raw { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '<', Method = InlineParseMethod.LinkReference });
+ }
+
+ ///
+ /// Attempts to parse a comment span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed bold text span, or null if this is not a bold text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ if (start >= maxEnd - 1)
+ {
+ return null;
+ }
+
+ // Check the start sequence.
+ string startSequence = markdown.Substring(start, 2);
+ if (startSequence != "')
+ var innerStart = start + 2;
+ int innerEnd = Common.IndexOf(markdown, " ", innerStart, maxEnd);
+ int trueEnd = innerEnd + 4;
+ if (innerEnd == -1)
+ {
+ innerEnd = Common.IndexOf(markdown, "/>", innerStart, maxEnd);
+ trueEnd = innerEnd + 2;
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+ }
+
+ // This link Reference wasn't closed properly if the next link starts before a close.
+ var nextLink = Common.IndexOf(markdown, " -1 && nextLink < innerEnd)
+ {
+ return null;
+ }
+
+ var length = trueEnd - start;
+ var contents = markdown.Substring(start, length);
+
+ string link = null;
+
+ try
+ {
+ var xml = XElement.Parse(contents);
+ var attr = xml.Attribute("name");
+ if (attr != null)
+ {
+ link = attr.Value;
+ }
+ }
+ catch
+ {
+ // Attempting to fetch link failed, ignore and continue.
+ }
+
+ // Remove whitespace if it exists.
+ if (trueEnd + 1 <= maxEnd && markdown[trueEnd] == ' ')
+ {
+ trueEnd += 1;
+ }
+
+ // We found something!
+ var result = new LinkAnchorInline
+ {
+ Raw = contents,
+ Link = link
+ };
+ return new InlineParseResult(result, start, trueEnd);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ return Raw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/MarkdownLinkInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/MarkdownLinkInline.cs
new file mode 100644
index 0000000..c706325
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/MarkdownLinkInline.cs
@@ -0,0 +1,290 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using Microsoft.Toolkit;
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a type of hyperlink where the text can be different from the target URL.
+ ///
+ public class MarkdownLinkInline : MarkdownInline, IInlineContainer, ILinkElement
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MarkdownLinkInline()
+ : base(MarkdownInlineType.MarkdownLink)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Gets or sets the link URL.
+ ///
+ public string Url { get; set; }
+
+ ///
+ /// Gets or sets a tooltip to display on hover.
+ ///
+ public string Tooltip { get; set; }
+
+ ///
+ /// Gets or sets the ID of a reference, if this is a reference-style link.
+ ///
+ public string ReferenceId { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '[', Method = InlineParseMethod.MarkdownLink });
+ }
+
+ ///
+ /// Attempts to parse a markdown link e.g. "[](http://www.reddit.com)".
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed markdown link, or null if this is not a markdown link.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Expect a '[' character.
+ if (start == maxEnd || markdown[start] != '[')
+ {
+ return null;
+ }
+
+ // Find the ']' character, keeping in mind that [test [0-9]](http://www.test.com) is allowed.
+ int linkTextOpen = start + 1;
+ int pos = linkTextOpen;
+ int linkTextClose;
+ int openSquareBracketCount = 0;
+ while (true)
+ {
+ linkTextClose = markdown.IndexOfAny(new char[] { '[', ']' }, pos, maxEnd - pos);
+ if (linkTextClose == -1)
+ {
+ return null;
+ }
+
+ if (markdown[linkTextClose] == '[')
+ {
+ openSquareBracketCount++;
+ }
+ else if (openSquareBracketCount > 0)
+ {
+ openSquareBracketCount--;
+ }
+ else
+ {
+ break;
+ }
+
+ pos = linkTextClose + 1;
+ }
+
+ // Skip whitespace.
+ pos = linkTextClose + 1;
+ while (pos < maxEnd && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
+ {
+ pos++;
+ }
+
+ if (pos == maxEnd)
+ {
+ return null;
+ }
+
+ // Expect the '(' character or the '[' character.
+ int linkOpen = pos;
+ if (markdown[pos] == '(')
+ {
+ // Skip whitespace.
+ linkOpen++;
+ while (linkOpen < maxEnd && ParseHelpers.IsMarkdownWhiteSpace(markdown[linkOpen]))
+ {
+ linkOpen++;
+ }
+
+ // Find the ')' character.
+ pos = linkOpen;
+ int linkClose = -1;
+ var openParenthesis = 0;
+ while (pos < maxEnd)
+ {
+ if (markdown[pos] == ')')
+ {
+ if (openParenthesis == 0)
+ {
+ linkClose = pos;
+ break;
+ }
+ else
+ {
+ openParenthesis--;
+ }
+ }
+
+ if (markdown[pos] == '(')
+ {
+ openParenthesis++;
+ }
+
+ pos++;
+ }
+
+ if (pos >= maxEnd)
+ {
+ return null;
+ }
+
+ int end = linkClose + 1;
+
+ // Skip whitespace backwards.
+ while (linkClose > linkOpen && ParseHelpers.IsMarkdownWhiteSpace(markdown[linkClose - 1]))
+ {
+ linkClose--;
+ }
+
+ // If there is no text whatsoever, then this is not a valid link.
+ if (linkOpen == linkClose)
+ {
+ return null;
+ }
+
+ // Check if there is tooltip text.
+ string url;
+ string tooltip = null;
+ bool lastUrlCharIsDoubleQuote = markdown[linkClose - 1] == '"';
+ int tooltipStart = Common.IndexOf(markdown, " \"", linkOpen, linkClose - 1);
+ if (tooltipStart == linkOpen)
+ {
+ return null;
+ }
+
+ if (lastUrlCharIsDoubleQuote && tooltipStart != -1)
+ {
+ // Extract the URL (resolving any escape sequences).
+ url = TextRunInline.ResolveEscapeSequences(markdown, linkOpen, tooltipStart).TrimEnd(' ', '\t', '\r', '\n');
+ tooltip = markdown.Substring(tooltipStart + 2, (linkClose - 1) - (tooltipStart + 2));
+ }
+ else
+ {
+ // Extract the URL (resolving any escape sequences).
+ url = TextRunInline.ResolveEscapeSequences(markdown, linkOpen, linkClose);
+ }
+
+ // Check the URL is okay.
+ if (!url.IsEmail())
+ {
+ if (!Common.IsUrlValid(url))
+ {
+ return null;
+ }
+ }
+ else
+ {
+ tooltip = url = $"mailto:{url}";
+ }
+
+ // We found a regular stand-alone link.
+ var result = new MarkdownLinkInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, linkTextOpen, linkTextClose, ignoreLinks: true),
+ Url = url.Replace(" ", "%20", StringComparison.Ordinal),
+ Tooltip = tooltip
+ };
+ return new InlineParseResult(result, start, end);
+ }
+ else if (markdown[pos] == '[')
+ {
+ // Find the ']' character.
+ int linkClose = Common.IndexOf(markdown, ']', pos + 1, maxEnd);
+ if (linkClose == -1)
+ {
+ return null;
+ }
+
+ // We found a reference-style link.
+ var result = new MarkdownLinkInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, linkTextOpen, linkTextClose, ignoreLinks: true),
+ ReferenceId = markdown.Substring(linkOpen + 1, linkClose - (linkOpen + 1))
+ };
+ if (string.IsNullOrEmpty(result.ReferenceId))
+ {
+ result.ReferenceId = markdown.Substring(linkTextOpen, linkTextClose - linkTextOpen);
+ }
+
+ return new InlineParseResult(result, start, linkClose + 1);
+ }
+
+ return null;
+ }
+
+ ///
+ /// If this is a reference-style link, attempts to converts it to a regular link.
+ ///
+ /// The document containing the list of references.
+ internal void ResolveReference(MarkdownDocument document)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (ReferenceId == null)
+ {
+ return;
+ }
+
+ // Look up the reference ID.
+ var reference = document.LookUpReference(ReferenceId);
+ if (reference == null)
+ {
+ return;
+ }
+
+ // The reference was found. Check the URL is valid.
+ if (!Common.IsUrlValid(reference.Url))
+ {
+ return;
+ }
+
+ // Everything is cool when you're part of a team.
+ Url = reference.Url;
+ Tooltip = reference.Tooltip;
+ ReferenceId = null;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null || Url == null)
+ {
+ return base.ToString();
+ }
+
+ if (ReferenceId != null)
+ {
+ return $"[{string.Join(string.Empty, Inlines)}][{ReferenceId}]";
+ }
+
+ return $"[{string.Join(string.Empty, Inlines)}]({Url})";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/StrikethroughTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/StrikethroughTextInline.cs
new file mode 100644
index 0000000..e997194
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/StrikethroughTextInline.cs
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing strikethrough text.
+ ///
+ public class StrikethroughTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StrikethroughTextInline()
+ : base(MarkdownInlineType.Strikethrough)
+ {
+ }
+
+ ///
+ /// Gets or sets The contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '~', Method = InlineParseMethod.Strikethrough });
+ }
+
+ ///
+ /// Attempts to parse a strikethrough text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed strikethrough text span, or null if this is not a strikethrough text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Check the start sequence.
+ if (start >= maxEnd - 1 || markdown.Substring(start, 2) != "~~")
+ {
+ return null;
+ }
+
+ // Find the end of the span.
+ var innerStart = start + 2;
+ int innerEnd = Common.IndexOf(markdown, "~~", innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ // The span must contain at least one character.
+ if (innerStart == innerEnd)
+ {
+ return null;
+ }
+
+ // The first character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]))
+ {
+ return null;
+ }
+
+ // The last character inside the span must NOT be a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new StrikethroughTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, innerEnd + 2);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "~~" + string.Join(string.Empty, Inlines) + "~~";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SubscriptTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SubscriptTextInline.cs
new file mode 100644
index 0000000..c20d79c
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SubscriptTextInline.cs
@@ -0,0 +1,94 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing subscript text.
+ ///
+ public class SubscriptTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SubscriptTextInline()
+ : base(MarkdownInlineType.Subscript)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '<', Method = InlineParseMethod.Subscript });
+ }
+
+ ///
+ /// Attempts to parse a subscript text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed subscript text span, or null if this is not a subscript text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Check the first character.
+ // e.g. "…… "
+ if (maxEnd - start < 5)
+ {
+ return null;
+ }
+
+ if (markdown.Substring(start, 5) != "")
+ {
+ return null;
+ }
+
+ int innerStart = start + 5;
+ int innerEnd = Common.IndexOf(markdown, " ", innerStart, maxEnd);
+
+ // if don't have the end character or no character between start and end
+ if (innerEnd == -1 || innerEnd == innerStart)
+ {
+ return null;
+ }
+
+ // No match if the character after the caret is a space.
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]) || ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ var result = new SubscriptTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, innerEnd + 6);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "" + string.Join(string.Empty, Inlines) + " ";
+ }
+ }
+}
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SuperscriptTextInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SuperscriptTextInline.cs
new file mode 100644
index 0000000..d9c4ba8
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/SuperscriptTextInline.cs
@@ -0,0 +1,150 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a span containing superscript text.
+ ///
+ public class SuperscriptTextInline : MarkdownInline, IInlineContainer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SuperscriptTextInline()
+ : base(MarkdownInlineType.Superscript)
+ {
+ }
+
+ ///
+ /// Gets or sets the contents of the inline.
+ ///
+ public IList Inlines { get; set; }
+
+ ///
+ /// Returns the chars that if found means we might have a match.
+ ///
+ internal static void AddTripChars(List tripCharHelpers)
+ {
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '^', Method = InlineParseMethod.Superscript });
+ tripCharHelpers.Add(new InlineTripCharHelper() { FirstChar = '<', Method = InlineParseMethod.Superscript });
+ }
+
+ ///
+ /// Attempts to parse a superscript text span.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed superscript text span, or null if this is not a superscript text span.
+ internal static InlineParseResult Parse(string markdown, int start, int maxEnd)
+ {
+ // Check the first character.
+ bool isHTMLSequence = false;
+ if (start == maxEnd || (markdown[start] != '^' && markdown[start] != '<'))
+ {
+ return null;
+ }
+
+ if (markdown[start] != '^')
+ {
+ if (maxEnd - start < 5)
+ {
+ return null;
+ }
+ else if (markdown.Substring(start, 5) != "")
+ {
+ return null;
+ }
+ else
+ {
+ isHTMLSequence = true;
+ }
+ }
+
+ if (isHTMLSequence)
+ {
+ int innerStart = start + 5;
+ int innerEnd, end;
+ innerEnd = Common.IndexOf(markdown, " ", innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ if (innerEnd == innerStart)
+ {
+ return null;
+ }
+
+ if (ParseHelpers.IsMarkdownWhiteSpace(markdown[innerStart]) || ParseHelpers.IsMarkdownWhiteSpace(markdown[innerEnd - 1]))
+ {
+ return null;
+ }
+
+ // We found something!
+ end = innerEnd + 6;
+ var result = new SuperscriptTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, end);
+ }
+ else
+ {
+ // The content might be enclosed in parentheses.
+ int innerStart = start + 1;
+ int innerEnd, end;
+ if (innerStart < maxEnd && markdown[innerStart] == '(')
+ {
+ // Find the end parenthesis.
+ innerStart++;
+ innerEnd = Common.IndexOf(markdown, ')', innerStart, maxEnd);
+ if (innerEnd == -1)
+ {
+ return null;
+ }
+
+ end = innerEnd + 1;
+ }
+ else
+ {
+ // Search for the next whitespace character.
+ innerEnd = Common.FindNextWhiteSpace(markdown, innerStart, maxEnd, ifNotFoundReturnLength: true);
+ if (innerEnd == innerStart)
+ {
+ // No match if the character after the caret is a space.
+ return null;
+ }
+
+ end = innerEnd;
+ }
+
+ // We found something!
+ var result = new SuperscriptTextInline
+ {
+ Inlines = Common.ParseInlineChildren(markdown, innerStart, innerEnd)
+ };
+ return new InlineParseResult(result, start, end);
+ }
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Inlines == null)
+ {
+ return base.ToString();
+ }
+
+ return "^(" + string.Join(string.Empty, Inlines) + ")";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/TextRunInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/TextRunInline.cs
new file mode 100644
index 0000000..786c9da
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Inlines/TextRunInline.cs
@@ -0,0 +1,470 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Inlines
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ ///
+ /// Represents a span containing plain text.
+ ///
+ public class TextRunInline : MarkdownInline, IInlineLeaf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TextRunInline()
+ : base(MarkdownInlineType.TextRun)
+ {
+ }
+
+ ///
+ /// Gets or sets the text for this run.
+ ///
+ public string Text { get; set; }
+
+ // A list of supported HTML entity names, along with their corresponding code points.
+ private static readonly Dictionary _entities = new Dictionary
+ {
+ { "quot", 0x0022 }, // "
+ { "amp", 0x0026 }, // &
+ { "apos", 0x0027 }, // '
+ { "lt", 0x003C }, // <
+ { "gt", 0x003E }, // >
+ { "nbsp", 0x00A0 }, //
+ { "#160", 0x00A0 }, // ?
+ { "iexcl", 0x00A1 }, // ¡
+ { "cent", 0x00A2 }, // ¢
+ { "pound", 0x00A3 }, // £
+ { "curren", 0x00A4 }, // ¤
+ { "yen", 0x00A5 }, // ¥
+ { "brvbar", 0x00A6 }, // ¦
+ { "sect", 0x00A7 }, // §
+ { "uml", 0x00A8 }, // ¨
+ { "copy", 0x00A9 }, // ©
+ { "ordf", 0x00AA }, // ª
+ { "laquo", 0x00AB }, // «
+ { "not", 0x00AC }, // ¬
+ { "shy", 0x00AD }, // ?
+ { "reg", 0x00AE }, // ®
+ { "macr", 0x00AF }, // ¯
+ { "deg", 0x00B0 }, // °
+ { "plusmn", 0x00B1 }, // ±
+ { "sup2", 0x00B2 }, // ²
+ { "sup3", 0x00B3 }, // ³
+ { "acute", 0x00B4 }, // ´
+ { "micro", 0x00B5 }, // µ
+ { "para", 0x00B6 }, // ¶
+ { "middot", 0x00B7 }, // ·
+ { "cedil", 0x00B8 }, // ¸
+ { "sup1", 0x00B9 }, // ¹
+ { "ordm", 0x00BA }, // º
+ { "raquo", 0x00BB }, // »
+ { "frac14", 0x00BC }, // ¼
+ { "frac12", 0x00BD }, // ½
+ { "frac34", 0x00BE }, // ¾
+ { "iquest", 0x00BF }, // ¿
+ { "Agrave", 0x00C0 }, // À
+ { "Aacute", 0x00C1 }, // Á
+ { "Acirc", 0x00C2 }, // Â
+ { "Atilde", 0x00C3 }, // Ã
+ { "Auml", 0x00C4 }, // Ä
+ { "Aring", 0x00C5 }, // Å
+ { "AElig", 0x00C6 }, // Æ
+ { "Ccedil", 0x00C7 }, // Ç
+ { "Egrave", 0x00C8 }, // È
+ { "Eacute", 0x00C9 }, // É
+ { "Ecirc", 0x00CA }, // Ê
+ { "Euml", 0x00CB }, // Ë
+ { "Igrave", 0x00CC }, // Ì
+ { "Iacute", 0x00CD }, // Í
+ { "Icirc", 0x00CE }, // Î
+ { "Iuml", 0x00CF }, // Ï
+ { "ETH", 0x00D0 }, // Ð
+ { "Ntilde", 0x00D1 }, // Ñ
+ { "Ograve", 0x00D2 }, // Ò
+ { "Oacute", 0x00D3 }, // Ó
+ { "Ocirc", 0x00D4 }, // Ô
+ { "Otilde", 0x00D5 }, // Õ
+ { "Ouml", 0x00D6 }, // Ö
+ { "times", 0x00D7 }, // ×
+ { "Oslash", 0x00D8 }, // Ø
+ { "Ugrave", 0x00D9 }, // Ù
+ { "Uacute", 0x00DA }, // Ú
+ { "Ucirc", 0x00DB }, // Û
+ { "Uuml", 0x00DC }, // Ü
+ { "Yacute", 0x00DD }, // Ý
+ { "THORN", 0x00DE }, // Þ
+ { "szlig", 0x00DF }, // ß
+ { "agrave", 0x00E0 }, // à
+ { "aacute", 0x00E1 }, // á
+ { "acirc", 0x00E2 }, // â
+ { "atilde", 0x00E3 }, // ã
+ { "auml", 0x00E4 }, // ä
+ { "aring", 0x00E5 }, // å
+ { "aelig", 0x00E6 }, // æ
+ { "ccedil", 0x00E7 }, // ç
+ { "egrave", 0x00E8 }, // è
+ { "eacute", 0x00E9 }, // é
+ { "ecirc", 0x00EA }, // ê
+ { "euml", 0x00EB }, // ë
+ { "igrave", 0x00EC }, // ì
+ { "iacute", 0x00ED }, // í
+ { "icirc", 0x00EE }, // î
+ { "iuml", 0x00EF }, // ï
+ { "eth", 0x00F0 }, // ð
+ { "ntilde", 0x00F1 }, // ñ
+ { "ograve", 0x00F2 }, // ò
+ { "oacute", 0x00F3 }, // ó
+ { "ocirc", 0x00F4 }, // ô
+ { "otilde", 0x00F5 }, // õ
+ { "ouml", 0x00F6 }, // ö
+ { "divide", 0x00F7 }, // ÷
+ { "oslash", 0x00F8 }, // ø
+ { "ugrave", 0x00F9 }, // ù
+ { "uacute", 0x00FA }, // ú
+ { "ucirc", 0x00FB }, // û
+ { "uuml", 0x00FC }, // ü
+ { "yacute", 0x00FD }, // ý
+ { "thorn", 0x00FE }, // þ
+ { "yuml", 0x00FF }, // ÿ
+ { "OElig", 0x0152 }, // Œ
+ { "oelig", 0x0153 }, // œ
+ { "Scaron", 0x0160 }, // Š
+ { "scaron", 0x0161 }, // š
+ { "Yuml", 0x0178 }, // Ÿ
+ { "fnof", 0x0192 }, // ƒ
+ { "circ", 0x02C6 }, // ˆ
+ { "tilde", 0x02DC }, // ˜
+ { "Alpha", 0x0391 }, // Α
+ { "Beta", 0x0392 }, // Β
+ { "Gamma", 0x0393 }, // Γ
+ { "Delta", 0x0394 }, // Δ
+ { "Epsilon", 0x0395 }, // Ε
+ { "Zeta", 0x0396 }, // Ζ
+ { "Eta", 0x0397 }, // Η
+ { "Theta", 0x0398 }, // Θ
+ { "Iota", 0x0399 }, // Ι
+ { "Kappa", 0x039A }, // Κ
+ { "Lambda", 0x039B }, // Λ
+ { "Mu", 0x039C }, // Μ
+ { "Nu", 0x039D }, // Ν
+ { "Xi", 0x039E }, // Ξ
+ { "Omicron", 0x039F }, // Ο
+ { "Pi", 0x03A0 }, // Π
+ { "Rho", 0x03A1 }, // Ρ
+ { "Sigma", 0x03A3 }, // Σ
+ { "Tau", 0x03A4 }, // Τ
+ { "Upsilon", 0x03A5 }, // Υ
+ { "Phi", 0x03A6 }, // Φ
+ { "Chi", 0x03A7 }, // Χ
+ { "Psi", 0x03A8 }, // Ψ
+ { "Omega", 0x03A9 }, // Ω
+ { "alpha", 0x03B1 }, // α
+ { "beta", 0x03B2 }, // β
+ { "gamma", 0x03B3 }, // γ
+ { "delta", 0x03B4 }, // δ
+ { "epsilon", 0x03B5 }, // ε
+ { "zeta", 0x03B6 }, // ζ
+ { "eta", 0x03B7 }, // η
+ { "theta", 0x03B8 }, // θ
+ { "iota", 0x03B9 }, // ι
+ { "kappa", 0x03BA }, // κ
+ { "lambda", 0x03BB }, // λ
+ { "mu", 0x03BC }, // μ
+ { "nu", 0x03BD }, // ν
+ { "xi", 0x03BE }, // ξ
+ { "omicron", 0x03BF }, // ο
+ { "pi", 0x03C0 }, // π
+ { "rho", 0x03C1 }, // ρ
+ { "sigmaf", 0x03C2 }, // ς
+ { "sigma", 0x03C3 }, // σ
+ { "tau", 0x03C4 }, // τ
+ { "upsilon", 0x03C5 }, // υ
+ { "phi", 0x03C6 }, // φ
+ { "chi", 0x03C7 }, // χ
+ { "psi", 0x03C8 }, // ψ
+ { "omega", 0x03C9 }, // ω
+ { "thetasym", 0x03D1 }, // ϑ
+ { "upsih", 0x03D2 }, // ϒ
+ { "piv", 0x03D6 }, // ϖ
+ { "ensp", 0x2002 }, // ?
+ { "emsp", 0x2003 }, // ?
+ { "thinsp", 0x2009 }, // ?
+ { "zwnj", 0x200C }, // ?
+ { "zwj", 0x200D }, // ?
+ { "lrm", 0x200E }, // ?
+ { "rlm", 0x200F }, // ?
+ { "ndash", 0x2013 }, // –
+ { "mdash", 0x2014 }, // —
+ { "lsquo", 0x2018 }, // ‘
+ { "rsquo", 0x2019 }, // ’
+ { "sbquo", 0x201A }, // ‚
+ { "ldquo", 0x201C }, // “
+ { "rdquo", 0x201D }, // ”
+ { "bdquo", 0x201E }, // „
+ { "dagger", 0x2020 }, // †
+ { "Dagger", 0x2021 }, // ‡
+ { "bull", 0x2022 }, // •
+ { "hellip", 0x2026 }, // …
+ { "permil", 0x2030 }, // ‰
+ { "prime", 0x2032 }, // ′
+ { "Prime", 0x2033 }, // ″
+ { "lsaquo", 0x2039 }, // ‹
+ { "rsaquo", 0x203A }, // ›
+ { "oline", 0x203E }, // ‾
+ { "frasl", 0x2044 }, // ⁄
+ { "euro", 0x20AC }, // €
+ { "image", 0x2111 }, // ℑ
+ { "weierp", 0x2118 }, // ℘
+ { "real", 0x211C }, // ℜ
+ { "trade", 0x2122 }, // ™
+ { "alefsym", 0x2135 }, // ℵ
+ { "larr", 0x2190 }, // ←
+ { "uarr", 0x2191 }, // ↑
+ { "rarr", 0x2192 }, // →
+ { "darr", 0x2193 }, // ↓
+ { "harr", 0x2194 }, // ↔
+ { "crarr", 0x21B5 }, // ↵
+ { "lArr", 0x21D0 }, // ⇐
+ { "uArr", 0x21D1 }, // ⇑
+ { "rArr", 0x21D2 }, // ⇒
+ { "dArr", 0x21D3 }, // ⇓
+ { "hArr", 0x21D4 }, // ⇔
+ { "forall", 0x2200 }, // ∀
+ { "part", 0x2202 }, // ∂
+ { "exist", 0x2203 }, // ∃
+ { "empty", 0x2205 }, // ∅
+ { "nabla", 0x2207 }, // ∇
+ { "isin", 0x2208 }, // ∈
+ { "notin", 0x2209 }, // ∉
+ { "ni", 0x220B }, // ∋
+ { "prod", 0x220F }, // ∏
+ { "sum", 0x2211 }, // ∑
+ { "minus", 0x2212 }, // −
+ { "lowast", 0x2217 }, // ∗
+ { "radic", 0x221A }, // √
+ { "prop", 0x221D }, // ∝
+ { "infin", 0x221E }, // ∞
+ { "ang", 0x2220 }, // ∠
+ { "and", 0x2227 }, // ∧
+ { "or", 0x2228 }, // ∨
+ { "cap", 0x2229 }, // ∩
+ { "cup", 0x222A }, // ∪
+ { "int", 0x222B }, // ∫
+ { "there4", 0x2234 }, // ∴
+ { "sim", 0x223C }, // ∼
+ { "cong", 0x2245 }, // ≅
+ { "asymp", 0x2248 }, // ≈
+ { "ne", 0x2260 }, // ≠
+ { "equiv", 0x2261 }, // ≡
+ { "le", 0x2264 }, // ≤
+ { "ge", 0x2265 }, // ≥
+ { "sub", 0x2282 }, // ⊂
+ { "sup", 0x2283 }, // ⊃
+ { "nsub", 0x2284 }, // ⊄
+ { "sube", 0x2286 }, // ⊆
+ { "supe", 0x2287 }, // ⊇
+ { "oplus", 0x2295 }, // ⊕
+ { "otimes", 0x2297 }, // ⊗
+ { "perp", 0x22A5 }, // ⊥
+ { "sdot", 0x22C5 }, // ⋅
+ { "lceil", 0x2308 }, // ⌈
+ { "rceil", 0x2309 }, // ⌉
+ { "lfloor", 0x230A }, // ⌊
+ { "rfloor", 0x230B }, // ⌋
+ { "lang", 0x2329 }, // 〈
+ { "rang", 0x232A }, // 〉
+ { "loz", 0x25CA }, // ◊
+ { "spades", 0x2660 }, // ♠
+ { "clubs", 0x2663 }, // ♣
+ { "hearts", 0x2665 }, // ♥
+ { "diams", 0x2666 }, // ♦
+ };
+
+ // A list of characters that can be escaped.
+ private static readonly char[] _escapeCharacters = new char[] { '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '|', '~', '^', '&', ':', '<', '>', '/' };
+
+ ///
+ /// Parses unformatted text.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed text span.
+ internal static TextRunInline Parse(string markdown, int start, int end)
+ {
+ // Handle escape sequences and entities.
+ // Note: this code is designed to be as fast as possible in the case where there are no
+ // escape sequences and no entities (expected to be the common case).
+ StringBuilder result = null;
+ int textPos = start;
+ int searchPos = start;
+ while (searchPos < end)
+ {
+ // Look for the next backslash.
+ int sequenceStartIndex = markdown.IndexOfAny(new char[] { '\\', '&' }, searchPos, end - searchPos);
+ if (sequenceStartIndex == -1)
+ {
+ break;
+ }
+
+ searchPos = sequenceStartIndex + 1;
+
+ char decodedChar;
+ if (markdown[sequenceStartIndex] == '\\')
+ {
+ // This is an escape sequence, with one more character expected.
+ if (sequenceStartIndex >= end - 1)
+ {
+ break;
+ }
+
+ // Check if the character after the backslash can be escaped.
+ decodedChar = markdown[sequenceStartIndex + 1];
+ if (Array.IndexOf(_escapeCharacters, decodedChar) < 0)
+ {
+ // This character cannot be escaped.
+ continue;
+ }
+
+ // This here's an escape sequence!
+ if (result == null)
+ {
+ result = new StringBuilder(end - start);
+ }
+
+ result.Append(markdown.Substring(textPos, sequenceStartIndex - textPos));
+ result.Append(decodedChar);
+ searchPos = textPos = sequenceStartIndex + 2;
+ }
+ else if (markdown[sequenceStartIndex] == '&')
+ {
+ // This is an entity e.g. " ".
+
+ // Look for the semicolon.
+ int semicolonIndex = markdown.IndexOf(';', sequenceStartIndex + 1, end - (sequenceStartIndex + 1));
+
+ // Unterminated entity.
+ if (semicolonIndex == -1)
+ {
+ continue;
+ }
+
+ // Okay, we have an entity, but is it one we recognize?
+ string entityName = markdown.Substring(sequenceStartIndex + 1, semicolonIndex - (sequenceStartIndex + 1));
+
+ // Unrecognized entity.
+ if (_entities.ContainsKey(entityName) == false)
+ {
+ continue;
+ }
+
+ // This here's an escape sequence!
+ if (result == null)
+ {
+ result = new StringBuilder(end - start);
+ }
+
+ result.Append(markdown.Substring(textPos, sequenceStartIndex - textPos));
+ result.Append((char)_entities[entityName]);
+ searchPos = textPos = semicolonIndex + 1;
+ }
+ }
+
+ if (result != null)
+ {
+ result.Append(markdown.Substring(textPos, end - textPos));
+ return new TextRunInline { Text = result.ToString() };
+ }
+
+ var length = end - start;
+
+ // HACK: in case end < start happens
+ if (length <= 0)
+ {
+ return new TextRunInline { Text = string.Empty };
+ }
+
+ return new TextRunInline { Text = markdown.Substring(start, length) };
+ }
+
+ ///
+ /// Parses unformatted text.
+ ///
+ /// The markdown text.
+ /// The location to start parsing.
+ /// The location to stop parsing.
+ /// A parsed text span.
+ internal static string ResolveEscapeSequences(string markdown, int start, int end)
+ {
+ // Handle escape sequences only.
+ // Note: this code is designed to be as fast as possible in the case where there are no
+ // escape sequences (expected to be the common case).
+ StringBuilder result = null;
+ int textPos = start;
+ int searchPos = start;
+ while (searchPos < end)
+ {
+ // Look for the next backslash.
+ int sequenceStartIndex = markdown.IndexOf('\\', searchPos, end - searchPos);
+ if (sequenceStartIndex == -1)
+ {
+ break;
+ }
+
+ searchPos = sequenceStartIndex + 1;
+
+ // This is an escape sequence, with one more character expected.
+ if (sequenceStartIndex >= end - 1)
+ {
+ break;
+ }
+
+ // Check if the character after the backslash can be escaped.
+ char decodedChar = markdown[sequenceStartIndex + 1];
+ if (Array.IndexOf(_escapeCharacters, decodedChar) < 0)
+ {
+ // This character cannot be escaped.
+ continue;
+ }
+
+ // This here's an escape sequence!
+ if (result == null)
+ {
+ result = new StringBuilder(end - start);
+ }
+
+ result.Append(markdown.Substring(textPos, sequenceStartIndex - textPos));
+ result.Append(decodedChar);
+ searchPos = textPos = sequenceStartIndex + 2;
+ }
+
+ if (result != null)
+ {
+ result.Append(markdown.Substring(textPos, end - textPos));
+ return result.ToString();
+ }
+
+ return markdown.Substring(start, end - start);
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Text == null)
+ {
+ return base.ToString();
+ }
+
+ return Text;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownBlock.cs
new file mode 100644
index 0000000..2b5d334
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownBlock.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// A Block Element is an element that is a container for other structures.
+ ///
+ public abstract class MarkdownBlock : MarkdownElement
+ {
+ ///
+ /// Gets or sets tells us what type this element is.
+ ///
+ public MarkdownBlockType Type { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal MarkdownBlock(MarkdownBlockType type)
+ {
+ Type = type;
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// true if the specified object is equal to the current object; otherwise, false.
+ public override bool Equals(object obj)
+ {
+ if (!base.Equals(obj) || !(obj is MarkdownBlock))
+ {
+ return false;
+ }
+
+ return Type == ((MarkdownBlock)obj).Type;
+ }
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ public override int GetHashCode()
+ {
+ return base.GetHashCode() ^ Type.GetHashCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownDocument.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownDocument.cs
new file mode 100644
index 0000000..934780a
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownDocument.cs
@@ -0,0 +1,417 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ ///
+ /// Represents a Markdown Document.
+ /// Initialize an instance and call to parse the Raw Markdown Text.
+ ///
+ public class MarkdownDocument : MarkdownBlock
+ {
+ ///
+ /// Gets a list of URL schemes.
+ ///
+ public static List KnownSchemes { get; private set; } = new List()
+ {
+ "http",
+ "https",
+ "ftp",
+ "steam",
+ "irc",
+ "news",
+ "mumble",
+ "ssh",
+ "ms-windows-store",
+ "sip"
+ };
+
+ private Dictionary _references;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MarkdownDocument()
+ : base(MarkdownBlockType.Root)
+ {
+ }
+
+ ///
+ /// Gets or sets the list of block elements.
+ ///
+ public IList Blocks { get; set; }
+
+ ///
+ /// Parses markdown document text.
+ ///
+ /// The markdown text.
+ public void Parse(string markdownText)
+ {
+ Blocks = Parse(markdownText, 0, markdownText.Length, quoteDepth: 0, actualEnd: out _);
+
+ // Remove any references from the list of blocks, and add them to a dictionary.
+ for (int i = Blocks.Count - 1; i >= 0; i--)
+ {
+ if (Blocks[i].Type == MarkdownBlockType.LinkReference)
+ {
+ var reference = (LinkReferenceBlock)Blocks[i];
+ if (_references == null)
+ {
+ _references = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!_references.ContainsKey(reference.Id))
+ {
+ _references.Add(reference.Id, reference);
+ }
+
+ Blocks.RemoveAt(i);
+ }
+ }
+ }
+
+ ///
+ /// Parses a markdown document.
+ ///
+ /// The markdown text.
+ /// The position to start parsing.
+ /// The position to stop parsing.
+ /// The current nesting level for block quoting.
+ /// Set to the position at which parsing ended. This can be
+ /// different from when the parser is being called recursively.
+ ///
+ /// A list of parsed blocks.
+ internal static List Parse(string markdown, int start, int end, int quoteDepth, out int actualEnd)
+ {
+ // We need to parse out the list of blocks.
+ // Some blocks need to start on a new paragraph (code, lists and tables) while other
+ // blocks can start on any line (headers, horizontal rules and quotes).
+ // Text that is outside of any other block becomes a paragraph.
+ var blocks = new List();
+ int startOfLine = start;
+ bool lineStartsNewParagraph = true;
+ var paragraphText = new StringBuilder();
+
+ // These are needed to parse underline-style header blocks.
+ int previousRealtStartOfLine = start;
+ int previousStartOfLine = start;
+ int previousEndOfLine = start;
+
+ // Go line by line.
+ while (startOfLine < end)
+ {
+ // Find the first non-whitespace character.
+ int nonSpacePos = startOfLine;
+ char nonSpaceChar = '\0';
+ int realStartOfLine = startOfLine; // i.e. including quotes.
+ int expectedQuotesRemaining = quoteDepth;
+ while (true)
+ {
+ while (nonSpacePos < end)
+ {
+ char c = markdown[nonSpacePos];
+ if (c == '\r' || c == '\n')
+ {
+ // The line is either entirely whitespace, or is empty.
+ break;
+ }
+
+ if (c != ' ' && c != '\t')
+ {
+ // The line has content.
+ nonSpaceChar = c;
+ break;
+ }
+
+ nonSpacePos++;
+ }
+
+ // When parsing blocks in a blockquote context, we need to count the number of
+ // quote characters ('>'). If there are less than expected AND this is the
+ // start of a new paragraph, then stop parsing.
+ if (expectedQuotesRemaining == 0)
+ {
+ break;
+ }
+
+ if (nonSpaceChar == '>')
+ {
+ // Expected block quote characters should be ignored.
+ expectedQuotesRemaining--;
+ nonSpacePos++;
+ nonSpaceChar = '\0';
+ startOfLine = nonSpacePos;
+
+ // Ignore the first space after the quote character, if there is one.
+ if (startOfLine < end && markdown[startOfLine] == ' ')
+ {
+ startOfLine++;
+ nonSpacePos++;
+ }
+ }
+ else
+ {
+ int lastIndentation = 0;
+ string lastline = null;
+
+ // Determines how many Quote levels were in the last line.
+ if (realStartOfLine > 0)
+ {
+ lastline = markdown.Substring(previousRealtStartOfLine, previousEndOfLine - previousRealtStartOfLine);
+ lastIndentation = lastline.Count(c => c == '>');
+ }
+
+ var currentEndOfLine = Common.FindNextSingleNewLine(markdown, nonSpacePos, end, out _);
+ var currentline = markdown.Substring(realStartOfLine, currentEndOfLine - realStartOfLine);
+ var currentIndentation = currentline.Count(c => c == '>');
+ var firstChar = markdown[realStartOfLine];
+
+ // This is a quote that doesn't start with a Quote marker, but carries on from the last line.
+ if (lastIndentation == 1)
+ {
+ if (nonSpaceChar != '\0' && firstChar != '>')
+ {
+ break;
+ }
+ }
+
+ // Collapse down a level of quotes if the current indentation is greater than the last indentation.
+ // Only if the last indentation is greater than 1, and the current indentation is greater than 0
+ if (lastIndentation > 1 && currentIndentation > 0 && currentIndentation < lastIndentation)
+ {
+ break;
+ }
+
+ // This must be the end of the blockquote. End the current paragraph, if any.
+ actualEnd = realStartOfLine;
+
+ if (paragraphText.Length > 0)
+ {
+ blocks.Add(ParagraphBlock.Parse(paragraphText.ToString()));
+ }
+
+ return blocks;
+ }
+ }
+
+ // Find the end of the current line.
+ int endOfLine = Common.FindNextSingleNewLine(markdown, nonSpacePos, end, out int startOfNextLine);
+
+ if (nonSpaceChar == '\0')
+ {
+ // The line is empty or nothing but whitespace.
+ lineStartsNewParagraph = true;
+
+ // End the current paragraph.
+ if (paragraphText.Length > 0)
+ {
+ blocks.Add(ParagraphBlock.Parse(paragraphText.ToString()));
+ paragraphText.Clear();
+ }
+ }
+ else
+ {
+ // This is a header if the line starts with a hash character,
+ // or if the line starts with '-' or a '=' character and has no other characters.
+ // Or a quote if the line starts with a greater than character (optionally preceded by whitespace).
+ // Or a horizontal rule if the line contains nothing but 3 '*', '-' or '_' characters (with optional whitespace).
+ MarkdownBlock newBlockElement = null;
+ if (nonSpaceChar == '-' && nonSpacePos == startOfLine)
+ {
+ // Yaml Header
+ newBlockElement = YamlHeaderBlock.Parse(markdown, startOfLine, markdown.Length, out startOfLine);
+ if (newBlockElement != null)
+ {
+ realStartOfLine = startOfLine;
+ endOfLine = startOfLine + 3;
+ startOfNextLine = Common.FindNextSingleNewLine(markdown, startOfLine, end, out startOfNextLine);
+
+ paragraphText.Clear();
+ }
+ }
+
+ if (newBlockElement == null && nonSpaceChar == '#' && nonSpacePos == startOfLine)
+ {
+ // Hash-prefixed header.
+ newBlockElement = HeaderBlock.ParseHashPrefixedHeader(markdown, startOfLine, endOfLine);
+ }
+ else if ((nonSpaceChar == '-' || nonSpaceChar == '=') && nonSpacePos == startOfLine && paragraphText.Length > 0)
+ {
+ // Underline style header. These are weird because you don't know you've
+ // got one until you've gone past it.
+ // Note: we intentionally deviate from reddit here in that we only
+ // recognize this type of header if the previous line is part of a
+ // paragraph. For example if you have this, the header at the bottom is
+ // ignored:
+ // a|b
+ // -|-
+ // 1|2
+ // ===
+ newBlockElement = HeaderBlock.ParseUnderlineStyleHeader(markdown, previousStartOfLine, previousEndOfLine, startOfLine, endOfLine);
+
+ if (newBlockElement != null)
+ {
+ // We're going to have to remove the header text from the pending
+ // paragraph by prematurely ending the current paragraph.
+ // We already made sure that there is a paragraph in progress.
+ paragraphText.Length -= (previousEndOfLine - previousStartOfLine);
+ }
+ }
+
+ // These characters overlap with the underline-style header - this check should go after that one.
+ if (newBlockElement == null && (nonSpaceChar == '*' || nonSpaceChar == '-' || nonSpaceChar == '_'))
+ {
+ newBlockElement = HorizontalRuleBlock.Parse(markdown, startOfLine, endOfLine);
+ }
+
+ if (newBlockElement == null && lineStartsNewParagraph)
+ {
+ // Some block elements must start on a new paragraph (tables, lists and code).
+ int endOfBlock = startOfNextLine;
+ if (nonSpaceChar == '*' || nonSpaceChar == '+' || nonSpaceChar == '-' || (nonSpaceChar >= '0' && nonSpaceChar <= '9'))
+ {
+ newBlockElement = ListBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out endOfBlock);
+ }
+
+ if (newBlockElement == null && (nonSpacePos > startOfLine || nonSpaceChar == '`'))
+ {
+ newBlockElement = CodeBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out endOfBlock);
+ }
+
+ if (newBlockElement == null)
+ {
+ newBlockElement = TableBlock.Parse(markdown, realStartOfLine, endOfLine, end, quoteDepth, out endOfBlock);
+ }
+
+ if (newBlockElement != null)
+ {
+ startOfNextLine = endOfBlock;
+ }
+ }
+
+ // This check needs to go after the code block check.
+ if (newBlockElement == null && nonSpaceChar == '>')
+ {
+ newBlockElement = QuoteBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out startOfNextLine);
+ }
+
+ // This check needs to go after the code block check.
+ if (newBlockElement == null && nonSpaceChar == '[')
+ {
+ newBlockElement = LinkReferenceBlock.Parse(markdown, startOfLine, endOfLine);
+ }
+
+ // Block elements start new paragraphs.
+ lineStartsNewParagraph = newBlockElement != null;
+
+ if (newBlockElement == null)
+ {
+ // The line contains paragraph text.
+ if (paragraphText.Length > 0)
+ {
+ // If the previous two characters were both spaces, then append a line break.
+ if (paragraphText.Length > 2 && paragraphText[paragraphText.Length - 1] == ' ' && paragraphText[paragraphText.Length - 2] == ' ')
+ {
+ // Replace the two spaces with a line break.
+ paragraphText[paragraphText.Length - 2] = '\r';
+ paragraphText[paragraphText.Length - 1] = '\n';
+ }
+ else
+ {
+ paragraphText.Append(" ");
+ }
+ }
+
+ // Add the last paragraph if we are at the end of the input text.
+ if (startOfNextLine >= end)
+ {
+ if (paragraphText.Length == 0)
+ {
+ // Optimize for single line paragraphs.
+ blocks.Add(ParagraphBlock.Parse(markdown.Substring(startOfLine, endOfLine - startOfLine)));
+ }
+ else
+ {
+ // Slow path.
+ paragraphText.Append(markdown.Substring(startOfLine, endOfLine - startOfLine));
+ blocks.Add(ParagraphBlock.Parse(paragraphText.ToString()));
+ }
+ }
+ else
+ {
+ paragraphText.Append(markdown.Substring(startOfLine, endOfLine - startOfLine));
+ }
+ }
+ else
+ {
+ // The line contained a block. End the current paragraph, if any.
+ if (paragraphText.Length > 0)
+ {
+ blocks.Add(ParagraphBlock.Parse(paragraphText.ToString()));
+ paragraphText.Clear();
+ }
+
+ blocks.Add(newBlockElement);
+ }
+ }
+
+ // Repeat.
+ previousRealtStartOfLine = realStartOfLine;
+ previousStartOfLine = startOfLine;
+ previousEndOfLine = endOfLine;
+ startOfLine = startOfNextLine;
+ }
+
+ actualEnd = startOfLine;
+ return blocks;
+ }
+
+ ///
+ /// Looks up a reference using the ID.
+ /// A reference is a line that looks like this:
+ /// [foo]: http://example.com/
+ ///
+ /// The ID of the reference (case insensitive).
+ /// The reference details, or null if the reference wasn't found.
+ public LinkReferenceBlock LookUpReference(string id)
+ {
+ if (id == null)
+ {
+ throw new ArgumentNullException(nameof(id));
+ }
+
+ if (_references == null)
+ {
+ return null;
+ }
+
+ if (_references.TryGetValue(id, out LinkReferenceBlock result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Converts the object into it's textual representation.
+ ///
+ /// The textual representation of this object.
+ public override string ToString()
+ {
+ if (Blocks == null)
+ {
+ return base.ToString();
+ }
+
+ return string.Join("\r\n", Blocks);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownElement.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownElement.cs
new file mode 100644
index 0000000..d01e7fa
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownElement.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// This is a class used as the base class of all markdown elements.
+ ///
+ public abstract class MarkdownElement
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownInline.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownInline.cs
new file mode 100644
index 0000000..29cff91
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/MarkdownInline.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// An internal class that is the base class for all inline elements.
+ ///
+ public abstract class MarkdownInline : MarkdownElement
+ {
+ ///
+ /// Gets or sets this element is.
+ ///
+ public MarkdownInlineType Type { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal MarkdownInline(MarkdownInlineType type)
+ {
+ Type = type;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ICodeBlockResolver.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ICodeBlockResolver.cs
new file mode 100644
index 0000000..1b6dbaf
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ICodeBlockResolver.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Xaml.Documents;
+
+ ///
+ /// A Parser to parse code strings into Syntax Highlighted text.
+ ///
+ public interface ICodeBlockResolver
+ {
+ ///
+ /// Parses Code Block text into Rich text.
+ ///
+ /// Block to add formatted Text to.
+ /// The raw code block text
+ /// The language of the Code Block, as specified by ```{Language} on the first line of the block,
+ /// e.g.
+ /// ```C#
+ /// public void Method();
+ /// ```
+ ///
+ /// Parsing was handled Successfully
+ bool ParseSyntax(InlineCollection inlineCollection, string text, string codeLanguage);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IImageResolver.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IImageResolver.cs
new file mode 100644
index 0000000..df69134
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IImageResolver.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Threading.Tasks;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// An interface used to resolve images in the markdown.
+ ///
+ public interface IImageResolver
+ {
+ ///
+ /// Resolves an Image from a Url.
+ ///
+ /// Url to Resolve.
+ /// Tooltip for Image.
+ /// Image
+ Task ResolveImageAsync(string url, string tooltip);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ILinkRegister.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ILinkRegister.cs
new file mode 100644
index 0000000..61ba936
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/ILinkRegister.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+
+ ///
+ /// An interface used to handle links in the markdown.
+ ///
+ public interface ILinkRegister
+ {
+ ///
+ /// Registers a Hyperlink with a LinkUrl.
+ ///
+ /// Hyperlink to Register.
+ /// Url to Register.
+ void RegisterNewHyperLink(Hyperlink newHyperlink, string linkUrl);
+
+ ///
+ /// Registers a Hyperlink with a LinkUrl.
+ ///
+ /// ImageLink to Register.
+ /// Url to Register.
+ /// Is Image an IsHyperlink.
+ void RegisterNewHyperLink(Image newImagelink, string linkUrl, bool isHyperLink);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IRenderContext.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IRenderContext.cs
new file mode 100644
index 0000000..0ab994b
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/IRenderContext.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Render
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Helper for holding persistent state of Renderer.
+ ///
+ public interface IRenderContext
+ {
+ ///
+ /// Gets or sets a value indicating whether to trim whitespace.
+ ///
+ bool TrimLeadingWhitespace { get; set; }
+
+ ///
+ /// Gets or sets the parent Element for this Context.
+ ///
+ object Parent { get; set; }
+
+ ///
+ /// Clones the Context.
+ ///
+ /// Clone
+ IRenderContext Clone();
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/InlineRenderContext.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/InlineRenderContext.cs
new file mode 100644
index 0000000..4961f01
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/InlineRenderContext.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Xaml.Documents;
+
+ ///
+ /// The Context of the Current Document Rendering.
+ ///
+ public class InlineRenderContext : RenderContext
+ {
+ internal InlineRenderContext(InlineCollection inlineCollection, IRenderContext context)
+ {
+ InlineCollection = inlineCollection;
+ TrimLeadingWhitespace = context.TrimLeadingWhitespace;
+ Parent = context.Parent;
+
+ if (context is RenderContext localcontext)
+ {
+ Foreground = localcontext.Foreground;
+ OverrideForeground = localcontext.OverrideForeground;
+ }
+
+ if (context is InlineRenderContext inlinecontext)
+ {
+ WithinBold = inlinecontext.WithinBold;
+ WithinItalics = inlinecontext.WithinItalics;
+ WithinHyperlink = inlinecontext.WithinHyperlink;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the Current Element is being rendered inside an Italics Run.
+ ///
+ public bool WithinItalics { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the Current Element is being rendered inside a Bold Run.
+ ///
+ public bool WithinBold { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the Current Element is being rendered inside a Link.
+ ///
+ public bool WithinHyperlink { get; set; }
+
+ ///
+ /// Gets or sets the list to add to.
+ ///
+ public InlineCollection InlineCollection { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Blocks.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Blocks.cs
new file mode 100644
index 0000000..b1fcae1
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Blocks.cs
@@ -0,0 +1,475 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Shapes;
+
+ ///
+ /// Block UI Methods for UWP UI Creation.
+ ///
+ public partial class MarkdownRenderer
+ {
+ ///
+ /// Renders a list of block elements.
+ ///
+ protected override void RenderBlocks(IEnumerable blockElements, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ base.RenderBlocks(blockElements, context);
+
+ // Remove the top margin from the first block element, the bottom margin from the last block element,
+ // and collapse adjacent margins.
+ FrameworkElement previousFrameworkElement = null;
+ for (int i = 0; i < blockUIElementCollection.Count; i++)
+ {
+ var frameworkElement = blockUIElementCollection[i] as FrameworkElement;
+ if (frameworkElement != null)
+ {
+ if (i == 0)
+ {
+ // Remove the top margin.
+ frameworkElement.Margin = new Thickness(
+ frameworkElement.Margin.Left,
+ 0,
+ frameworkElement.Margin.Right,
+ frameworkElement.Margin.Bottom);
+ }
+ else if (previousFrameworkElement != null)
+ {
+ // Remove the bottom margin.
+ frameworkElement.Margin = new Thickness(
+ frameworkElement.Margin.Left,
+ Math.Max(frameworkElement.Margin.Top, previousFrameworkElement.Margin.Bottom),
+ frameworkElement.Margin.Right,
+ frameworkElement.Margin.Bottom);
+ previousFrameworkElement.Margin = new Thickness(
+ previousFrameworkElement.Margin.Left,
+ previousFrameworkElement.Margin.Top,
+ previousFrameworkElement.Margin.Right,
+ 0);
+ }
+ }
+
+ previousFrameworkElement = frameworkElement;
+ }
+ }
+
+ ///
+ /// Renders a paragraph element.
+ ///
+ protected override void RenderParagraph(ParagraphBlock element, IRenderContext context)
+ {
+ var paragraph = new Paragraph
+ {
+ Margin = ParagraphMargin,
+ LineHeight = ParagraphLineHeight
+ };
+
+ var childContext = new InlineRenderContext(paragraph.Inlines, context)
+ {
+ Parent = paragraph
+ };
+
+ RenderInlineChildren(element.Inlines, childContext);
+
+ var textBlock = CreateOrReuseRichTextBlock(context);
+ textBlock.Blocks.Add(paragraph);
+ }
+
+ ///
+ /// Renders a yaml header element.
+ ///
+ protected override void RenderYamlHeader(YamlHeaderBlock element, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ var table = new MarkdownTable(element.Children.Count, 2, YamlBorderThickness, YamlBorderBrush)
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ Margin = TableMargin
+ };
+
+ // Split key and value
+ string[] childrenKeys = new string[element.Children.Count];
+ string[] childrenValues = new string[element.Children.Count];
+ element.Children.Keys.CopyTo(childrenKeys, 0);
+ element.Children.Values.CopyTo(childrenValues, 0);
+
+ // Add each column
+ for (int i = 0; i < element.Children.Count; i++)
+ {
+ // Add each cell
+ var keyCell = new TextBlock
+ {
+ Text = childrenKeys[i],
+ Foreground = Foreground,
+ TextAlignment = TextAlignment.Center,
+ FontWeight = FontWeights.Bold,
+ Margin = TableCellPadding
+ };
+ var valueCell = new TextBlock
+ {
+ Text = childrenValues[i],
+ Foreground = Foreground,
+ TextAlignment = TextAlignment.Left,
+ Margin = TableCellPadding,
+ TextWrapping = TextWrapping.Wrap
+ };
+ Grid.SetRow(keyCell, 0);
+ Grid.SetColumn(keyCell, i);
+ Grid.SetRow(valueCell, 1);
+ Grid.SetColumn(valueCell, i);
+ table.Children.Add(keyCell);
+ table.Children.Add(valueCell);
+ }
+
+ blockUIElementCollection.Add(table);
+ }
+
+ ///
+ /// Renders a header element.
+ ///
+ protected override void RenderHeader(HeaderBlock element, IRenderContext context)
+ {
+ var textBlock = CreateOrReuseRichTextBlock(context);
+
+ var paragraph = new Paragraph();
+ var childInlines = paragraph.Inlines;
+ switch (element.HeaderLevel)
+ {
+ case 1:
+ paragraph.Margin = Header1Margin;
+ paragraph.FontSize = Header1FontSize;
+ paragraph.FontWeight = Header1FontWeight;
+ paragraph.Foreground = Header1Foreground;
+ break;
+
+ case 2:
+ paragraph.Margin = Header2Margin;
+ paragraph.FontSize = Header2FontSize;
+ paragraph.FontWeight = Header2FontWeight;
+ paragraph.Foreground = Header2Foreground;
+ break;
+
+ case 3:
+ paragraph.Margin = Header3Margin;
+ paragraph.FontSize = Header3FontSize;
+ paragraph.FontWeight = Header3FontWeight;
+ paragraph.Foreground = Header3Foreground;
+ break;
+
+ case 4:
+ paragraph.Margin = Header4Margin;
+ paragraph.FontSize = Header4FontSize;
+ paragraph.FontWeight = Header4FontWeight;
+ paragraph.Foreground = Header4Foreground;
+ break;
+
+ case 5:
+ paragraph.Margin = Header5Margin;
+ paragraph.FontSize = Header5FontSize;
+ paragraph.FontWeight = Header5FontWeight;
+ paragraph.Foreground = Header5Foreground;
+ break;
+
+ case 6:
+ paragraph.Margin = Header6Margin;
+ paragraph.FontSize = Header6FontSize;
+ paragraph.FontWeight = Header6FontWeight;
+ paragraph.Foreground = Header6Foreground;
+
+ var underline = new Underline();
+ childInlines = underline.Inlines;
+ paragraph.Inlines.Add(underline);
+ break;
+ }
+
+ // Render the children into the para inline.
+ var childContext = new InlineRenderContext(childInlines, context)
+ {
+ Parent = paragraph,
+ TrimLeadingWhitespace = true
+ };
+
+ RenderInlineChildren(element.Inlines, childContext);
+
+ // Add it to the blocks
+ textBlock.Blocks.Add(paragraph);
+ }
+
+ ///
+ /// Renders a list element.
+ ///
+ protected override void RenderListElement(ListBlock element, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ // Create a grid with two columns.
+ Grid grid = new Grid
+ {
+ Margin = ListMargin
+ };
+
+ // The first column for the bullet (or number) and the second for the text.
+ grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(ListGutterWidth) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
+
+ for (int rowIndex = 0; rowIndex < element.Items.Count; rowIndex++)
+ {
+ var listItem = element.Items[rowIndex];
+
+ // Add a row definition.
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ // Add the bullet or number.
+ var bullet = CreateTextBlock(localContext);
+ bullet.Margin = ParagraphMargin;
+ switch (element.Style)
+ {
+ case ListStyle.Bulleted:
+ bullet.Text = "•";
+ break;
+
+ case ListStyle.Numbered:
+ bullet.Text = $"{rowIndex + 1}.";
+ break;
+ }
+
+ bullet.HorizontalAlignment = HorizontalAlignment.Right;
+ bullet.Margin = new Thickness(0, 0, ListBulletSpacing, 0);
+ Grid.SetRow(bullet, rowIndex);
+ grid.Children.Add(bullet);
+
+ // Add the list item content.
+ var content = new StackPanel();
+ var childContext = new UIElementCollectionRenderContext(content.Children, localContext);
+ RenderBlocks(listItem.Blocks, childContext);
+ Grid.SetColumn(content, 1);
+ Grid.SetRow(content, rowIndex);
+ grid.Children.Add(content);
+ }
+
+ blockUIElementCollection.Add(grid);
+ }
+
+ ///
+ /// Renders a horizontal rule element.
+ ///
+ protected override void RenderHorizontalRule(IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ var brush = localContext.Foreground;
+ if (HorizontalRuleBrush != null && !localContext.OverrideForeground)
+ {
+ brush = HorizontalRuleBrush;
+ }
+
+ var rectangle = new Rectangle
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Height = HorizontalRuleThickness,
+ Fill = brush,
+ Margin = HorizontalRuleMargin
+ };
+
+ blockUIElementCollection.Add(rectangle);
+ }
+
+ ///
+ /// Renders a quote element.
+ ///
+ protected override void RenderQuote(QuoteBlock element, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ var stackPanel = new StackPanel();
+ var childContext = new UIElementCollectionRenderContext(stackPanel.Children, context)
+ {
+ Parent = stackPanel
+ };
+
+ if (QuoteForeground != null && !localContext.OverrideForeground)
+ {
+ childContext.Foreground = QuoteForeground;
+ }
+
+ RenderBlocks(element.Blocks, childContext);
+
+ var border = new Border
+ {
+ Margin = QuoteMargin,
+ Background = QuoteBackground,
+ BorderBrush = childContext.OverrideForeground ? childContext.Foreground : QuoteBorderBrush ?? childContext.Foreground,
+ BorderThickness = QuoteBorderThickness,
+ Padding = QuotePadding,
+ Child = stackPanel
+ };
+
+ blockUIElementCollection.Add(border);
+ }
+
+ ///
+ /// Renders a code element.
+ ///
+ protected override void RenderCode(CodeBlock element, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ var brush = localContext.Foreground;
+ if (CodeForeground != null && !localContext.OverrideForeground)
+ {
+ brush = CodeForeground;
+ }
+
+ var textBlock = new RichTextBlock
+ {
+ FontFamily = CodeFontFamily ?? FontFamily,
+ Foreground = brush,
+ LineHeight = FontSize * 1.4,
+ FlowDirection = FlowDirection
+ };
+
+ textBlock.PointerWheelChanged += Preventative_PointerWheelChanged;
+
+ var paragraph = new Paragraph();
+ textBlock.Blocks.Add(paragraph);
+
+ // Allows external Syntax Highlighting
+ var hasCustomSyntax = CodeBlockResolver.ParseSyntax(paragraph.Inlines, element.Text, element.CodeLanguage);
+ if (!hasCustomSyntax)
+ {
+ paragraph.Inlines.Add(new Run { Text = element.Text });
+ }
+
+ // Ensures that Code has Horizontal Scroll and doesn't wrap.
+ var viewer = new ScrollViewer
+ {
+ Background = CodeBackground,
+ BorderBrush = CodeBorderBrush,
+ BorderThickness = CodeBorderThickness,
+ Padding = CodePadding,
+ Margin = CodeMargin,
+ Content = textBlock
+ };
+
+ if (!WrapCodeBlock)
+ {
+ viewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ viewer.HorizontalScrollMode = ScrollMode.Auto;
+ viewer.VerticalScrollMode = ScrollMode.Disabled;
+ viewer.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ }
+
+ // Add it to the blocks
+ blockUIElementCollection.Add(viewer);
+ }
+
+ ///
+ /// Renders a table element.
+ ///
+ protected override void RenderTable(TableBlock element, IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ var table = new MarkdownTable(element.ColumnDefinitions.Count, element.Rows.Count, TableBorderThickness, TableBorderBrush)
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ Margin = TableMargin
+ };
+
+ // Add each row.
+ for (int rowIndex = 0; rowIndex < element.Rows.Count; rowIndex++)
+ {
+ var row = element.Rows[rowIndex];
+
+ // Add each cell.
+ for (int cellIndex = 0; cellIndex < Math.Min(element.ColumnDefinitions.Count, row.Cells.Count); cellIndex++)
+ {
+ var cell = row.Cells[cellIndex];
+
+ // Cell content.
+ var cellContent = CreateOrReuseRichTextBlock(new UIElementCollectionRenderContext(null, context));
+ cellContent.Margin = TableCellPadding;
+ Grid.SetRow(cellContent, rowIndex);
+ Grid.SetColumn(cellContent, cellIndex);
+ switch (element.ColumnDefinitions[cellIndex].Alignment)
+ {
+ case ColumnAlignment.Center:
+ cellContent.TextAlignment = TextAlignment.Center;
+ break;
+
+ case ColumnAlignment.Right:
+ cellContent.TextAlignment = TextAlignment.Right;
+ break;
+ }
+
+ if (rowIndex == 0)
+ {
+ cellContent.FontWeight = FontWeights.Bold;
+ }
+
+ var paragraph = new Paragraph();
+
+ var childContext = new InlineRenderContext(paragraph.Inlines, context)
+ {
+ Parent = paragraph,
+ TrimLeadingWhitespace = true
+ };
+
+ RenderInlineChildren(cell.Inlines, childContext);
+
+ cellContent.Blocks.Add(paragraph);
+ table.Children.Add(cellContent);
+ }
+ }
+
+ blockUIElementCollection.Add(table);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Dimensions.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Dimensions.cs
new file mode 100644
index 0000000..343a971
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Dimensions.cs
@@ -0,0 +1,227 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Measurement Properties for elements in the Markdown.
+ ///
+ public partial class MarkdownRenderer
+ {
+ ///
+ /// Gets or sets the distance between the border and its child object.
+ ///
+ public Thickness Padding { get; set; }
+
+ ///
+ /// Gets or sets the border thickness of a control.
+ ///
+ public Thickness BorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the thickness of the border around code blocks.
+ ///
+ public Thickness CodeBorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the thickness of the border around inline code.
+ ///
+ public Thickness InlineCodeBorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the space outside of code blocks.
+ ///
+ public Thickness CodeMargin { get; set; }
+
+ ///
+ /// Gets or sets the space between the code border and the text.
+ ///
+ public Thickness CodePadding { get; set; }
+
+ ///
+ /// Gets or sets the space between the code border and the text.
+ ///
+ public Thickness InlineCodePadding { get; set; }
+
+ ///
+ /// Gets or sets the margin of inline code.
+ ///
+ public Thickness InlineCodeMargin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 1 headers.
+ ///
+ public double Header1FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 1 headers.
+ ///
+ public Thickness Header1Margin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 2 headers.
+ ///
+ public double Header2FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 2 headers.
+ ///
+ public Thickness Header2Margin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 3 headers.
+ ///
+ public double Header3FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 3 headers.
+ ///
+ public Thickness Header3Margin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 4 headers.
+ ///
+ public double Header4FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 4 headers.
+ ///
+ public Thickness Header4Margin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 5 headers.
+ ///
+ public double Header5FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 5 headers.
+ ///
+ public Thickness Header5Margin { get; set; }
+
+ ///
+ /// Gets or sets the font size for level 6 headers.
+ ///
+ public double Header6FontSize { get; set; }
+
+ ///
+ /// Gets or sets the margin for level 6 headers.
+ ///
+ public Thickness Header6Margin { get; set; }
+
+ ///
+ /// Gets or sets the margin used for horizontal rules.
+ ///
+ public Thickness HorizontalRuleMargin { get; set; }
+
+ ///
+ /// Gets or sets the vertical thickness of the horizontal rule.
+ ///
+ public double HorizontalRuleThickness { get; set; }
+
+ ///
+ /// Gets or sets the margin used by lists.
+ ///
+ public Thickness ListMargin { get; set; }
+
+ ///
+ /// Gets or sets the width of the space used by list item bullets/numbers.
+ ///
+ public double ListGutterWidth { get; set; }
+
+ ///
+ /// Gets or sets the space between the list item bullets/numbers and the list item content.
+ ///
+ public double ListBulletSpacing { get; set; }
+
+ ///
+ /// Gets or sets the margin used for paragraphs.
+ ///
+ public Thickness ParagraphMargin { get; set; }
+
+ ///
+ /// Gets or sets the line height used for paragraphs.
+ ///
+ public int ParagraphLineHeight { get; set; }
+
+ ///
+ /// Gets or sets the thickness of quote borders.
+ ///
+ public Thickness QuoteBorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the space outside of quote borders.
+ ///
+ public Thickness QuoteMargin { get; set; }
+
+ ///
+ /// Gets or sets the space between the quote border and the text.
+ ///
+ public Thickness QuotePadding { get; set; }
+
+ ///
+ /// Gets or sets the thickness of any table borders.
+ ///
+ public double TableBorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the thickness of any yaml header borders.
+ ///
+ public double YamlBorderThickness { get; set; }
+
+ ///
+ /// Gets or sets the padding inside each cell.
+ ///
+ public Thickness TableCellPadding { get; set; }
+
+ ///
+ /// Gets or sets the margin used by tables.
+ ///
+ public Thickness TableMargin { get; set; }
+
+ ///
+ /// Gets or sets the size of the text in this control.
+ ///
+ public double FontSize { get; set; }
+
+ ///
+ /// Gets or sets the uniform spacing between characters, in units of 1/1000 of an em.
+ ///
+ public int CharacterSpacing { get; set; }
+
+ ///
+ /// Gets or sets the word wrapping behavior.
+ ///
+ public TextWrapping TextWrapping { get; set; }
+
+ ///
+ /// Gets or sets the degree to which a font is condensed or expanded on the screen.
+ ///
+ public FontStretch FontStretch { get; set; }
+
+ ///
+ /// Gets or sets the stretch used for images.
+ ///
+ public Stretch ImageStretch { get; set; }
+
+ ///
+ /// Gets or sets the MaxHeight for images.
+ ///
+ public double ImageMaxHeight { get; set; }
+
+ ///
+ /// Gets or sets the MaxWidth for images.
+ ///
+ public double ImageMaxWidth { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to wrap text in the Code Block, or use Horizontal Scroll.
+ ///
+ public bool WrapCodeBlock { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Inlines.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Inlines.cs
new file mode 100644
index 0000000..f40472f
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Inlines.cs
@@ -0,0 +1,595 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+ using System.Text;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Inline UI Methods for UWP UI Creation.
+ ///
+ public partial class MarkdownRenderer
+ {
+ ///
+ /// Renders Emoji element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderEmoji(EmojiInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var inlineCollection = localContext.InlineCollection;
+
+ var emoji = new Run
+ {
+ FontFamily = EmojiFontFamily ?? DefaultEmojiFont,
+ Text = element.Text
+ };
+
+ inlineCollection.Add(emoji);
+ }
+
+ ///
+ /// Renders a text run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderTextRun(TextRunInline element, IRenderContext context)
+ {
+ InternalRenderTextRun(element, context);
+ }
+
+ private Run InternalRenderTextRun(TextRunInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var inlineCollection = localContext.InlineCollection;
+
+ // Create the text run
+ Run textRun = new Run
+ {
+ Text = CollapseWhitespace(context, element.Text)
+ };
+
+ // Add it
+ inlineCollection.Add(textRun);
+ return textRun;
+ }
+
+ ///
+ /// Renders a bold run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderBoldRun(BoldTextInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ // Create the text run
+ Span boldSpan = new Span
+ {
+ FontWeight = FontWeights.Bold
+ };
+
+ var childContext = new InlineRenderContext(boldSpan.Inlines, context)
+ {
+ Parent = boldSpan,
+ WithinBold = true
+ };
+
+ // Render the children into the bold inline.
+ RenderInlineChildren(element.Inlines, childContext);
+
+ // Add it to the current inline collection
+ localContext.InlineCollection.Add(boldSpan);
+ }
+
+ ///
+ /// Renders a link element
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderMarkdownLink(MarkdownLinkInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ // HACK: Superscript is not allowed within a hyperlink. But if we switch it around, so
+ // that the superscript is outside the hyperlink, then it will render correctly.
+ // This assumes that the entire hyperlink is to be rendered as superscript.
+ if (AllTextIsSuperscript(element) == false)
+ {
+ // Regular ol' hyperlink.
+ var link = new Hyperlink();
+
+ // Register the link
+ LinkRegister.RegisterNewHyperLink(link, element.Url);
+
+ // Remove superscripts.
+ RemoveSuperscriptRuns(element, insertCaret: true);
+
+ // Render the children into the link inline.
+ var childContext = new InlineRenderContext(link.Inlines, context)
+ {
+ Parent = link,
+ WithinHyperlink = true
+ };
+
+ if (localContext.OverrideForeground)
+ {
+ link.Foreground = localContext.Foreground;
+ }
+ else if (LinkForeground != null)
+ {
+ link.Foreground = LinkForeground;
+ }
+
+ RenderInlineChildren(element.Inlines, childContext);
+ context.TrimLeadingWhitespace = childContext.TrimLeadingWhitespace;
+
+ ToolTipService.SetToolTip(link, element.Tooltip ?? element.Url);
+
+ // Add it to the current inlines
+ localContext.InlineCollection.Add(link);
+ }
+ else
+ {
+ // THE HACK IS ON!
+
+ // Create a fake superscript element.
+ var fakeSuperscript = new SuperscriptTextInline
+ {
+ Inlines = new List
+ {
+ element
+ }
+ };
+
+ // Remove superscripts.
+ RemoveSuperscriptRuns(element, insertCaret: false);
+
+ // Now render it.
+ RenderSuperscriptRun(fakeSuperscript, context);
+ }
+ }
+
+ ///
+ /// Renders a raw link element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderHyperlink(HyperlinkInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var link = new Hyperlink();
+
+ // Register the link
+ LinkRegister.RegisterNewHyperLink(link, element.Url);
+
+ var brush = localContext.Foreground;
+ if (LinkForeground != null && !localContext.OverrideForeground)
+ {
+ brush = LinkForeground;
+ }
+
+ // Make a text block for the link
+ Run linkText = new Run
+ {
+ Text = CollapseWhitespace(context, element.Text),
+ Foreground = brush
+ };
+
+ link.Inlines.Add(linkText);
+
+ try
+ {
+ //Add it to the current inline collection
+ localContext.InlineCollection.Add(link);
+ }
+ catch // Invalid hyperlink
+ {
+ link.Inlines.Clear();
+ }
+ }
+
+ ///
+ /// Renders an image element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override async void RenderImage(ImageInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var inlineCollection = localContext.InlineCollection;
+
+ var placeholder = InternalRenderTextRun(new TextRunInline { Text = element.Text, Type = MarkdownInlineType.TextRun }, context);
+ var resolvedImage = await ImageResolver.ResolveImageAsync(element.RenderUrl, element.Tooltip);
+
+ // if image can not be resolved we have to return
+ if (resolvedImage == null)
+ {
+ return;
+ }
+
+ var image = new Image
+ {
+ Source = resolvedImage,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ Stretch = ImageStretch
+ };
+
+ HyperlinkButton hyperlinkButton = new HyperlinkButton()
+ {
+ Content = image
+ };
+
+ var viewbox = new Viewbox
+ {
+ Child = hyperlinkButton,
+ StretchDirection = StretchDirection.DownOnly
+ };
+
+ viewbox.PointerWheelChanged += Preventative_PointerWheelChanged;
+
+ var scrollViewer = new ScrollViewer
+ {
+ Content = viewbox,
+ VerticalScrollMode = ScrollMode.Disabled,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
+ };
+
+ var imageContainer = new InlineUIContainer() { Child = scrollViewer };
+
+ bool ishyperlink = element.RenderUrl != element.Url;
+
+ LinkRegister.RegisterNewHyperLink(image, element.Url, ishyperlink);
+
+ if (ImageMaxHeight > 0)
+ {
+ viewbox.MaxHeight = ImageMaxHeight;
+ }
+
+ if (ImageMaxWidth > 0)
+ {
+ viewbox.MaxWidth = ImageMaxWidth;
+ }
+
+ if (element.ImageWidth > 0)
+ {
+ image.Width = element.ImageWidth;
+ image.Stretch = Stretch.UniformToFill;
+ }
+
+ if (element.ImageHeight > 0)
+ {
+ if (element.ImageWidth == 0)
+ {
+ image.Width = element.ImageHeight;
+ }
+
+ image.Height = element.ImageHeight;
+ image.Stretch = Stretch.UniformToFill;
+ }
+
+ if (element.ImageHeight > 0 && element.ImageWidth > 0)
+ {
+ image.Stretch = Stretch.Fill;
+ }
+
+ // If image size is given then scroll to view overflown part
+ if (element.ImageHeight > 0 || element.ImageWidth > 0)
+ {
+ scrollViewer.HorizontalScrollMode = ScrollMode.Auto;
+ scrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ }
+
+ // Else resize the image
+ else
+ {
+ scrollViewer.HorizontalScrollMode = ScrollMode.Disabled;
+ scrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ }
+
+ ToolTipService.SetToolTip(image, element.Tooltip);
+
+ // Try to add it to the current inlines
+ // Could fail because some containers like Hyperlink cannot have inlined images
+ try
+ {
+ var placeholderIndex = inlineCollection.IndexOf(placeholder);
+ inlineCollection.Remove(placeholder);
+ inlineCollection.Insert(placeholderIndex, imageContainer);
+ }
+ catch
+ {
+ // Ignore error
+ }
+ }
+
+ ///
+ /// Renders a text run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderItalicRun(ItalicTextInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ // Create the text run
+ Span italicSpan = new Span
+ {
+ FontStyle = FontStyle.Italic
+ };
+
+ var childContext = new InlineRenderContext(italicSpan.Inlines, context)
+ {
+ Parent = italicSpan,
+ WithinItalics = true
+ };
+
+ // Render the children into the italic inline.
+ RenderInlineChildren(element.Inlines, childContext);
+
+ // Add it to the current inlines
+ localContext.InlineCollection.Add(italicSpan);
+ }
+
+ ///
+ /// Renders a strike-through element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderStrikethroughRun(StrikethroughTextInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ Span span = new Span();
+
+ if (TextDecorationsSupported)
+ {
+ span.TextDecorations = TextDecorations.Strikethrough;
+ }
+ else
+ {
+ span.FontFamily = new FontFamily("Consolas");
+ }
+
+ var childContext = new InlineRenderContext(span.Inlines, context)
+ {
+ Parent = span
+ };
+
+ // Render the children into the inline.
+ RenderInlineChildren(element.Inlines, childContext);
+
+ if (!TextDecorationsSupported)
+ {
+ AlterChildRuns(span, (parentSpan, run) =>
+ {
+ var text = run.Text;
+ var builder = new StringBuilder(text.Length * 2);
+ foreach (var c in text)
+ {
+ builder.Append((char)0x0336);
+ builder.Append(c);
+ }
+
+ run.Text = builder.ToString();
+ });
+ }
+
+ // Add it to the current inlines
+ localContext.InlineCollection.Add(span);
+ }
+
+ ///
+ /// Renders a superscript element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderSuperscriptRun(SuperscriptTextInline element, IRenderContext context)
+ {
+ var localContext = context as InlineRenderContext;
+ var parent = localContext?.Parent as TextElement;
+ if (localContext == null && parent == null)
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ // Le , InlineUIContainers are not allowed within hyperlinks.
+ if (localContext.WithinHyperlink)
+ {
+ RenderInlineChildren(element.Inlines, context);
+ return;
+ }
+
+ var paragraph = new Paragraph
+ {
+ FontSize = parent.FontSize * 0.8,
+ FontFamily = parent.FontFamily,
+ FontStyle = parent.FontStyle,
+ FontWeight = parent.FontWeight
+ };
+
+ var childContext = new InlineRenderContext(paragraph.Inlines, context)
+ {
+ Parent = paragraph
+ };
+
+ RenderInlineChildren(element.Inlines, childContext);
+
+ var richTextBlock = CreateOrReuseRichTextBlock(new UIElementCollectionRenderContext(null, context));
+ richTextBlock.Blocks.Add(paragraph);
+
+ var border = new Border
+ {
+ Padding = new Thickness(0, 0, 0, paragraph.FontSize * 0.2),
+ Child = richTextBlock
+ };
+
+ var inlineUIContainer = new InlineUIContainer
+ {
+ Child = border
+ };
+
+ // Add it to the current inlines
+ localContext.InlineCollection.Add(inlineUIContainer);
+ }
+
+ ///
+ /// Renders a subscript element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderSubscriptRun(SubscriptTextInline element, IRenderContext context)
+ {
+ var localContext = context as InlineRenderContext;
+ var parent = localContext?.Parent as TextElement;
+ if (localContext == null && parent == null)
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var paragraph = new Paragraph
+ {
+ FontSize = parent.FontSize * 0.7,
+ FontFamily = parent.FontFamily,
+ FontStyle = parent.FontStyle,
+ FontWeight = parent.FontWeight
+ };
+
+ var childContext = new InlineRenderContext(paragraph.Inlines, context)
+ {
+ Parent = paragraph
+ };
+
+ RenderInlineChildren(element.Inlines, childContext);
+
+ var richTextBlock = CreateOrReuseRichTextBlock(new UIElementCollectionRenderContext(null, context));
+ richTextBlock.Blocks.Add(paragraph);
+
+ var border = new Border
+ {
+ Margin = new Thickness(0, 0, 0, (-1) * (paragraph.FontSize * 0.6)),
+ Child = richTextBlock
+ };
+
+ var inlineUIContainer = new InlineUIContainer
+ {
+ Child = border
+ };
+
+ // Add it to the current inlines
+ localContext.InlineCollection.Add(inlineUIContainer);
+ }
+
+ ///
+ /// Renders a code element
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected override void RenderCodeRun(CodeInline element, IRenderContext context)
+ {
+ if (!(context is InlineRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var text = CreateTextBlock(localContext);
+ text.Text = CollapseWhitespace(context, element.Text);
+ text.FontFamily = InlineCodeFontFamily ?? FontFamily;
+ text.Foreground = InlineCodeForeground ?? Foreground;
+
+ if (localContext.WithinItalics)
+ {
+ text.FontStyle = FontStyle.Italic;
+ }
+
+ if (localContext.WithinBold)
+ {
+ text.FontWeight = FontWeights.Bold;
+ }
+
+ var borderthickness = InlineCodeBorderThickness;
+ var padding = InlineCodePadding;
+
+ var border = new Border
+ {
+ BorderThickness = borderthickness,
+ BorderBrush = InlineCodeBorderBrush,
+ Background = InlineCodeBackground,
+ Child = text,
+ Padding = padding,
+ Margin = InlineCodeMargin
+ };
+
+ // Aligns content in InlineUI, see https://social.msdn.microsoft.com/Forums/silverlight/en-US/48b5e91e-efc5-4768-8eaf-f897849fcf0b/richtextbox-inlineuicontainer-vertical-alignment-issue?forum=silverlightarchieve
+ border.RenderTransform = new TranslateTransform
+ {
+ Y = 4
+ };
+
+ var inlineUIContainer = new InlineUIContainer
+ {
+ Child = border,
+ };
+
+ try
+ {
+ // Add it to the current inline collection
+ localContext.InlineCollection.Add(inlineUIContainer);
+ }
+ catch // Fallback
+ {
+ Run run = new Run
+ {
+ Text = text.Text,
+ FontFamily = InlineCodeFontFamily ?? FontFamily,
+ Foreground = InlineCodeForeground ?? Foreground
+ };
+
+ // Additional formatting
+ if (localContext.WithinItalics) run.FontStyle = FontStyle.Italic;
+ if (localContext.WithinBold) run.FontWeight = FontWeights.Bold;
+
+ // Add the fallback block
+ localContext.InlineCollection.Add(run);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Properties.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Properties.cs
new file mode 100644
index 0000000..bc9c2a4
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.Properties.cs
@@ -0,0 +1,245 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Reflection;
+ using Windows.Foundation.Metadata;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Properties for the UWP Markdown Renderer
+ ///
+ public partial class MarkdownRenderer
+ {
+ private static bool? _textDecorationsSupported = null;
+
+ private static bool TextDecorationsSupported => (bool)(_textDecorationsSupported ??
+ (_textDecorationsSupported = ApiInformation.IsTypePresent("Windows.UI.Text.TextDecorations")));
+
+ ///
+ /// Super Hack to retain inertia and passing the Scroll data onto the Parent ScrollViewer.
+ ///
+ private static readonly MethodInfo pointerWheelChanged = typeof(ScrollViewer).GetMethod("OnPointerWheelChanged", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ ///
+ /// Gets or sets the Root Framework Element.
+ ///
+ private FrameworkElement RootElement { get; set; }
+
+ ///
+ /// Gets the interface that is used to register hyperlinks.
+ ///
+ protected ILinkRegister LinkRegister { get; }
+
+ ///
+ /// Gets the interface that is used to resolve images.
+ ///
+ protected IImageResolver ImageResolver { get; }
+
+ ///
+ /// Gets the Parser to parse code strings into Syntax Highlighted text.
+ ///
+ protected ICodeBlockResolver CodeBlockResolver { get; }
+
+ ///
+ /// Gets the Default Emoji Font.
+ ///
+ protected FontFamily DefaultEmojiFont { get; }
+
+ ///
+ /// Gets or sets a brush that provides the background of the control.
+ ///
+ public Brush Background { get; set; }
+
+ ///
+ /// Gets or sets a brush that describes the border fill of a control.
+ ///
+ public Brush BorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the of the markdown.
+ ///
+ public FlowDirection FlowDirection { get; set; }
+
+ ///
+ /// Gets or sets the font used to display text in the control.
+ ///
+ public FontFamily FontFamily { get; set; }
+
+ ///
+ /// Gets or sets the style in which the text is rendered.
+ ///
+ public FontStyle FontStyle { get; set; }
+
+ ///
+ /// Gets or sets the thickness of the specified font.
+ ///
+ public FontWeight FontWeight { get; set; }
+
+ ///
+ /// Gets or sets a brush that describes the foreground color.
+ ///
+ public Brush Foreground { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether text selection is enabled.
+ ///
+ public bool IsTextSelectionEnabled { get; set; }
+
+ ///
+ /// Gets or sets the brush used to fill the background of a code block.
+ ///
+ public Brush CodeBackground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to fill the background of inline code.
+ ///
+ public Brush InlineCodeBackground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to fill the foreground of inline code.
+ ///
+ public Brush InlineCodeForeground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to fill the border of inline code.
+ ///
+ public Brush InlineCodeBorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render the border fill of a code block.
+ ///
+ public Brush CodeBorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render the text inside a code block. If this is
+ /// null , then is used.
+ ///
+ public Brush CodeForeground { get; set; }
+
+ ///
+ /// Gets or sets the font used to display code. If this is null , then
+ /// is used.
+ ///
+ public FontFamily CodeFontFamily { get; set; }
+
+ ///
+ /// Gets or sets the font used to display code. If this is null , then
+ /// is used.
+ ///
+ public FontFamily InlineCodeFontFamily { get; set; }
+
+ ///
+ /// Gets or sets the font used to display emojis. If this is null , then
+ /// Segoe UI Emoji font is used.
+ ///
+ public FontFamily EmojiFontFamily { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 1 headers.
+ ///
+ public FontWeight Header1FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 1 headers.
+ ///
+ public Brush Header1Foreground { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 2 headers.
+ ///
+ public FontWeight Header2FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 2 headers.
+ ///
+ public Brush Header2Foreground { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 3 headers.
+ ///
+ public FontWeight Header3FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 3 headers.
+ ///
+ public Brush Header3Foreground { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 4 headers.
+ ///
+ public FontWeight Header4FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 4 headers.
+ ///
+ public Brush Header4Foreground { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 5 headers.
+ ///
+ public FontWeight Header5FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 5 headers.
+ ///
+ public Brush Header5Foreground { get; set; }
+
+ ///
+ /// Gets or sets the font weight to use for level 6 headers.
+ ///
+ public FontWeight Header6FontWeight { get; set; }
+
+ ///
+ /// Gets or sets the foreground brush for level 6 headers.
+ ///
+ public Brush Header6Foreground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render a horizontal rule. If this is null , then
+ /// is used.
+ ///
+ public Brush HorizontalRuleBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to fill the background of a quote block.
+ ///
+ public Brush QuoteBackground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render a quote border. If this is null , then
+ /// is used.
+ ///
+ public Brush QuoteBorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render the text inside a quote block. If this is
+ /// null , then is used.
+ ///
+ public Brush QuoteForeground { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render table borders. If this is null , then
+ /// is used.
+ ///
+ public Brush TableBorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render table borders. If this is null , then
+ /// is used.
+ ///
+ public Brush YamlBorderBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used to render links. If this is null , then
+ /// is used.
+ ///
+ public Brush LinkForeground { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.cs
new file mode 100644
index 0000000..e9d0354
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRenderer.cs
@@ -0,0 +1,221 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Microsoft.Toolkit.Uwp.UI;
+ using System;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Generates Framework Elements for the UWP Markdown Textblock.
+ ///
+ public partial class MarkdownRenderer : MarkdownRendererBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Document to Render.
+ /// The LinkRegister, will use itself.
+ /// The Image Resolver, will use itself.
+ /// The Code Block Resolver, will use itself.
+ public MarkdownRenderer(MarkdownDocument document, ILinkRegister linkRegister, IImageResolver imageResolver, ICodeBlockResolver codeBlockResolver)
+ : base(document)
+ {
+ LinkRegister = linkRegister;
+ ImageResolver = imageResolver;
+ CodeBlockResolver = codeBlockResolver;
+ DefaultEmojiFont = new FontFamily("Segoe UI Emoji");
+ }
+
+ ///
+ /// Called externally to render markdown to a text block.
+ ///
+ /// A XAML UI element.
+ public UIElement Render()
+ {
+ var stackPanel = new StackPanel();
+ RootElement = stackPanel;
+ Render(new UIElementCollectionRenderContext(stackPanel.Children) { Foreground = Foreground });
+
+ // Set background and border properties.
+ stackPanel.Background = Background;
+ stackPanel.BorderBrush = BorderBrush;
+ stackPanel.BorderThickness = BorderThickness;
+ stackPanel.Padding = Padding;
+
+ return stackPanel;
+ }
+
+ ///
+ /// Creates a new RichTextBlock, if the last element of the provided collection isn't already a RichTextBlock.
+ ///
+ /// The rich text block
+ protected RichTextBlock CreateOrReuseRichTextBlock(IRenderContext context)
+ {
+ if (!(context is UIElementCollectionRenderContext localContext))
+ {
+ throw new RenderContextIncorrectException();
+ }
+
+ var blockUIElementCollection = localContext.BlockUIElementCollection;
+
+ // Reuse the last RichTextBlock, if possible.
+ if (blockUIElementCollection != null && blockUIElementCollection.Count > 0 && blockUIElementCollection[blockUIElementCollection.Count - 1] is RichTextBlock)
+ {
+ return (RichTextBlock)blockUIElementCollection[blockUIElementCollection.Count - 1];
+ }
+
+ var result = new RichTextBlock
+ {
+ CharacterSpacing = CharacterSpacing,
+ FontFamily = FontFamily,
+ FontSize = FontSize,
+ FontStretch = FontStretch,
+ FontStyle = FontStyle,
+ FontWeight = FontWeight,
+ Foreground = localContext.Foreground,
+ IsTextSelectionEnabled = IsTextSelectionEnabled,
+ TextWrapping = TextWrapping,
+ FlowDirection = FlowDirection
+ };
+ localContext.BlockUIElementCollection?.Add(result);
+
+ return result;
+ }
+
+ ///
+ /// Creates a new TextBlock, with default settings.
+ ///
+ /// The created TextBlock
+ protected TextBlock CreateTextBlock(RenderContext context)
+ {
+ var result = new TextBlock
+ {
+ CharacterSpacing = CharacterSpacing,
+ FontFamily = FontFamily,
+ FontSize = FontSize,
+ FontStretch = FontStretch,
+ FontStyle = FontStyle,
+ FontWeight = FontWeight,
+ Foreground = context.Foreground,
+ IsTextSelectionEnabled = IsTextSelectionEnabled,
+ TextWrapping = TextWrapping,
+ FlowDirection = FlowDirection
+ };
+ return result;
+ }
+
+ ///
+ /// Performs an action against any runs that occur within the given span.
+ ///
+ protected void AlterChildRuns(Span parentSpan, Action action)
+ {
+ foreach (var inlineElement in parentSpan.Inlines)
+ {
+ if (inlineElement is Span span)
+ {
+ AlterChildRuns(span, action);
+ }
+ else if (inlineElement is Run)
+ {
+ action(parentSpan, (Run)inlineElement);
+ }
+ }
+ }
+
+ ///
+ /// Checks if all text elements inside the given container are superscript.
+ ///
+ /// true if all text is superscript (level 1); false otherwise.
+ private bool AllTextIsSuperscript(IInlineContainer container, int superscriptLevel = 0)
+ {
+ foreach (var inline in container.Inlines)
+ {
+ if (inline is SuperscriptTextInline textInline)
+ {
+ // Remove any nested superscripts.
+ if (AllTextIsSuperscript(textInline, superscriptLevel + 1) == false)
+ {
+ return false;
+ }
+ }
+ else if (inline is IInlineContainer inlineContainer)
+ {
+ // Remove any superscripts.
+ if (AllTextIsSuperscript(inlineContainer, superscriptLevel) == false)
+ {
+ return false;
+ }
+ }
+ else if (inline is IInlineLeaf leaf && superscriptLevel != 1)
+ {
+ if (!ParseHelpers.IsMarkdownBlankOrWhiteSpace(leaf.Text) || string.IsNullOrWhiteSpace(leaf.Text))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes all superscript elements from the given container.
+ ///
+ private void RemoveSuperscriptRuns(IInlineContainer container, bool insertCaret)
+ {
+ for (int i = 0; i < container.Inlines.Count; i++)
+ {
+ var inline = container.Inlines[i];
+ if (inline is SuperscriptTextInline textInline)
+ {
+ // Remove any nested superscripts.
+ RemoveSuperscriptRuns(textInline, insertCaret);
+
+ // Remove the superscript element, insert all the children.
+ container.Inlines.RemoveAt(i);
+ if (insertCaret)
+ {
+ container.Inlines.Insert(i++, new TextRunInline { Text = "^" });
+ }
+
+ foreach (var superscriptInline in textInline.Inlines)
+ {
+ container.Inlines.Insert(i++, superscriptInline);
+ }
+
+ i--;
+ }
+ else if (inline is IInlineContainer)
+ {
+ // Remove any superscripts.
+ RemoveSuperscriptRuns((IInlineContainer)inline, insertCaret);
+ }
+ }
+ }
+
+ private void Preventative_PointerWheelChanged(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
+ {
+ var pointerPoint = e.GetCurrentPoint((UIElement)sender);
+
+ if (pointerPoint.Properties.IsHorizontalMouseWheel)
+ {
+ e.Handled = false;
+ return;
+ }
+
+ var rootViewer = DependencyObjectExtensions.FindAscendant(RootElement);
+ if (rootViewer != null)
+ {
+ pointerWheelChanged?.Invoke(rootViewer, new object[] { e });
+ e.Handled = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Blocks.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Blocks.cs
new file mode 100644
index 0000000..b46746f
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Blocks.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Render
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Block Rendering Methods.
+ ///
+ public partial class MarkdownRendererBase
+ {
+ ///
+ /// Renders a paragraph element.
+ ///
+ protected abstract void RenderParagraph(ParagraphBlock element, IRenderContext context);
+
+ ///
+ /// Renders a yaml header element.
+ ///
+ protected abstract void RenderYamlHeader(YamlHeaderBlock element, IRenderContext context);
+
+ ///
+ /// Renders a header element.
+ ///
+ protected abstract void RenderHeader(HeaderBlock element, IRenderContext context);
+
+ ///
+ /// Renders a list element.
+ ///
+ protected abstract void RenderListElement(ListBlock element, IRenderContext context);
+
+ ///
+ /// Renders a horizontal rule element.
+ ///
+ protected abstract void RenderHorizontalRule(IRenderContext context);
+
+ ///
+ /// Renders a quote element.
+ ///
+ protected abstract void RenderQuote(QuoteBlock element, IRenderContext context);
+
+ ///
+ /// Renders a code element.
+ ///
+ protected abstract void RenderCode(CodeBlock element, IRenderContext context);
+
+ ///
+ /// Renders a table element.
+ ///
+ protected abstract void RenderTable(TableBlock element, IRenderContext context);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Inlines.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Inlines.cs
new file mode 100644
index 0000000..9d7121a
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.Inlines.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Render
+
+namespace Notepads.Controls.Markdown
+{
+ ///
+ /// Inline Rendering Methods.
+ ///
+ public partial class MarkdownRendererBase
+ {
+ ///
+ /// Renders emoji element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderEmoji(EmojiInline element, IRenderContext context);
+
+ ///
+ /// Renders a text run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderTextRun(TextRunInline element, IRenderContext context);
+
+ ///
+ /// Renders a bold run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderBoldRun(BoldTextInline element, IRenderContext context);
+
+ ///
+ /// Renders a link element
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderMarkdownLink(MarkdownLinkInline element, IRenderContext context);
+
+ ///
+ /// Renders an image element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderImage(ImageInline element, IRenderContext context);
+
+ ///
+ /// Renders a raw link element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderHyperlink(HyperlinkInline element, IRenderContext context);
+
+ ///
+ /// Renders a text run element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderItalicRun(ItalicTextInline element, IRenderContext context);
+
+ ///
+ /// Renders a strikethrough element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderStrikethroughRun(StrikethroughTextInline element, IRenderContext context);
+
+ ///
+ /// Renders a superscript element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderSuperscriptRun(SuperscriptTextInline element, IRenderContext context);
+
+ ///
+ /// Renders a subscript element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderSubscriptRun(SubscriptTextInline element, IRenderContext context);
+
+ ///
+ /// Renders a code element
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected abstract void RenderCodeRun(CodeInline element, IRenderContext context);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.cs
new file mode 100644
index 0000000..fd20461
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownRendererBase.cs
@@ -0,0 +1,250 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System.Collections.Generic;
+ using System.Text;
+
+ ///
+ /// A base renderer for Rendering Markdown into Controls.
+ ///
+ public abstract partial class MarkdownRendererBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Markdown Document to Render
+ protected MarkdownRendererBase(MarkdownDocument document)
+ {
+ Document = document;
+ }
+
+ ///
+ /// Renders all Content to the Provided Parent UI.
+ ///
+ /// UI Context
+ public virtual void Render(IRenderContext context)
+ {
+ RenderBlocks(Document.Blocks, context);
+ }
+
+ ///
+ /// Renders a list of block elements.
+ ///
+ protected virtual void RenderBlocks(IEnumerable blockElements, IRenderContext context)
+ {
+ foreach (MarkdownBlock element in blockElements)
+ {
+ RenderBlock(element, context);
+ }
+ }
+
+ ///
+ /// Called to render a block element.
+ ///
+ protected void RenderBlock(MarkdownBlock element, IRenderContext context)
+ {
+ {
+ switch (element.Type)
+ {
+ case MarkdownBlockType.Paragraph:
+ RenderParagraph((ParagraphBlock)element, context);
+ break;
+
+ case MarkdownBlockType.Quote:
+ RenderQuote((QuoteBlock)element, context);
+ break;
+
+ case MarkdownBlockType.Code:
+ RenderCode((CodeBlock)element, context);
+ break;
+
+ case MarkdownBlockType.Header:
+ RenderHeader((HeaderBlock)element, context);
+ break;
+
+ case MarkdownBlockType.List:
+ RenderListElement((ListBlock)element, context);
+ break;
+
+ case MarkdownBlockType.HorizontalRule:
+ RenderHorizontalRule(context);
+ break;
+
+ case MarkdownBlockType.Table:
+ RenderTable((TableBlock)element, context);
+ break;
+
+ case MarkdownBlockType.YamlHeader:
+ RenderYamlHeader((YamlHeaderBlock)element, context);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Renders all of the children for the given element.
+ ///
+ /// The parsed inline elements to render.
+ /// Persistent state.
+ protected void RenderInlineChildren(IList inlineElements, IRenderContext context)
+ {
+ foreach (MarkdownInline element in inlineElements)
+ {
+ switch (element.Type)
+ {
+ case MarkdownInlineType.Comment:
+ case MarkdownInlineType.LinkReference:
+ break;
+
+ default:
+ RenderInline(element, context);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Called to render an inline element.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected void RenderInline(MarkdownInline element, IRenderContext context)
+ {
+ switch (element.Type)
+ {
+ case MarkdownInlineType.TextRun:
+ RenderTextRun((TextRunInline)element, context);
+ break;
+
+ case MarkdownInlineType.Italic:
+ RenderItalicRun((ItalicTextInline)element, context);
+ break;
+
+ case MarkdownInlineType.Bold:
+ RenderBoldRun((BoldTextInline)element, context);
+ break;
+
+ case MarkdownInlineType.MarkdownLink:
+ CheckRenderMarkdownLink((MarkdownLinkInline)element, context);
+ break;
+
+ case MarkdownInlineType.RawHyperlink:
+ RenderHyperlink((HyperlinkInline)element, context);
+ break;
+
+ case MarkdownInlineType.Strikethrough:
+ RenderStrikethroughRun((StrikethroughTextInline)element, context);
+ break;
+
+ case MarkdownInlineType.Superscript:
+ RenderSuperscriptRun((SuperscriptTextInline)element, context);
+ break;
+
+ case MarkdownInlineType.Subscript:
+ RenderSubscriptRun((SubscriptTextInline)element, context);
+ break;
+
+ case MarkdownInlineType.Code:
+ RenderCodeRun((CodeInline)element, context);
+ break;
+
+ case MarkdownInlineType.Image:
+ RenderImage((ImageInline)element, context);
+ break;
+
+ case MarkdownInlineType.Emoji:
+ RenderEmoji((EmojiInline)element, context);
+ break;
+ }
+ }
+
+ ///
+ /// Removes leading whitespace, but only if this is the first run in the block.
+ ///
+ /// The corrected string
+ protected static string CollapseWhitespace(IRenderContext context, string text)
+ {
+ bool dontOutputWhitespace = context.TrimLeadingWhitespace;
+ StringBuilder result = null;
+ for (int i = 0; i < text.Length; i++)
+ {
+ char c = text[i];
+ if (c == ' ' || c == '\t')
+ {
+ if (dontOutputWhitespace)
+ {
+ if (result == null)
+ {
+ result = new StringBuilder(text.Substring(0, i), text.Length);
+ }
+ }
+ else
+ {
+ result?.Append(c);
+
+ dontOutputWhitespace = true;
+ }
+ }
+ else
+ {
+ result?.Append(c);
+
+ dontOutputWhitespace = false;
+ }
+ }
+
+ context.TrimLeadingWhitespace = false;
+ return result == null ? text : result.ToString();
+ }
+
+ ///
+ /// Verifies if the link is valid, before processing into a link, or plain text.
+ ///
+ /// The parsed inline element to render.
+ /// Persistent state.
+ protected void CheckRenderMarkdownLink(MarkdownLinkInline element, IRenderContext context)
+ {
+ // Avoid processing when link text is empty.
+ if (element.Inlines.Count == 0)
+ {
+ return;
+ }
+
+ // Attempt to resolve references.
+ element.ResolveReference(Document);
+ if (element.Url == null)
+ {
+ // The element couldn't be resolved, just render it as text.
+ RenderInlineChildren(element.Inlines, context);
+ return;
+ }
+
+ foreach (MarkdownInline inline in element.Inlines)
+ {
+ if (inline is ImageInline imageInline)
+ {
+ // this is an image, create Image.
+ if (!string.IsNullOrEmpty(imageInline.ReferenceId))
+ {
+ imageInline.ResolveReference(Document);
+ }
+
+ imageInline.Url = element.Url;
+ RenderImage(imageInline, context);
+ return;
+ }
+ }
+
+ RenderMarkdownLink(element, context);
+ }
+
+ ///
+ /// Gets the markdown document that will be rendered.
+ ///
+ protected MarkdownDocument Document { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownTable.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownTable.cs
new file mode 100644
index 0000000..b87d001
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/MarkdownTable.cs
@@ -0,0 +1,217 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Windows.Foundation;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+ using Windows.UI.Xaml.Shapes;
+
+ ///
+ /// A custom panel control that arranges elements similar to how an HTML table would.
+ ///
+ internal class MarkdownTable : Panel
+ {
+ private readonly int _columnCount;
+ private readonly int _rowCount;
+ private readonly double _borderThickness;
+ private double[] _columnWidths;
+ private double[] _rowHeights;
+
+ public MarkdownTable(int columnCount, int rowCount, double borderThickness, Brush borderBrush)
+ {
+ _columnCount = columnCount;
+ _rowCount = rowCount;
+ _borderThickness = borderThickness;
+ for (int col = 0; col < columnCount + 1; col++)
+ {
+ Children.Add(new Rectangle { Fill = borderBrush });
+ }
+
+ for (int row = 0; row < rowCount + 1; row++)
+ {
+ Children.Add(new Rectangle { Fill = borderBrush });
+ }
+ }
+
+ // Helper method to enumerate FrameworkElements instead of UIElements.
+ private IEnumerable ContentChildren
+ {
+ get
+ {
+ for (int i = _columnCount + _rowCount + 2; i < Children.Count; i++)
+ {
+ yield return (FrameworkElement)Children[i];
+ }
+ }
+ }
+
+ // Helper method to get table vertical edges.
+ private IEnumerable VerticalLines
+ {
+ get
+ {
+ for (int i = 0; i < _columnCount + 1; i++)
+ {
+ yield return (Rectangle)Children[i];
+ }
+ }
+ }
+
+ // Helper method to get table horizontal edges.
+ private IEnumerable HorizontalLines
+ {
+ get
+ {
+ for (int i = _columnCount + 1; i < _columnCount + _rowCount + 2; i++)
+ {
+ yield return (Rectangle)Children[i];
+ }
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ // Measure the width of each column, with no horizontal width restrictions.
+ var naturalColumnWidths = new double[_columnCount];
+ foreach (var child in ContentChildren)
+ {
+ var columnIndex = Grid.GetColumn(child);
+ child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ naturalColumnWidths[columnIndex] = Math.Max(naturalColumnWidths[columnIndex], child.DesiredSize.Width);
+ }
+
+ // Now figure out the actual column widths.
+ var remainingContentWidth = availableSize.Width - ((_columnCount + 1) * _borderThickness);
+ _columnWidths = new double[_columnCount];
+ int remainingColumnCount = _columnCount;
+ while (remainingColumnCount > 0)
+ {
+ // Calculate the fair width of all columns.
+ double fairWidth = Math.Max(0, remainingContentWidth / remainingColumnCount);
+
+ // Are there any columns less than that? If so, they get what they are asking for.
+ bool recalculationNeeded = false;
+ for (int i = 0; i < _columnCount; i++)
+ {
+ if (_columnWidths[i] == 0 && naturalColumnWidths[i] < fairWidth)
+ {
+ _columnWidths[i] = naturalColumnWidths[i];
+ remainingColumnCount--;
+ remainingContentWidth -= _columnWidths[i];
+ recalculationNeeded = true;
+ }
+ }
+
+ // If there are no columns less than the fair width, every remaining column gets that width.
+ if (recalculationNeeded == false)
+ {
+ for (int i = 0; i < _columnCount; i++)
+ {
+ if (_columnWidths[i] == 0)
+ {
+ _columnWidths[i] = fairWidth;
+ }
+ }
+
+ break;
+ }
+ }
+
+ // TODO: we can skip this step if none of the column widths changed, and just re-use
+ // the row heights we obtained earlier.
+
+ // Now measure row heights.
+ _rowHeights = new double[_rowCount];
+ foreach (var child in ContentChildren)
+ {
+ var columnIndex = Grid.GetColumn(child);
+ var rowIndex = Grid.GetRow(child);
+ child.Measure(new Size(_columnWidths[columnIndex], double.PositiveInfinity));
+ _rowHeights[rowIndex] = Math.Max(_rowHeights[rowIndex], child.DesiredSize.Height);
+ }
+
+ return new Size(
+ _columnWidths.Sum() + (_borderThickness * (_columnCount + 1)),
+ _rowHeights.Sum() + ((_rowCount + 1) * _borderThickness));
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (_columnWidths == null || _rowHeights == null)
+ {
+ throw new InvalidOperationException("Expected Measure to be called first.");
+ }
+
+ // Arrange content.
+ foreach (var child in ContentChildren)
+ {
+ var columnIndex = Grid.GetColumn(child);
+ var rowIndex = Grid.GetRow(child);
+
+ var rect = new Rect(0, 0, 0, 0)
+ {
+ X = _borderThickness
+ };
+
+ for (int col = 0; col < columnIndex; col++)
+ {
+ rect.X += _borderThickness + _columnWidths[col];
+ }
+
+ rect.Y = _borderThickness;
+ for (int row = 0; row < rowIndex; row++)
+ {
+ rect.Y += _borderThickness + _rowHeights[row];
+ }
+
+ rect.Width = _columnWidths[columnIndex];
+ rect.Height = _rowHeights[rowIndex];
+ child.Arrange(rect);
+ }
+
+ // Arrange vertical border elements.
+ {
+ int colIndex = 0;
+ double x = 0;
+ foreach (var borderLine in VerticalLines)
+ {
+ borderLine.Arrange(new Rect(x, 0, _borderThickness, finalSize.Height));
+ if (colIndex >= _columnWidths.Length)
+ {
+ break;
+ }
+
+ x += _borderThickness + _columnWidths[colIndex];
+ colIndex++;
+ }
+ }
+
+ // Arrange horizontal border elements.
+ {
+ int rowIndex = 0;
+ double y = 0;
+ foreach (var borderLine in HorizontalLines)
+ {
+ borderLine.Arrange(new Rect(0, y, finalSize.Width, _borderThickness));
+ if (rowIndex >= _rowHeights.Length)
+ {
+ break;
+ }
+
+ y += _borderThickness + _rowHeights[rowIndex];
+ rowIndex++;
+ }
+ }
+
+ return finalSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContext.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContext.cs
new file mode 100644
index 0000000..f796b63
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContext.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// The Context of the Current Position
+ ///
+ public abstract class RenderContext : IRenderContext
+ {
+ ///
+ /// Gets or sets the Foreground of the Current Context.
+ ///
+ public Brush Foreground { get; set; }
+
+ ///
+ public bool TrimLeadingWhitespace { get; set; }
+
+ ///
+ public object Parent { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to override the Foreground Property.
+ ///
+ public bool OverrideForeground { get; set; }
+
+ ///
+ public IRenderContext Clone()
+ {
+ return (IRenderContext)MemberwiseClone();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContextIncorrectException.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContextIncorrectException.cs
new file mode 100644
index 0000000..776196d
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/RenderContextIncorrectException.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using System;
+
+ ///
+ /// An Exception that occurs when the Render Context is Incorrect.
+ ///
+ public class RenderContextIncorrectException : Exception
+ {
+ internal RenderContextIncorrectException() : base("Markdown Render Context missing or incorrect.")
+ {
+ }
+
+ public RenderContextIncorrectException(string message) : base(message)
+ {
+ }
+
+ public RenderContextIncorrectException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/UIElementCollectionRenderContext.cs b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/UIElementCollectionRenderContext.cs
new file mode 100644
index 0000000..6068039
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/Markdown/Render/UIElementCollectionRenderContext.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock/Render
+
+namespace Notepads.Controls.Markdown
+{
+ using Windows.UI.Xaml.Controls;
+
+ ///
+ /// The Context of the Current Document Rendering.
+ ///
+ public class UIElementCollectionRenderContext : RenderContext
+ {
+ internal UIElementCollectionRenderContext(UIElementCollection blockUIElementCollection)
+ {
+ BlockUIElementCollection = blockUIElementCollection;
+ }
+
+ internal UIElementCollectionRenderContext(UIElementCollection blockUIElementCollection, IRenderContext context)
+ : this(blockUIElementCollection)
+ {
+ TrimLeadingWhitespace = context.TrimLeadingWhitespace;
+ Parent = context.Parent;
+
+ if (context is RenderContext localcontext)
+ {
+ Foreground = localcontext.Foreground;
+ OverrideForeground = localcontext.OverrideForeground;
+ }
+ }
+
+ ///
+ /// Gets or sets the list to add to.
+ ///
+ public UIElementCollection BlockUIElementCollection { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownRenderedEventArgs.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownRenderedEventArgs.cs
new file mode 100644
index 0000000..faeb1ae
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownRenderedEventArgs.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// Arguments for the OnMarkdownRendered event which indicates when the markdown has been
+ /// rendered.
+ ///
+ public class MarkdownRenderedEventArgs : EventArgs
+ {
+ internal MarkdownRenderedEventArgs(Exception ex)
+ {
+ Exception = ex;
+ }
+
+ ///
+ /// Gets the exception if there was one. If the exception is null there was no error.
+ ///
+ public Exception Exception { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Dimensions.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Dimensions.cs
new file mode 100644
index 0000000..0e09a7f
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Dimensions.cs
@@ -0,0 +1,667 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Measurement Properties for elements in the Markdown.
+ ///
+ public partial class MarkdownTextBlock
+ {
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodePaddingProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodePadding),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeMarginProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodeMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeBorderThicknessProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodeBorderThickness),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ImageStretchProperty = DependencyProperty.Register(
+ nameof(ImageStretch),
+ typeof(Stretch),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(Stretch.None, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeBorderThicknessProperty = DependencyProperty.Register(
+ nameof(CodeBorderThickness),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeMarginProperty = DependencyProperty.Register(
+ nameof(CodeMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodePaddingProperty = DependencyProperty.Register(
+ nameof(CodePadding),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header1FontSizeProperty = DependencyProperty.Register(
+ nameof(Header1FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header1MarginProperty = DependencyProperty.Register(
+ nameof(Header1Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header2FontSizeProperty = DependencyProperty.Register(
+ nameof(Header2FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header2MarginProperty = DependencyProperty.Register(
+ nameof(Header2Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header3FontSizeProperty = DependencyProperty.Register(
+ nameof(Header3FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header3MarginProperty = DependencyProperty.Register(
+ nameof(Header3Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header4FontSizeProperty = DependencyProperty.Register(
+ nameof(Header4FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header4MarginProperty = DependencyProperty.Register(
+ nameof(Header4Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header5FontSizeProperty = DependencyProperty.Register(
+ nameof(Header5FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header5MarginProperty = DependencyProperty.Register(
+ nameof(Header5Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header6MarginProperty = DependencyProperty.Register(
+ nameof(Header6Margin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header6FontSizeProperty = DependencyProperty.Register(
+ nameof(Header6FontSize),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty HorizontalRuleMarginProperty = DependencyProperty.Register(
+ nameof(HorizontalRuleMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty HorizontalRuleThicknessProperty = DependencyProperty.Register(
+ nameof(HorizontalRuleThickness),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ListMarginProperty = DependencyProperty.Register(
+ nameof(ListMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ListGutterWidthProperty = DependencyProperty.Register(
+ nameof(ListGutterWidth),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ListBulletSpacingProperty = DependencyProperty.Register(
+ nameof(ListBulletSpacing),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ParagraphMarginProperty = DependencyProperty.Register(
+ nameof(ParagraphMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty ParagraphLineHeightProperty = DependencyProperty.Register(
+ nameof(ParagraphLineHeight),
+ typeof(int),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuoteBorderThicknessProperty = DependencyProperty.Register(
+ nameof(QuoteBorderThickness),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuoteMarginProperty = DependencyProperty.Register(
+ nameof(QuoteMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuotePaddingProperty = DependencyProperty.Register(
+ nameof(QuotePadding),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty YamlBorderThicknessProperty = DependencyProperty.Register(
+ nameof(YamlBorderThickness),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TableBorderThicknessProperty = DependencyProperty.Register(
+ nameof(TableBorderThickness),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TableCellPaddingProperty = DependencyProperty.Register(
+ nameof(TableCellPadding),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TableMarginProperty = DependencyProperty.Register(
+ nameof(TableMargin),
+ typeof(Thickness),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(
+ nameof(TextWrapping),
+ typeof(TextWrapping),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for
+ ///
+ public static readonly DependencyProperty ImageMaxHeightProperty = DependencyProperty.Register(
+ nameof(ImageMaxHeight),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(0.0, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for
+ ///
+ public static readonly DependencyProperty ImageMaxWidthProperty = DependencyProperty.Register(
+ nameof(ImageMaxWidth),
+ typeof(double),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(0.0, OnPropertyChangedStatic));
+
+ ///
+ /// Gets or sets the MaxWidth for images.
+ ///
+ public double ImageMaxWidth
+ {
+ get => (double)GetValue(ImageMaxWidthProperty);
+ set => SetValue(ImageMaxWidthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the MaxHeight for images.
+ ///
+ public double ImageMaxHeight
+ {
+ get => (double)GetValue(ImageMaxHeightProperty);
+ set => SetValue(ImageMaxHeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the stretch used for images.
+ ///
+ public Stretch ImageStretch
+ {
+ get => (Stretch)GetValue(ImageStretchProperty);
+ set => SetValue(ImageStretchProperty, value);
+ }
+
+ ///
+ /// Gets or sets the thickness of the border around code blocks.
+ ///
+ public Thickness CodeBorderThickness
+ {
+ get => (Thickness)GetValue(CodeBorderThicknessProperty);
+ set => SetValue(CodeBorderThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the thickness of the border for inline code.
+ ///
+ public Thickness InlineCodeBorderThickness
+ {
+ get => (Thickness)GetValue(InlineCodeBorderThicknessProperty);
+ set => SetValue(InlineCodeBorderThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the space between the code border and the text.
+ ///
+ public Thickness InlineCodePadding
+ {
+ get => (Thickness)GetValue(InlineCodePaddingProperty);
+ set => SetValue(InlineCodePaddingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for inline code.
+ ///
+ public Thickness InlineCodeMargin
+ {
+ get => (Thickness)GetValue(InlineCodeMarginProperty);
+ set => SetValue(InlineCodeMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the space between the code border and the text.
+ ///
+ public Thickness CodeMargin
+ {
+ get => (Thickness)GetValue(CodeMarginProperty);
+ set => SetValue(CodeMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets space between the code border and the text.
+ ///
+ public Thickness CodePadding
+ {
+ get => (Thickness)GetValue(CodePaddingProperty);
+ set => SetValue(CodePaddingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 1 headers.
+ ///
+ public double Header1FontSize
+ {
+ get => (double)GetValue(Header1FontSizeProperty);
+ set => SetValue(Header1FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 1 headers.
+ ///
+ public Thickness Header1Margin
+ {
+ get => (Thickness)GetValue(Header1MarginProperty);
+ set => SetValue(Header1MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 2 headers.
+ ///
+ public double Header2FontSize
+ {
+ get => (double)GetValue(Header2FontSizeProperty);
+ set => SetValue(Header2FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 2 headers.
+ ///
+ public Thickness Header2Margin
+ {
+ get => (Thickness)GetValue(Header2MarginProperty);
+ set => SetValue(Header2MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 3 headers.
+ ///
+ public double Header3FontSize
+ {
+ get => (double)GetValue(Header3FontSizeProperty);
+ set => SetValue(Header3FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 3 headers.
+ ///
+ public Thickness Header3Margin
+ {
+ get => (Thickness)GetValue(Header3MarginProperty);
+ set => SetValue(Header3MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 4 headers.
+ ///
+ public double Header4FontSize
+ {
+ get => (double)GetValue(Header4FontSizeProperty);
+ set => SetValue(Header4FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 4 headers.
+ ///
+ public Thickness Header4Margin
+ {
+ get => (Thickness)GetValue(Header4MarginProperty);
+ set => SetValue(Header4MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 5 headers.
+ ///
+ public double Header5FontSize
+ {
+ get => (double)GetValue(Header5FontSizeProperty);
+ set => SetValue(Header5FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 5 headers.
+ ///
+ public Thickness Header5Margin
+ {
+ get => (Thickness)GetValue(Header5MarginProperty);
+ set => SetValue(Header5MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font size for level 6 headers.
+ ///
+ public double Header6FontSize
+ {
+ get => (double)GetValue(Header6FontSizeProperty);
+ set => SetValue(Header6FontSizeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin for level 6 headers.
+ ///
+ public Thickness Header6Margin
+ {
+ get => (Thickness)GetValue(Header6MarginProperty);
+ set => SetValue(Header6MarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin used for horizontal rules.
+ ///
+ public Thickness HorizontalRuleMargin
+ {
+ get => (Thickness)GetValue(HorizontalRuleMarginProperty);
+ set => SetValue(HorizontalRuleMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the vertical thickness of the horizontal rule.
+ ///
+ public double HorizontalRuleThickness
+ {
+ get => (double)GetValue(HorizontalRuleThicknessProperty);
+ set => SetValue(HorizontalRuleThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin used by lists.
+ ///
+ public Thickness ListMargin
+ {
+ get => (Thickness)GetValue(ListMarginProperty);
+ set => SetValue(ListMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the width of the space used by list item bullets/numbers.
+ ///
+ public double ListGutterWidth
+ {
+ get => (double)GetValue(ListGutterWidthProperty);
+ set => SetValue(ListGutterWidthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the space between the list item bullets/numbers and the list item content.
+ ///
+ public double ListBulletSpacing
+ {
+ get => (double)GetValue(ListBulletSpacingProperty);
+ set => SetValue(ListBulletSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin used for paragraphs.
+ ///
+ public Thickness ParagraphMargin
+ {
+ get => (Thickness)GetValue(ParagraphMarginProperty);
+ set => SetValue(ParagraphMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the line height used for paragraphs.
+ ///
+ public int ParagraphLineHeight
+ {
+ get => (int)GetValue(ParagraphLineHeightProperty);
+ set => SetValue(ParagraphLineHeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the thickness of quote borders.
+ ///
+ public Thickness QuoteBorderThickness
+ {
+ get => (Thickness)GetValue(QuoteBorderThicknessProperty);
+ set => SetValue(QuoteBorderThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the space outside of quote borders.
+ ///
+ public Thickness QuoteMargin
+ {
+ get => (Thickness)GetValue(QuoteMarginProperty);
+ set => SetValue(QuoteMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the space between the quote border and the text.
+ ///
+ public Thickness QuotePadding
+ {
+ get => (Thickness)GetValue(QuotePaddingProperty);
+ set => SetValue(QuotePaddingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the thickness of any yaml header borders.
+ ///
+ public double YamlBorderThickness
+ {
+ get => (double)GetValue(YamlBorderThicknessProperty);
+ set => SetValue(YamlBorderThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the thickness of any table borders.
+ ///
+ public double TableBorderThickness
+ {
+ get => (double)GetValue(TableBorderThicknessProperty);
+ set => SetValue(TableBorderThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the padding inside each cell.
+ ///
+ public Thickness TableCellPadding
+ {
+ get => (Thickness)GetValue(TableCellPaddingProperty);
+ set => SetValue(TableCellPaddingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the margin used by tables.
+ ///
+ public Thickness TableMargin
+ {
+ get => (Thickness)GetValue(TableMarginProperty);
+ set => SetValue(TableMarginProperty, value);
+ }
+
+ ///
+ /// Gets or sets the word wrapping behavior.
+ ///
+ public TextWrapping TextWrapping
+ {
+ get => (TextWrapping)GetValue(TextWrappingProperty);
+ set => SetValue(TextWrappingProperty, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Events.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Events.cs
new file mode 100644
index 0000000..fca5840
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Events.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+
+ ///
+ /// An efficient and extensible control that can parse and render markdown.
+ ///
+ public partial class MarkdownTextBlock
+ {
+ ///
+ /// Calls OnPropertyChanged.
+ ///
+ private static void OnPropertyChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var instance = d as MarkdownTextBlock;
+
+ // Defer to the instance method.
+ instance?.OnPropertyChanged(d, e.Property);
+ }
+
+ ///
+ /// Fired when the value of a DependencyProperty is changed.
+ ///
+ private void OnPropertyChanged(DependencyObject d, DependencyProperty prop)
+ {
+ RenderMarkdown();
+ }
+
+ ///
+ /// Fired when a user taps one of the link elements
+ ///
+ private void Hyperlink_Click(Hyperlink sender, HyperlinkClickEventArgs args)
+ {
+ LinkHandled((string)sender.GetValue(HyperlinkUrlProperty), true);
+ }
+
+ ///
+ /// Fired when a user taps one of the image elements
+ ///
+ private void NewImagelink_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
+ {
+ string hyperLink = (string)(sender as Image).GetValue(HyperlinkUrlProperty);
+ bool isHyperLink = (bool)(sender as Image).GetValue(IsHyperlinkProperty);
+ LinkHandled(hyperLink, isHyperLink);
+ }
+
+ ///
+ /// Fired when the text is done parsing and formatting. Fires each time the markdown is rendered.
+ ///
+ public event EventHandler MarkdownRendered;
+
+ ///
+ /// Fired when a link element in the markdown was tapped.
+ ///
+ public event EventHandler LinkClicked;
+
+ ///
+ /// Fired when an image element in the markdown was tapped.
+ ///
+ public event EventHandler ImageClicked;
+
+ ///
+ /// Fired when an image from the markdown document needs to be resolved.
+ /// The default implementation is basically new BitmapImage(new Uri(e.Url));.
+ /// You must set to true in order to process your changes.
+ ///
+ public event EventHandler ImageResolving;
+
+ ///
+ /// Fired when a Code Block is being Rendered.
+ /// The default implementation is to output the CodeBlock as Plain Text.
+ /// You must set to true in order to process your changes.
+ ///
+ public event EventHandler CodeBlockResolving;
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Methods.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Methods.cs
new file mode 100644
index 0000000..a5ede2d
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Methods.cs
@@ -0,0 +1,376 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using ColorCode;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Media;
+ using Windows.UI.Xaml.Media.Imaging;
+ using Notepads.Controls.Markdown;
+
+ ///
+ /// An efficient and extensible control that can parse and render markdown.
+ ///
+ public partial class MarkdownTextBlock
+ {
+ ///
+ /// Sets the Markdown Renderer for Rendering the UI.
+ ///
+ /// The Inherited Markdown Render
+ public void SetRenderer()
+ where T : MarkdownRenderer
+ {
+ renderertype = typeof(T);
+ }
+
+ ///
+ /// Called to preform a render of the current Markdown.
+ ///
+ private void RenderMarkdown()
+ {
+ // Leave if we don't have our root yet.
+ if (_rootElement == null)
+ {
+ return;
+ }
+
+ // Disconnect from OnClick handlers.
+ UnhookListeners();
+
+ // Clear everything that exists.
+ _listeningHyperlinks.Clear();
+
+ var markdownRenderedArgs = new MarkdownRenderedEventArgs(null);
+
+ // Make sure we have something to parse.
+ if (string.IsNullOrWhiteSpace(Text))
+ {
+ _rootElement.Child = null;
+ }
+ else
+ {
+ try
+ {
+ // Try to parse the markdown.
+ MarkdownDocument markdown = new MarkdownDocument();
+ foreach (string str in SchemeList.Split(',').ToList())
+ {
+ if (!string.IsNullOrEmpty(str))
+ {
+ MarkdownDocument.KnownSchemes.Add(str);
+ }
+ }
+
+ markdown.Parse(Text);
+
+ // Now try to display it
+ if (!(Activator.CreateInstance(renderertype, markdown, this, this, this) is MarkdownRenderer renderer))
+ {
+ throw new Exception("Markdown Renderer was not of the correct type.");
+ }
+
+ renderer.Background = Background;
+ renderer.BorderBrush = BorderBrush;
+ renderer.BorderThickness = BorderThickness;
+ renderer.CharacterSpacing = CharacterSpacing;
+ renderer.FontFamily = FontFamily;
+ renderer.FontSize = FontSize;
+ renderer.FontStretch = FontStretch;
+ renderer.FontStyle = FontStyle;
+ renderer.FontWeight = FontWeight;
+ renderer.Foreground = Foreground;
+ renderer.IsTextSelectionEnabled = IsTextSelectionEnabled;
+ renderer.Padding = Padding;
+ renderer.CodeBackground = CodeBackground;
+ renderer.CodeBorderBrush = CodeBorderBrush;
+ renderer.CodeBorderThickness = CodeBorderThickness;
+ renderer.InlineCodeBorderThickness = InlineCodeBorderThickness;
+ renderer.InlineCodeBackground = InlineCodeBackground;
+ renderer.InlineCodeBorderBrush = InlineCodeBorderBrush;
+ renderer.InlineCodePadding = InlineCodePadding;
+ renderer.InlineCodeFontFamily = InlineCodeFontFamily;
+ renderer.InlineCodeForeground = InlineCodeForeground;
+ renderer.CodeForeground = CodeForeground;
+ renderer.CodeFontFamily = CodeFontFamily;
+ renderer.CodePadding = CodePadding;
+ renderer.CodeMargin = CodeMargin;
+ renderer.EmojiFontFamily = EmojiFontFamily;
+ renderer.Header1FontSize = Header1FontSize;
+ renderer.Header1FontWeight = Header1FontWeight;
+ renderer.Header1Margin = Header1Margin;
+ renderer.Header1Foreground = Header1Foreground;
+ renderer.Header2FontSize = Header2FontSize;
+ renderer.Header2FontWeight = Header2FontWeight;
+ renderer.Header2Margin = Header2Margin;
+ renderer.Header2Foreground = Header2Foreground;
+ renderer.Header3FontSize = Header3FontSize;
+ renderer.Header3FontWeight = Header3FontWeight;
+ renderer.Header3Margin = Header3Margin;
+ renderer.Header3Foreground = Header3Foreground;
+ renderer.Header4FontSize = Header4FontSize;
+ renderer.Header4FontWeight = Header4FontWeight;
+ renderer.Header4Margin = Header4Margin;
+ renderer.Header4Foreground = Header4Foreground;
+ renderer.Header5FontSize = Header5FontSize;
+ renderer.Header5FontWeight = Header5FontWeight;
+ renderer.Header5Margin = Header5Margin;
+ renderer.Header5Foreground = Header5Foreground;
+ renderer.Header6FontSize = Header6FontSize;
+ renderer.Header6FontWeight = Header6FontWeight;
+ renderer.Header6Margin = Header6Margin;
+ renderer.Header6Foreground = Header6Foreground;
+ renderer.HorizontalRuleBrush = HorizontalRuleBrush;
+ renderer.HorizontalRuleMargin = HorizontalRuleMargin;
+ renderer.HorizontalRuleThickness = HorizontalRuleThickness;
+ renderer.ListMargin = ListMargin;
+ renderer.ListGutterWidth = ListGutterWidth;
+ renderer.ListBulletSpacing = ListBulletSpacing;
+ renderer.ParagraphMargin = ParagraphMargin;
+ renderer.ParagraphLineHeight = ParagraphLineHeight;
+ renderer.QuoteBackground = QuoteBackground;
+ renderer.QuoteBorderBrush = QuoteBorderBrush;
+ renderer.QuoteBorderThickness = QuoteBorderThickness;
+ renderer.QuoteForeground = QuoteForeground;
+ renderer.QuoteMargin = QuoteMargin;
+ renderer.QuotePadding = QuotePadding;
+ renderer.TableBorderBrush = TableBorderBrush;
+ renderer.TableBorderThickness = TableBorderThickness;
+ renderer.YamlBorderBrush = YamlBorderBrush;
+ renderer.YamlBorderThickness = YamlBorderThickness;
+ renderer.TableCellPadding = TableCellPadding;
+ renderer.TableMargin = TableMargin;
+ renderer.TextWrapping = TextWrapping;
+ renderer.LinkForeground = LinkForeground;
+ renderer.ImageStretch = ImageStretch;
+ renderer.ImageMaxHeight = ImageMaxHeight;
+ renderer.ImageMaxWidth = ImageMaxWidth;
+ renderer.WrapCodeBlock = WrapCodeBlock;
+ renderer.FlowDirection = FlowDirection;
+
+ _rootElement.Child = renderer.Render();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("Error while parsing and rendering: " + ex.Message);
+ if (Debugger.IsAttached)
+ {
+ Debugger.Break();
+ }
+
+ markdownRenderedArgs = new MarkdownRenderedEventArgs(ex);
+ }
+ }
+
+ // Indicate that the parse is done.
+ MarkdownRendered?.Invoke(this, markdownRenderedArgs);
+ }
+
+ private void HookListeners()
+ {
+ // Re-hook all hyper link events we currently have
+ foreach (object link in _listeningHyperlinks)
+ {
+ if (link is Hyperlink hyperlink)
+ {
+ hyperlink.Click -= Hyperlink_Click;
+ hyperlink.Click += Hyperlink_Click;
+ }
+ else if (link is Image image)
+ {
+ image.Tapped -= NewImagelink_Tapped;
+ image.Tapped += NewImagelink_Tapped;
+ }
+ }
+ }
+
+ private void UnhookListeners()
+ {
+ // Unhook any hyper link events if we have any
+ foreach (object link in _listeningHyperlinks)
+ {
+ if (link is Hyperlink hyperlink)
+ {
+ hyperlink.Click -= Hyperlink_Click;
+ }
+ else if (link is Image image)
+ {
+ image.Tapped -= NewImagelink_Tapped;
+ }
+ }
+ }
+
+ ///
+ /// Called when the render has a link we need to listen to.
+ ///
+ public void RegisterNewHyperLink(Hyperlink newHyperlink, string linkUrl)
+ {
+ // Setup a listener for clicks.
+ newHyperlink.Click += Hyperlink_Click;
+
+ // Associate the URL with the hyperlink.
+ newHyperlink.SetValue(HyperlinkUrlProperty, linkUrl);
+
+ // Add it to our list
+ _listeningHyperlinks.Add(newHyperlink);
+ }
+
+ ///
+ /// Called when the render has a link we need to listen to.
+ ///
+ public void RegisterNewHyperLink(Image newImagelink, string linkUrl, bool isHyperLink)
+ {
+ // Setup a listener for clicks.
+ newImagelink.Tapped += NewImagelink_Tapped;
+
+ // Associate the URL with the hyperlink.
+ newImagelink.SetValue(HyperlinkUrlProperty, linkUrl);
+
+ // Set if the Image is HyperLink or not
+ newImagelink.SetValue(IsHyperlinkProperty, isHyperLink);
+
+ // Add it to our list
+ _listeningHyperlinks.Add(newImagelink);
+ }
+
+ ///
+ /// Called when the renderer needs to display a image.
+ ///
+ /// A representing the asynchronous operation.
+ async Task IImageResolver.ResolveImageAsync(string url, string tooltip)
+ {
+ if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
+ {
+ if (!string.IsNullOrEmpty(UriPrefix))
+ {
+ url = $"{UriPrefix}{url}";
+ }
+ }
+
+ var eventArgs = new ImageResolvingEventArgs(url, tooltip);
+ ImageResolving?.Invoke(this, eventArgs);
+
+ await eventArgs.WaitForDeferrals();
+
+ try
+ {
+ return eventArgs.Handled
+ ? eventArgs.Image
+ : GetImageSource(new Uri(url));
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+
+ ImageSource GetImageSource(Uri imageUrl)
+ {
+ if (_isSvgImageSupported)
+ {
+ if (Path.GetExtension(imageUrl.AbsolutePath)?.ToLowerInvariant() == ".svg")
+ {
+ return new SvgImageSource(imageUrl);
+ }
+ }
+
+ return new BitmapImage(imageUrl);
+ }
+ }
+
+ ///
+ /// Called when a Code Block is being rendered.
+ ///
+ /// Parsing was handled Successfully
+ bool ICodeBlockResolver.ParseSyntax(InlineCollection inlineCollection, string text, string codeLanguage)
+ {
+ var eventArgs = new CodeBlockResolvingEventArgs(inlineCollection, text, codeLanguage);
+ CodeBlockResolving?.Invoke(this, eventArgs);
+
+ try
+ {
+ var result = eventArgs.Handled;
+ if (UseSyntaxHighlighting && !result && codeLanguage != null)
+ {
+ var language = Languages.FindById(codeLanguage);
+ if (language != null)
+ {
+ RichTextBlockFormatter formatter;
+ if (CodeStyling != null)
+ {
+ formatter = new RichTextBlockFormatter(CodeStyling);
+ }
+ else
+ {
+ //var theme = themeListener.CurrentTheme == ApplicationTheme.Dark ? ElementTheme.Dark : ElementTheme.Light;
+ //if (RequestedTheme != ElementTheme.Default)
+ //{
+ // theme = RequestedTheme;
+ //}
+
+ var theme = ActualTheme;
+ if (RequestedTheme != ElementTheme.Default)
+ {
+ theme = RequestedTheme;
+ }
+
+ formatter = new RichTextBlockFormatter(theme);
+ }
+
+ formatter.FormatInlines(text, language, inlineCollection);
+ return true;
+ }
+ }
+
+ return result;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Called when a link needs to be handled
+ ///
+ internal async void LinkHandled(string url, bool isHyperlink)
+ {
+ // Links that are nested within superscript elements cause the Click event to fire multiple times.
+ // e.g. this markdown "[^bot](http://www.reddit.com/r/youtubefactsbot/wiki/index)"
+ // Therefore we detect and ignore multiple clicks.
+ if (multiClickDetectionTriggered)
+ {
+ return;
+ }
+
+ multiClickDetectionTriggered = true;
+ await Dispatcher.RunAsync(CoreDispatcherPriority.High, () => multiClickDetectionTriggered = false);
+
+ // Get the hyperlink URL.
+ if (url == null)
+ {
+ return;
+ }
+
+ // Fire off the event.
+ var eventArgs = new LinkClickedEventArgs(url);
+ if (isHyperlink)
+ {
+ LinkClicked?.Invoke(this, eventArgs);
+ }
+ else
+ {
+ ImageClicked?.Invoke(this, eventArgs);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Properties.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Properties.cs
new file mode 100644
index 0000000..ae95a32
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.Properties.cs
@@ -0,0 +1,692 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using System;
+ using System.Collections.Generic;
+ using ColorCode.Styling;
+ using Windows.Foundation.Metadata;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+ using Notepads.Controls.Markdown;
+
+ ///
+ /// An efficient and extensible control that can parse and render markdown.
+ ///
+ public partial class MarkdownTextBlock
+ {
+ // SvgImageSource was introduced in Creators Update (15063)
+ private static readonly bool _isSvgImageSupported = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 4);
+
+ // Used to attach the URL to hyperlinks.
+ private static readonly DependencyProperty HyperlinkUrlProperty =
+ DependencyProperty.RegisterAttached("HyperlinkUrl", typeof(string), typeof(MarkdownTextBlock), new PropertyMetadata(null));
+
+ // Checks if clicked image is a hyperlink or not.
+ private static readonly DependencyProperty IsHyperlinkProperty =
+ DependencyProperty.RegisterAttached("IsHyperLink", typeof(string), typeof(MarkdownTextBlock), new PropertyMetadata(null));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeStylingProperty =
+ DependencyProperty.Register(
+ nameof(CodeStyling),
+ typeof(StyleDictionary),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty UseSyntaxHighlightingProperty =
+ DependencyProperty.Register(
+ nameof(UseSyntaxHighlighting),
+ typeof(bool),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(true, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty WrapCodeBlockProperty =
+ DependencyProperty.Register(nameof(WrapCodeBlock), typeof(bool), typeof(MarkdownTextBlock), new PropertyMetadata(false));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
+ nameof(Text),
+ typeof(string),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(string.Empty, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeBackgroundProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodeBackground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeForegroundProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodeForeground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeBorderBrushProperty =
+ DependencyProperty.Register(
+ nameof(InlineCodeBorderBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty IsTextSelectionEnabledProperty = DependencyProperty.Register(
+ nameof(IsTextSelectionEnabled),
+ typeof(bool),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(true, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty LinkForegroundProperty = DependencyProperty.Register(
+ nameof(LinkForeground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeBackgroundProperty = DependencyProperty.Register(
+ nameof(CodeBackground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeBorderBrushProperty = DependencyProperty.Register(
+ nameof(CodeBorderBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeForegroundProperty = DependencyProperty.Register(
+ nameof(CodeForeground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty CodeFontFamilyProperty = DependencyProperty.Register(
+ nameof(CodeFontFamily),
+ typeof(FontFamily),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty InlineCodeFontFamilyProperty = DependencyProperty.Register(
+ nameof(InlineCodeFontFamily),
+ typeof(FontFamily),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty EmojiFontFamilyProperty = DependencyProperty.Register(
+ nameof(EmojiFontFamily),
+ typeof(FontFamily),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header1FontWeightProperty = DependencyProperty.Register(
+ nameof(Header1FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header1ForegroundProperty = DependencyProperty.Register(
+ nameof(Header1Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header2FontWeightProperty = DependencyProperty.Register(
+ nameof(Header2FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header2ForegroundProperty = DependencyProperty.Register(
+ nameof(Header2Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header3FontWeightProperty = DependencyProperty.Register(
+ nameof(Header3FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header3ForegroundProperty = DependencyProperty.Register(
+ nameof(Header3Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header4FontWeightProperty = DependencyProperty.Register(
+ nameof(Header4FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header4ForegroundProperty = DependencyProperty.Register(
+ nameof(Header4Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header5FontWeightProperty = DependencyProperty.Register(
+ nameof(Header5FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header5ForegroundProperty = DependencyProperty.Register(
+ nameof(Header5Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header6FontWeightProperty = DependencyProperty.Register(
+ nameof(Header6FontWeight),
+ typeof(FontWeight),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty Header6ForegroundProperty = DependencyProperty.Register(
+ nameof(Header6Foreground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty HorizontalRuleBrushProperty = DependencyProperty.Register(
+ nameof(HorizontalRuleBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuoteBackgroundProperty = DependencyProperty.Register(
+ nameof(QuoteBackground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuoteBorderBrushProperty = DependencyProperty.Register(
+ nameof(QuoteBorderBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty QuoteForegroundProperty = DependencyProperty.Register(
+ nameof(QuoteForeground),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty TableBorderBrushProperty = DependencyProperty.Register(
+ nameof(TableBorderBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty YamlBorderBrushProperty = DependencyProperty.Register(
+ nameof(YamlBorderBrush),
+ typeof(Brush),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(null, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty UriPrefixProperty = DependencyProperty.Register(
+ nameof(UriPrefix),
+ typeof(string),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(string.Empty, OnPropertyChangedStatic));
+
+ ///
+ /// Gets the dependency property for .
+ ///
+ public static readonly DependencyProperty SchemeListProperty = DependencyProperty.Register(
+ nameof(SchemeList),
+ typeof(string),
+ typeof(MarkdownTextBlock),
+ new PropertyMetadata(string.Empty, OnPropertyChangedStatic));
+
+ ///
+ /// Gets or sets the markdown text to display.
+ ///
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to use Syntax Highlighting on Code.
+ ///
+ public bool UseSyntaxHighlighting
+ {
+ get => (bool)GetValue(UseSyntaxHighlightingProperty);
+ set => SetValue(UseSyntaxHighlightingProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to Wrap the Code Block or use a Horizontal Scroll.
+ ///
+ public bool WrapCodeBlock
+ {
+ get => (bool)GetValue(WrapCodeBlockProperty);
+ set => SetValue(WrapCodeBlockProperty, value);
+ }
+
+ ///
+ /// Gets or sets the Default Code Styling for Code Blocks.
+ ///
+ public StyleDictionary CodeStyling
+ {
+ get => (StyleDictionary)GetValue(CodeStylingProperty);
+ set => SetValue(CodeStylingProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether text selection is enabled.
+ ///
+ public bool IsTextSelectionEnabled
+ {
+ get => (bool)GetValue(IsTextSelectionEnabledProperty);
+ set => SetValue(IsTextSelectionEnabledProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render links. If this is
+ /// null , then Foreground is used.
+ ///
+ public Brush LinkForeground
+ {
+ get => (Brush)GetValue(LinkForegroundProperty);
+ set => SetValue(LinkForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to fill the background of a code block.
+ ///
+ public Brush CodeBackground
+ {
+ get => (Brush)GetValue(CodeBackgroundProperty);
+ set => SetValue(CodeBackgroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render the border fill of a code block.
+ ///
+ public Brush CodeBorderBrush
+ {
+ get => (Brush)GetValue(CodeBorderBrushProperty);
+ set => SetValue(CodeBorderBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render the text inside a code block. If this is
+ /// null , then Foreground is used.
+ ///
+ public Brush CodeForeground
+ {
+ get => (Brush)GetValue(CodeForegroundProperty);
+ set => SetValue(CodeForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font used to display code. If this is null , then
+ /// is used.
+ ///
+ public FontFamily CodeFontFamily
+ {
+ get => (FontFamily)GetValue(CodeFontFamilyProperty);
+ set => SetValue(CodeFontFamilyProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font used to display code. If this is null , then
+ /// is used.
+ ///
+ public FontFamily InlineCodeFontFamily
+ {
+ get => (FontFamily)GetValue(InlineCodeFontFamilyProperty);
+ set => SetValue(InlineCodeFontFamilyProperty, value);
+ }
+
+ ///
+ /// Gets or sets the background brush for inline code.
+ ///
+ public Brush InlineCodeBackground
+ {
+ get => (Brush)GetValue(InlineCodeBackgroundProperty);
+ set => SetValue(InlineCodeBackgroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for inline code.
+ ///
+ public Brush InlineCodeForeground
+ {
+ get => (Brush)GetValue(InlineCodeForegroundProperty);
+ set => SetValue(InlineCodeForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the border brush for inline code.
+ ///
+ public Brush InlineCodeBorderBrush
+ {
+ get => (Brush)GetValue(InlineCodeBorderBrushProperty);
+ set => SetValue(InlineCodeBorderBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font used to display emojis. If this is null , then
+ /// Segoe UI Emoji font is used.
+ ///
+ public FontFamily EmojiFontFamily
+ {
+ get => (FontFamily)GetValue(EmojiFontFamilyProperty);
+ set => SetValue(EmojiFontFamilyProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 1 headers.
+ ///
+ public FontWeight Header1FontWeight
+ {
+ get => (FontWeight)GetValue(Header1FontWeightProperty);
+ set => SetValue(Header1FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 1 headers.
+ ///
+ public Brush Header1Foreground
+ {
+ get => (Brush)GetValue(Header1ForegroundProperty);
+ set => SetValue(Header1ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 2 headers.
+ ///
+ public FontWeight Header2FontWeight
+ {
+ get => (FontWeight)GetValue(Header2FontWeightProperty);
+ set => SetValue(Header2FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 2 headers.
+ ///
+ public Brush Header2Foreground
+ {
+ get => (Brush)GetValue(Header2ForegroundProperty);
+ set => SetValue(Header2ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 3 headers.
+ ///
+ public FontWeight Header3FontWeight
+ {
+ get => (FontWeight)GetValue(Header3FontWeightProperty);
+ set => SetValue(Header3FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 3 headers.
+ ///
+ public Brush Header3Foreground
+ {
+ get => (Brush)GetValue(Header3ForegroundProperty);
+ set => SetValue(Header3ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 4 headers.
+ ///
+ public FontWeight Header4FontWeight
+ {
+ get => (FontWeight)GetValue(Header4FontWeightProperty);
+ set => SetValue(Header4FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 4 headers.
+ ///
+ public Brush Header4Foreground
+ {
+ get => (Brush)GetValue(Header4ForegroundProperty);
+ set => SetValue(Header4ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 5 headers.
+ ///
+ public FontWeight Header5FontWeight
+ {
+ get => (FontWeight)GetValue(Header5FontWeightProperty);
+ set => SetValue(Header5FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 5 headers.
+ ///
+ public Brush Header5Foreground
+ {
+ get => (Brush)GetValue(Header5ForegroundProperty);
+ set => SetValue(Header5ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the font weight to use for level 6 headers.
+ ///
+ public FontWeight Header6FontWeight
+ {
+ get => (FontWeight)GetValue(Header6FontWeightProperty);
+ set => SetValue(Header6FontWeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the foreground brush for level 6 headers.
+ ///
+ public Brush Header6Foreground
+ {
+ get => (Brush)GetValue(Header6ForegroundProperty);
+ set => SetValue(Header6ForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render a horizontal rule. If this is null , then
+ /// is used.
+ ///
+ public Brush HorizontalRuleBrush
+ {
+ get => (Brush)GetValue(HorizontalRuleBrushProperty);
+ set => SetValue(HorizontalRuleBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to fill the background of a quote block.
+ ///
+ public Brush QuoteBackground
+ {
+ get => (Brush)GetValue(QuoteBackgroundProperty);
+ set => SetValue(QuoteBackgroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render a quote border. If this is null , then
+ /// is used.
+ ///
+ public Brush QuoteBorderBrush
+ {
+ get => (Brush)GetValue(QuoteBorderBrushProperty);
+ set => SetValue(QuoteBorderBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render the text inside a quote block. If this is
+ /// null , then Foreground is used.
+ ///
+ public Brush QuoteForeground
+ {
+ get => (Brush)GetValue(QuoteForegroundProperty);
+ set => SetValue(QuoteForegroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render table borders. If this is null , then
+ /// is used.
+ ///
+ public Brush TableBorderBrush
+ {
+ get => (Brush)GetValue(TableBorderBrushProperty);
+ set => SetValue(TableBorderBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the brush used to render yaml borders. If this is null , then
+ /// is used.
+ ///
+ public Brush YamlBorderBrush
+ {
+ get => (Brush)GetValue(TableBorderBrushProperty);
+ set => SetValue(TableBorderBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the Prefix of Uri.
+ ///
+ public string UriPrefix
+ {
+ get => (string)GetValue(UriPrefixProperty);
+ set => SetValue(UriPrefixProperty, value);
+ }
+
+ ///
+ /// Gets or sets the SchemeList.
+ ///
+ public string SchemeList
+ {
+ get => (string)GetValue(SchemeListProperty);
+ set => SetValue(SchemeListProperty, value);
+ }
+
+ ///
+ /// Holds a list of hyperlinks we are listening to.
+ ///
+ private readonly List _listeningHyperlinks = new List();
+
+ ///
+ /// The root element for our rendering.
+ ///
+ private Border _rootElement;
+
+ private bool multiClickDetectionTriggered;
+
+ private Type renderertype = typeof(MarkdownRenderer);
+
+ //private ThemeListener themeListener;
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.cs b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.cs
new file mode 100644
index 0000000..c39cec7
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.cs
@@ -0,0 +1,130 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Notepads.Controls.Markdown;
+
+ ///
+ /// An efficient and extensible control that can parse and render markdown.
+ ///
+ public partial class MarkdownTextBlock : Control, ILinkRegister, IImageResolver, ICodeBlockResolver
+ {
+ private long _fontSizePropertyToken;
+ private long _flowDirectionPropertyToken;
+ private long _backgroundPropertyToken;
+ private long _borderBrushPropertyToken;
+ private long _borderThicknessPropertyToken;
+ private long _characterSpacingPropertyToken;
+ private long _fontFamilyPropertyToken;
+ private long _fontStretchPropertyToken;
+ private long _fontStylePropertyToken;
+ private long _fontWeightPropertyToken;
+ private long _foregroundPropertyToken;
+ private long _paddingPropertyToken;
+ private long _requestedThemePropertyToken;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MarkdownTextBlock()
+ {
+ // Set our style.
+ DefaultStyleKey = typeof(MarkdownTextBlock);
+
+ Loaded += OnLoaded;
+ Unloaded += OnUnloaded;
+ }
+
+ //private void ThemeListener_ThemeChanged(Helpers.ThemeListener sender)
+ //{
+ // if (ToElementTheme(sender.CurrentTheme) != RequestedTheme)
+ // {
+ // RenderMarkdown();
+ // }
+ //}
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ RegisterThemeChangedHandler();
+ HookListeners();
+
+ // Register for property callbacks that are owned by our parent class.
+ _fontSizePropertyToken = RegisterPropertyChangedCallback(FontSizeProperty, OnPropertyChanged);
+ _flowDirectionPropertyToken = RegisterPropertyChangedCallback(FlowDirectionProperty, OnPropertyChanged);
+ _backgroundPropertyToken = RegisterPropertyChangedCallback(BackgroundProperty, OnPropertyChanged);
+ _borderBrushPropertyToken = RegisterPropertyChangedCallback(BorderBrushProperty, OnPropertyChanged);
+ _borderThicknessPropertyToken = RegisterPropertyChangedCallback(BorderThicknessProperty, OnPropertyChanged);
+ _characterSpacingPropertyToken = RegisterPropertyChangedCallback(CharacterSpacingProperty, OnPropertyChanged);
+ _fontFamilyPropertyToken = RegisterPropertyChangedCallback(FontFamilyProperty, OnPropertyChanged);
+ _fontStretchPropertyToken = RegisterPropertyChangedCallback(FontStretchProperty, OnPropertyChanged);
+ _fontStylePropertyToken = RegisterPropertyChangedCallback(FontStyleProperty, OnPropertyChanged);
+ _fontWeightPropertyToken = RegisterPropertyChangedCallback(FontWeightProperty, OnPropertyChanged);
+ _foregroundPropertyToken = RegisterPropertyChangedCallback(ForegroundProperty, OnPropertyChanged);
+ _paddingPropertyToken = RegisterPropertyChangedCallback(PaddingProperty, OnPropertyChanged);
+ _requestedThemePropertyToken = RegisterPropertyChangedCallback(RequestedThemeProperty, OnPropertyChanged);
+ }
+
+ private void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ //if (themeListener != null)
+ //{
+ // UnhookListeners();
+ // themeListener.ThemeChanged -= ThemeListener_ThemeChanged;
+ // themeListener.Dispose();
+ // themeListener = null;
+ //}
+
+ // Register for property callbacks that are owned by our parent class.
+ UnregisterPropertyChangedCallback(FontSizeProperty, _fontSizePropertyToken);
+ UnregisterPropertyChangedCallback(FlowDirectionProperty, _flowDirectionPropertyToken);
+ UnregisterPropertyChangedCallback(BackgroundProperty, _backgroundPropertyToken);
+ UnregisterPropertyChangedCallback(BorderBrushProperty, _borderBrushPropertyToken);
+ UnregisterPropertyChangedCallback(BorderThicknessProperty, _borderThicknessPropertyToken);
+ UnregisterPropertyChangedCallback(CharacterSpacingProperty, _characterSpacingPropertyToken);
+ UnregisterPropertyChangedCallback(FontFamilyProperty, _fontFamilyPropertyToken);
+ UnregisterPropertyChangedCallback(FontStretchProperty, _fontStretchPropertyToken);
+ UnregisterPropertyChangedCallback(FontStyleProperty, _fontStylePropertyToken);
+ UnregisterPropertyChangedCallback(FontWeightProperty, _fontWeightPropertyToken);
+ UnregisterPropertyChangedCallback(ForegroundProperty, _foregroundPropertyToken);
+ UnregisterPropertyChangedCallback(PaddingProperty, _paddingPropertyToken);
+ UnregisterPropertyChangedCallback(RequestedThemeProperty, _requestedThemePropertyToken);
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ RegisterThemeChangedHandler();
+
+ // Grab our root
+ _rootElement = GetTemplateChild("RootElement") as Border;
+
+ // And make sure to render any markdown we have.
+ RenderMarkdown();
+ }
+
+ private void RegisterThemeChangedHandler()
+ {
+ //themeListener = themeListener ?? new ThemeListener();
+ //themeListener.ThemeChanged -= ThemeListener_ThemeChanged;
+ //themeListener.ThemeChanged += ThemeListener_ThemeChanged;
+ }
+
+ //public static ElementTheme ToElementTheme(ApplicationTheme theme)
+ //{
+ // switch (theme)
+ // {
+ // case ApplicationTheme.Light:
+ // return ElementTheme.Light;
+ // case ApplicationTheme.Dark:
+ // return ElementTheme.Dark;
+ // default:
+ // return ElementTheme.Default;
+ // }
+ //}
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.xaml b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.xaml
new file mode 100644
index 0000000..c6e00de
--- /dev/null
+++ b/src/src/Notepads.Controls/MarkdownTextBlock/MarkdownTextBlock.xaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Notepads.Controls.csproj b/src/src/Notepads.Controls/Notepads.Controls.csproj
new file mode 100644
index 0000000..5fc4126
--- /dev/null
+++ b/src/src/Notepads.Controls/Notepads.Controls.csproj
@@ -0,0 +1,338 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {7AA5E631-B663-420E-A08F-002CD81DF855}
+ Library
+ Properties
+ Notepads.Controls
+ Notepads.Controls
+ en-US
+ UAP
+ 10.0.22621.0
+ 10.0.17763.0
+ 14
+ 512
+ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Production\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ prompt
+ 4
+
+
+ x86
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ full
+ false
+ prompt
+
+
+ x86
+ bin\x86\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ x86
+ bin\x86\Production\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ ARM
+ true
+ bin\ARM\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ full
+ false
+ prompt
+
+
+ ARM
+ bin\ARM\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ ARM
+ bin\ARM\Production\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ ARM64
+ true
+ bin\ARM64\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ full
+ false
+ prompt
+
+
+ ARM64
+ bin\ARM64\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ ARM64
+ bin\ARM64\Production\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ x64
+ true
+ bin\x64\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ full
+ false
+ prompt
+
+
+ x64
+ bin\x64\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ x64
+ bin\x64\Production\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ false
+ prompt
+
+
+ PackageReference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2.0.15
+
+
+ 6.2.14
+
+
+ 7.1.3
+
+
+ 1.28.2
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Designer
+ MSBuild:Compile
+
+
+
+
+ 14.0
+
+
+
+
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Properties/AssemblyInfo.cs b/src/src/Notepads.Controls/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..dc09e44
--- /dev/null
+++ b/src/src/Notepads.Controls/Properties/AssemblyInfo.cs
@@ -0,0 +1,28 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Notepads.Controls")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Notepads.Controls")]
+[assembly: AssemblyCopyright("Copyright © 2019-2024 Jackie (Jiaqi) Liu")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: ComVisible(false)]
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Properties/Notepads.Controls.rd.xml b/src/src/Notepads.Controls/Properties/Notepads.Controls.rd.xml
new file mode 100644
index 0000000..49f7e13
--- /dev/null
+++ b/src/src/Notepads.Controls/Properties/Notepads.Controls.rd.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.Controls/SetsView/SetClosingEventArgs.cs b/src/src/Notepads.Controls/SetsView/SetClosingEventArgs.cs
new file mode 100644
index 0000000..c09a247
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetClosingEventArgs.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// Event arguments for event.
+ ///
+ public class SetClosingEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Item being closed.
+ /// container being closed.
+ public SetClosingEventArgs(object item, SetsViewItem set)
+ {
+ Item = item;
+ Set = set;
+ }
+
+ ///
+ /// Gets the Item being closed.
+ ///
+ public object Item { get; }
+
+ ///
+ /// Gets the Set being closed.
+ ///
+ public SetsViewItem Set { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the notification should be closed.
+ ///
+ public bool Cancel { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetDraggedOutsideEventArgs.cs b/src/src/Notepads.Controls/SetsView/SetDraggedOutsideEventArgs.cs
new file mode 100644
index 0000000..058bc07
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetDraggedOutsideEventArgs.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// A class used by the TabDraggedOutside Event
+ ///
+ public class SetDraggedOutsideEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// data context of element dragged
+ /// container being dragged.
+ public SetDraggedOutsideEventArgs(object item, SetsViewItem set)
+ {
+ Item = item;
+ Set = set;
+ }
+
+ ///
+ /// Gets or sets the Item/Data Context of the item being dragged outside of the .
+ ///
+ public object Item { get; set; }
+
+ ///
+ /// Gets the Set being dragged outside of the .
+ ///
+ public SetsViewItem Set { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetSelectedEventArgs.cs b/src/src/Notepads.Controls/SetsView/SetSelectedEventArgs.cs
new file mode 100644
index 0000000..b42981e
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetSelectedEventArgs.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System;
+
+ ///
+ /// Event arguments for event.
+ ///
+ public class SetSelectedEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Selected item.
+ /// Selected set.
+ public SetSelectedEventArgs(object item, SetsViewItem set)
+ {
+ Item = item;
+ Set = set;
+ }
+
+ ///
+ /// Gets the Selected Item.
+ ///
+ public object Item { get; }
+
+ ///
+ /// Gets the Selected Set.
+ ///
+ public SetsViewItem Set { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsView.HeaderLayout.cs b/src/src/Notepads.Controls/SetsView/SetsView.HeaderLayout.cs
new file mode 100644
index 0000000..27707bd
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsView.HeaderLayout.cs
@@ -0,0 +1,220 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System;
+ using System.Linq;
+ using Windows.UI.Xaml;
+
+ ///
+ /// SetsView methods related to calculating the width of the Headers.
+ ///
+ public partial class SetsView
+ {
+ // Attached property for storing widths of sets if set by other means during layout.
+ private static double GetOriginalWidth(SetsViewItem obj)
+ {
+ return (double)obj.GetValue(OriginalWidthProperty);
+ }
+
+ private static void SetOriginalWidth(SetsViewItem obj, double value)
+ {
+ obj.SetValue(OriginalWidthProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
+ private static readonly DependencyProperty OriginalWidthProperty =
+ DependencyProperty.RegisterAttached("OriginalWidth", typeof(double), typeof(SetsView), new PropertyMetadata(null));
+
+ private static void OnLayoutEffectingPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ if (sender is SetsView setsView && setsView._hasLoaded)
+ {
+ setsView.SetsView_SizeChanged(setsView, null);
+ }
+ }
+
+ private void SetsView_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ // We need to do this calculation here in Size Changed as the
+ // Columns don't have their Actual Size calculated in Measure or Arrange.
+ if (_hasLoaded && _setsViewContainer != null)
+ {
+ // Look for our special columns to calculate size of other 'stuff'
+ var taken = _setsViewContainer.ColumnDefinitions.Sum(cd => GetIgnoreColumn(cd) ? 0 : cd.ActualWidth);
+
+ // Get the column we want to work on for available space
+ var setc = _setsViewContainer.ColumnDefinitions.FirstOrDefault(cd => GetConstrainColumn(cd));
+ if (setc != null)
+ {
+ var available = ActualWidth - taken;
+ var required = 0.0;
+ var minsetwidth = double.MaxValue;
+
+ if (SetsWidthBehavior == SetsWidthMode.Actual)
+ {
+ if (_setsScroller != null)
+ {
+ // If we have a scroll container, get its size.
+ required = _setsScroller.ExtentWidth;
+ }
+
+ // Restore original widths
+ foreach (var item in Items)
+ {
+ if (!(ContainerFromItem(item) is SetsViewItem set))
+ {
+ continue; // container not generated yet
+ }
+
+ if (set.ReadLocalValue(OriginalWidthProperty) != DependencyProperty.UnsetValue)
+ {
+ set.Width = GetOriginalWidth(set);
+ }
+ }
+ }
+ else if (available > 0)
+ {
+ // Calculate the width for each set from the provider and determine how much space they take.
+ foreach (var item in Items)
+ {
+ if (!(ContainerFromItem(item) is SetsViewItem set))
+ {
+ continue; // container not generated yet
+ }
+
+ minsetwidth = Math.Min(minsetwidth, set.MinWidth);
+
+ double width = double.NaN;
+
+ switch (SetsWidthBehavior)
+ {
+ case SetsWidthMode.Equal:
+ width = ProvideEqualWidth(set, available);
+ break;
+ case SetsWidthMode.Compact:
+ width = ProvideCompactWidth(set);
+ break;
+ }
+
+ if (set.ReadLocalValue(OriginalWidthProperty) == DependencyProperty.UnsetValue)
+ {
+ SetOriginalWidth(set, set.Width);
+ }
+
+ if (width > double.Epsilon)
+ {
+ set.Width = width;
+ required += Math.Max(Math.Min(width, set.MaxWidth), set.MinWidth);
+ }
+ else
+ {
+ set.Width = GetOriginalWidth(set);
+ required += set.ActualWidth;
+ }
+ }
+ }
+ else
+ {
+ // Fix negative bounds.
+ available = 0.0;
+
+ // Still need to determine a 'minimum' width (if available)
+ // TODO: Consolidate this logic with above better?
+ foreach (var item in Items)
+ {
+ if (!(ContainerFromItem(item) is SetsViewItem set))
+ {
+ continue; // container not generated yet
+ }
+
+ minsetwidth = Math.Min(minsetwidth, set.MinWidth);
+ }
+ }
+
+ if (!(minsetwidth < double.MaxValue))
+ {
+ minsetwidth = 0.0; // No Containers, no visual, 0 size.
+ }
+
+ if (available > minsetwidth)
+ {
+ // Constrain the column based on our required and available space
+ setc.MaxWidth = available;
+ }
+
+ //// TODO: If it's less, should we move the selected set to only be the one shown by default?
+
+ if (available <= minsetwidth || Math.Abs(available - minsetwidth) < double.Epsilon)
+ {
+ setc.Width = new GridLength(minsetwidth);
+ }
+ else if (required >= available)
+ {
+ // Fix size as we don't have enough space for all the sets.
+ setc.Width = new GridLength(available);
+ }
+ else
+ {
+ // We haven't filled up our space, so we want to expand to take as much as needed.
+ setc.Width = GridLength.Auto;
+ }
+ }
+ UpdateScrollViewerShadows();
+ UpdateScrollViewerNavigateButtons();
+ UpdateSetSeparators();
+ }
+ }
+
+ private double ProvideEqualWidth(SetsViewItem set, double availableWidth)
+ {
+ if (double.IsNaN(SelectedSetWidth))
+ {
+ if (Items.Count <= 1)
+ {
+ return availableWidth;
+ }
+
+ return Math.Max(set.MinWidth, availableWidth / Items.Count);
+ }
+ else if (Items.Count() <= 1)
+ {
+ // Default case of a single set, make it full size.
+ return Math.Min(SelectedSetWidth, availableWidth);
+ }
+ else
+ {
+ var width = (availableWidth - SelectedSetWidth) / (Items.Count - 1);
+
+ // Constrain between Min and Selected (Max)
+ if (width < set.MinWidth)
+ {
+ width = set.MinWidth;
+ }
+ else if (width > SelectedSetWidth)
+ {
+ width = SelectedSetWidth;
+ }
+
+ // If it's selected make it full size, otherwise whatever the size should be.
+ return set.IsSelected
+ ? Math.Min(SelectedSetWidth, availableWidth)
+ : width;
+ }
+ }
+
+ private double ProvideCompactWidth(SetsViewItem set)
+ {
+ // If we're selected and have a value for that, then just return that.
+ if (set.IsSelected && !double.IsNaN(SelectedSetWidth))
+ {
+ return SelectedSetWidth;
+ }
+
+ // Otherwise use min size.
+ return set.MinWidth;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsView.ItemSources.cs b/src/src/Notepads.Controls/SetsView/SetsView.ItemSources.cs
new file mode 100644
index 0000000..7ae0cec
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsView.ItemSources.cs
@@ -0,0 +1,114 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System.Reflection;
+ using Windows.Foundation.Collections;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls.Primitives;
+
+ ///
+ /// SetsView methods related to tracking Items and ItemsSource changes.
+ ///
+ public partial class SetsView
+ {
+ // Temporary tracking of previous collections for removing events.
+ private MethodInfo _removeItemsSourceMethod;
+
+ ///
+ protected override void OnItemsChanged(object e)
+ {
+ IVectorChangedEventArgs args = (IVectorChangedEventArgs)e;
+
+ base.OnItemsChanged(e);
+
+ if (args?.CollectionChange == CollectionChange.ItemRemoved && SelectedIndex == -1)
+ {
+ // If we remove the selected item we should select the previous item
+ int startIndex = (int)args.Index + 1;
+ if (startIndex > Items?.Count)
+ {
+ startIndex = 0;
+ }
+
+ SelectedIndex = FindNextSetIndex(startIndex, -1);
+ }
+
+ // Update Sizing (in case there are less items now)
+ SetsView_SizeChanged(this, null);
+ }
+
+ private void ItemContainerGenerator_ItemsChanged(object sender, ItemsChangedEventArgs e)
+ {
+ var action = (CollectionChange)e.Action;
+ if (action == CollectionChange.Reset)
+ {
+ // Reset collection to reload later.
+ _hasLoaded = false;
+ }
+ }
+
+ private void SetInitialSelection()
+ {
+ if (SelectedItem == null)
+ {
+ // If we have an index, but didn't get the selection, make the selection
+ if (SelectedIndex >= 0 && SelectedIndex < Items.Count)
+ {
+ SelectedItem = Items[SelectedIndex];
+ }
+
+ // Otherwise, select the first item by default
+ else if (Items.Count >= 1)
+ {
+ SelectedItem = Items[0];
+ }
+ }
+ }
+
+ // Finds the next visible & enabled set index.
+ private int FindNextSetIndex(int startIndex, int direction)
+ {
+ int index = startIndex;
+ if (direction != 0)
+ {
+ for (int i = 0; i < Items.Count; i++)
+ {
+ index += direction;
+
+ if (index >= Items.Count)
+ {
+ index = 0;
+ }
+ else if (index < 0)
+ {
+ index = Items.Count - 1;
+ }
+
+ if (ContainerFromIndex(index) is SetsViewItem setItem && setItem.IsEnabled && setItem.Visibility == Visibility.Visible)
+ {
+ break;
+ }
+ }
+ }
+
+ return index;
+ }
+
+ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProperty dp)
+ {
+ // Use reflection to store a 'Remove' method of any possible collection in ItemsSource
+ // Cache for efficiency later.
+ if (ItemsSource != null)
+ {
+ _removeItemsSourceMethod = ItemsSource.GetType().GetMethod("Remove");
+ }
+ else
+ {
+ _removeItemsSourceMethod = null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsView.Properties.cs b/src/src/Notepads.Controls/SetsView/SetsView.Properties.cs
new file mode 100644
index 0000000..9b9a270
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsView.Properties.cs
@@ -0,0 +1,277 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+
+ ///
+ /// SetsView properties.
+ ///
+ public partial class SetsView
+ {
+ ///
+ /// Gets or sets the content to appear to the left or above the set strip.
+ ///
+ public object SetsStartHeader
+ {
+ get => GetValue(SetsStartHeaderProperty);
+ set => SetValue(SetsStartHeaderProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsStartHeaderProperty =
+ DependencyProperty.Register(nameof(SetsStartHeader), typeof(object), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public DataTemplate SetsStartHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(SetsStartHeaderTemplateProperty);
+ set => SetValue(SetsStartHeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsStartHeaderTemplateProperty =
+ DependencyProperty.Register(nameof(SetsStartHeaderTemplate), typeof(DataTemplate), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the content to appear next to the set strip.
+ ///
+ public object SetsActionHeader
+ {
+ get => GetValue(SetsActionHeaderProperty);
+ set => SetValue(SetsActionHeaderProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsActionHeaderProperty =
+ DependencyProperty.Register(nameof(SetsActionHeader), typeof(object), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public DataTemplate SetsActionHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(SetsActionHeaderTemplateProperty);
+ set => SetValue(SetsActionHeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsActionHeaderTemplateProperty =
+ DependencyProperty.Register(nameof(SetsActionHeaderTemplate), typeof(DataTemplate), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the content to appear to the right or below the action header.
+ ///
+ public object SetsPaddingHeader
+ {
+ get => GetValue(SetsPaddingHeaderProperty);
+ set => SetValue(SetsPaddingHeaderProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsPaddingHeaderProperty =
+ DependencyProperty.Register(nameof(SetsPaddingHeader), typeof(object), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public DataTemplate SetsPaddingHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(SetsPaddingHeaderTemplateProperty);
+ set => SetValue(SetsPaddingHeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsPaddingHeaderTemplateProperty =
+ DependencyProperty.Register(nameof(SetsPaddingHeaderTemplate), typeof(DataTemplate), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the content to appear to the right or below the set strip.
+ ///
+ public object SetsEndHeader
+ {
+ get => GetValue(SetsEndHeaderProperty);
+ set => SetValue(SetsEndHeaderProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsEndHeaderProperty =
+ DependencyProperty.Register(nameof(SetsEndHeader), typeof(object), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public DataTemplate SetsEndHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(SetsEndHeaderTemplateProperty);
+ set => SetValue(SetsEndHeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsEndHeaderTemplateProperty =
+ DependencyProperty.Register(nameof(SetsEndHeaderTemplate), typeof(DataTemplate), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the default for the .
+ ///
+ public DataTemplate ItemHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(ItemHeaderTemplateTemplateProperty);
+ set => SetValue(ItemHeaderTemplateTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty ItemHeaderTemplateTemplateProperty =
+ DependencyProperty.Register(nameof(ItemHeaderTemplate), typeof(DataTemplate), typeof(SetsView), new PropertyMetadata(null, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets a value indicating whether by default a Set can be closed or not if no value to is provided.
+ ///
+ public bool CanCloseSets
+ {
+ get => (bool)GetValue(CanCloseSetsProperty);
+ set => SetValue(CanCloseSetsProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty CanCloseSetsProperty =
+ DependencyProperty.Register(nameof(CanCloseSets), typeof(bool), typeof(SetsView), new PropertyMetadata(false, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets a value indicating whether a Close Button should be included in layout calculations.
+ ///
+ public bool IsCloseButtonOverlay
+ {
+ get => (bool)GetValue(IsCloseButtonOverlayProperty);
+ set => SetValue(IsCloseButtonOverlayProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty IsCloseButtonOverlayProperty =
+ DependencyProperty.Register(nameof(IsCloseButtonOverlay), typeof(bool), typeof(SetsView), new PropertyMetadata(false, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets a value indicating the size of the selected set. By default this is set to Auto and the selected set size doesn't change.
+ ///
+ public double SelectedSetWidth
+ {
+ get => (double)GetValue(SelectedSetWidthProperty);
+ set => SetValue(SelectedSetWidthProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SelectedSetWidthProperty =
+ DependencyProperty.Register(nameof(SelectedSetWidth), typeof(double), typeof(SetsView), new PropertyMetadata(double.NaN, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets or sets the current which determins how set headers' width behave.
+ ///
+ public SetsWidthMode SetsWidthBehavior
+ {
+ get => (SetsWidthMode)GetValue(SetsWidthBehaviorProperty);
+ set => SetValue(SetsWidthBehaviorProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SetsWidthBehaviorProperty =
+ DependencyProperty.Register(nameof(SetsWidthBehavior), typeof(SetsWidthMode), typeof(SetsView), new PropertyMetadata(SetsWidthMode.Actual, OnLayoutEffectingPropertyChanged));
+
+ ///
+ /// Gets the attached property value to indicate if this grid column should be ignored when calculating header sizes.
+ ///
+ /// Grid Column.
+ /// Boolean indicating if this column is ignored by SetsViewHeader logic.
+ public static bool GetIgnoreColumn(ColumnDefinition obj)
+ {
+ return (bool)obj.GetValue(IgnoreColumnProperty);
+ }
+
+ ///
+ /// Sets the attached property value for
+ ///
+ /// Grid Column.
+ /// Boolean value
+ public static void SetIgnoreColumn(ColumnDefinition obj, bool value)
+ {
+ obj?.SetValue(IgnoreColumnProperty, value);
+ }
+
+ ///
+ /// Identifies the attached property.
+ ///
+ /// The identifier for the IgnoreColumn attached property.
+ public static readonly DependencyProperty IgnoreColumnProperty =
+ DependencyProperty.RegisterAttached("IgnoreColumn", typeof(bool), typeof(SetsView), new PropertyMetadata(false));
+
+ ///
+ /// Gets the attached value indicating this column should be restricted for the headers.
+ ///
+ /// Grid Column.
+ /// True if this column should be constrained.
+ public static bool GetConstrainColumn(ColumnDefinition obj)
+ {
+ return (bool)obj.GetValue(ConstrainColumnProperty);
+ }
+
+ ///
+ /// Sets the attached property value for the
+ ///
+ /// Grid Column.
+ /// Boolean value.
+ public static void SetConstrainColumn(ColumnDefinition obj, bool value)
+ {
+ obj?.SetValue(ConstrainColumnProperty, value);
+ }
+
+ ///
+ /// Identifies the attached property.
+ ///
+ /// The identifier for the ConstrainColumn attached property.
+ public static readonly DependencyProperty ConstrainColumnProperty =
+ DependencyProperty.RegisterAttached("ConstrainColumn", typeof(bool), typeof(SetsView), new PropertyMetadata(false));
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsView.cs b/src/src/Notepads.Controls/SetsView/SetsView.cs
new file mode 100644
index 0000000..66cabe9
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsView.cs
@@ -0,0 +1,547 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using Microsoft.Toolkit.Uwp.UI;
+ using System;
+ using System.Linq;
+ using Windows.ApplicationModel.DataTransfer;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Data;
+ using Windows.UI.Xaml.Input;
+
+ ///
+ /// SetsView is a control for displaying a set of sets and their content.
+ ///
+ [TemplatePart(Name = SetsContentPresenterName, Type = typeof(ContentPresenter))]
+ [TemplatePart(Name = SetsViewContainerName, Type = typeof(Grid))]
+ [TemplatePart(Name = SetsItemsPresenterName, Type = typeof(ItemsPresenter))]
+ [TemplatePart(Name = SetsScrollViewerName, Type = typeof(ScrollViewer))]
+ [TemplatePart(Name = SetsScrollBackButtonName, Type = typeof(ButtonBase))]
+ [TemplatePart(Name = SetsScrollForwardButtonName, Type = typeof(ButtonBase))]
+ [TemplatePart(Name = SetsItemsScrollViewerLeftSideShadowName, Type = typeof(DropShadowPanel))]
+ [TemplatePart(Name = SetsItemsScrollViewerRightSideShadowName, Type = typeof(DropShadowPanel))]
+ public partial class SetsView : ListViewBase
+ {
+ private const int ScrollAmount = 50; // TODO: Should this be based on SetsWidthMode
+
+ private const string SetsContentPresenterName = "SetsContentPresenter";
+ private const string SetsViewContainerName = "SetsViewContainer";
+ private const string SetsItemsPresenterName = "SetsItemsPresenter";
+ private const string SetsScrollViewerName = "ScrollViewer";
+ private const string SetsItemsScrollViewerLeftSideShadowName = "SetsItemsScrollViewerLeftSideShadow";
+ private const string SetsItemsScrollViewerRightSideShadowName = "SetsItemsScrollViewerRightSideShadow";
+ private const string SetsScrollBackButtonName = "SetsScrollBackButton";
+ private const string SetsScrollForwardButtonName = "SetsScrollForwardButton";
+
+ private ContentPresenter _setsContentPresenter;
+ private Grid _setsViewContainer;
+ private ItemsPresenter _setItemsPresenter;
+ private ScrollViewer _setsScroller;
+ private DropShadowPanel _setsItemsScrollViewerLeftSideShadow;
+ private DropShadowPanel _setsItemsScrollViewerRightSideShadow;
+ private ButtonBase _setsScrollBackButton;
+ private ButtonBase _setsScrollForwardButton;
+
+ private bool _hasLoaded;
+ private bool _isDragging;
+
+ private double _scrollViewerHorizontalOffset = .0f;
+ public double ScrollViewerHorizontalOffset => _scrollViewerHorizontalOffset;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SetsView()
+ {
+ DefaultStyleKey = typeof(SetsView);
+
+ // Container Generation Hooks
+ RegisterPropertyChangedCallback(ItemsSourceProperty, ItemsSource_PropertyChanged);
+ ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
+
+ // Drag and Layout Hooks
+ DragItemsStarting += SetsView_DragItemsStarting;
+ DragItemsCompleted += SetsView_DragItemsCompleted;
+ SizeChanged += SetsView_SizeChanged;
+
+ // Selection Hook
+ SelectionChanged += SetsView_SelectionChanged;
+ }
+
+ ///
+ /// Occurs when a set is dragged by the user outside of the . Generally, this paradigm is used to create a new-window with the torn-off set.
+ /// The creation and handling of the new-window is left to the app's developer.
+ ///
+ public event EventHandler SetDraggedOutside;
+
+ ///
+ /// Occurs when a set's Close button is clicked. Set to true to prevent automatic Set Closure.
+ ///
+ public event EventHandler SetClosing;
+
+ ///
+ /// Occurs when a set is selected in .
+ ///
+ public event EventHandler SetSelected;
+
+ ///
+ /// Occurs when a set is tapped in .
+ ///
+ public event EventHandler SetTapped;
+
+ ///
+ /// Occurs when a set is double tapped in .
+ ///
+ public event EventHandler SetDoubleTapped;
+
+ ///
+ protected override DependencyObject GetContainerForItemOverride()
+ {
+ return new SetsViewItem();
+ }
+
+ ///
+ protected override bool IsItemItsOwnContainerOverride(object item)
+ {
+ return item is SetsViewItem;
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ if (_setItemsPresenter != null)
+ {
+ _setItemsPresenter.SizeChanged -= SetsView_SizeChanged;
+ }
+
+ if (_setsScroller != null)
+ {
+ _setsScroller.Loaded -= SetsScrollViewer_Loaded;
+ _setsScroller.ViewChanged -= SetsScrollViewer_ViewChanged;
+ }
+
+ _setsContentPresenter = GetTemplateChild(SetsContentPresenterName) as ContentPresenter;
+ _setsViewContainer = GetTemplateChild(SetsViewContainerName) as Grid;
+ _setItemsPresenter = GetTemplateChild(SetsItemsPresenterName) as ItemsPresenter;
+ _setsScroller = GetTemplateChild(SetsScrollViewerName) as ScrollViewer;
+
+ if (_setItemsPresenter != null)
+ {
+ _setItemsPresenter.SizeChanged += SetsView_SizeChanged;
+ }
+
+ if (_setsScroller != null)
+ {
+ _setsScroller.Loaded += SetsScrollViewer_Loaded;
+ _setsScroller.ViewChanged += SetsScrollViewer_ViewChanged;
+ }
+ }
+
+ private void SetsScrollViewer_Loaded(object sender, RoutedEventArgs e)
+ {
+ _setsScroller.Loaded -= SetsScrollViewer_Loaded;
+
+ if (_setsScrollBackButton != null)
+ {
+ _setsScrollBackButton.Click -= ScrollSetBackButton_Click;
+ }
+
+ if (_setsScrollForwardButton != null)
+ {
+ _setsScrollForwardButton.Click -= ScrollSetForwardButton_Click;
+ }
+
+ _setsScrollBackButton = _setsScroller.FindDescendant(SetsScrollBackButtonName) as ButtonBase;
+ _setsScrollForwardButton = _setsScroller.FindDescendant(SetsScrollForwardButtonName) as ButtonBase;
+ _setsItemsScrollViewerLeftSideShadow = _setsScroller.FindDescendant(SetsItemsScrollViewerLeftSideShadowName) as DropShadowPanel;
+ _setsItemsScrollViewerRightSideShadow = _setsScroller.FindDescendant(SetsItemsScrollViewerRightSideShadowName) as DropShadowPanel;
+
+ if (_setsScrollBackButton != null)
+ {
+ _setsScrollBackButton.Click += ScrollSetBackButton_Click;
+ }
+
+ if (_setsScrollForwardButton != null)
+ {
+ _setsScrollForwardButton.Click += ScrollSetForwardButton_Click;
+ }
+ }
+
+ private void ScrollSetBackButton_Click(object sender, RoutedEventArgs e)
+ {
+ _setsScroller.ChangeView(Math.Max(0, _setsScroller.HorizontalOffset - ScrollAmount), null, null);
+ }
+
+ private void ScrollSetForwardButton_Click(object sender, RoutedEventArgs e)
+ {
+ _setsScroller.ChangeView(Math.Min(_setsScroller.ScrollableWidth, _setsScroller.HorizontalOffset + ScrollAmount), null, null);
+ }
+
+ private void SetsView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isDragging)
+ {
+ // Skip if we're dragging, we'll reset when we're done.
+ return;
+ }
+
+ if (_setsContentPresenter != null)
+ {
+ if (SelectedItem == null)
+ {
+ _setsContentPresenter.Content = null;
+ _setsContentPresenter.ContentTemplate = null;
+ }
+ else
+ {
+ if (ContainerFromItem(SelectedItem) is SetsViewItem container)
+ {
+ _setsContentPresenter.Content = container.Content;
+ _setsContentPresenter.ContentTemplate = container.ContentTemplate;
+
+ if (e != null) _setsContentPresenter.Loaded += SetsContentPresenter_Loaded;
+ }
+ }
+
+ UpdateScrollViewerShadows();
+ UpdateSetSeparators();
+ }
+
+ // If our width can be effected by the selection, need to run algorithm.
+ if (!double.IsNaN(SelectedSetWidth))
+ {
+ SetsView_SizeChanged(sender, null);
+ }
+ }
+
+ private void UpdateSetSeparators()
+ {
+ if (SelectedIndex > 0)
+ {
+ (ContainerFromIndex(0) as SetsViewItem)?.ShowLeftSideSeparator();
+ }
+ else
+ {
+ (ContainerFromIndex(0) as SetsViewItem)?.HideLeftSideSeparator();
+ }
+
+ for (int i = 0; i < Items?.Count; i++)
+ {
+ if (i != SelectedIndex && i != SelectedIndex - 1)
+ {
+ (ContainerFromIndex(i) as SetsViewItem)?.ShowRightSideSeparator();
+ }
+ else
+ {
+ (ContainerFromIndex(i) as SetsViewItem)?.HideRightSideSeparator();
+ }
+ }
+ }
+
+ private void SetsContentPresenter_Loaded(object sender, RoutedEventArgs e)
+ {
+ _setsContentPresenter.Loaded -= SetsContentPresenter_Loaded;
+ var args = new SetSelectedEventArgs(((SetsViewItem)ContainerFromItem(SelectedItem))?.Content, (SetsViewItem)ContainerFromItem(SelectedItem));
+ SetSelected?.Invoke(this, args);
+ }
+
+ ///
+ protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
+ {
+ base.PrepareContainerForItemOverride(element, item);
+
+ if (!(element is SetsViewItem setItem)) return;
+
+ setItem.Loaded -= SetsViewItem_Loaded;
+ setItem.Closing -= SetsViewItem_Closing;
+ setItem.Tapped -= SetsViewItem_Tapped;
+ setItem.DoubleTapped -= SetsViewItem_DoubleTapped;
+ setItem.PointerEntered -= SetItem_PointerEntered;
+ setItem.PointerExited -= SetItem_PointerExited;
+ setItem.Loaded += SetsViewItem_Loaded;
+ setItem.Closing += SetsViewItem_Closing;
+ setItem.Tapped += SetsViewItem_Tapped;
+ setItem.DoubleTapped += SetsViewItem_DoubleTapped;
+ setItem.PointerEntered += SetItem_PointerEntered;
+ setItem.PointerExited += SetItem_PointerExited;
+
+ if (setItem.Header == null)
+ {
+ setItem.Header = item;
+ }
+
+ if (setItem.HeaderTemplate == null)
+ {
+ var headertemplatebinding = new Binding()
+ {
+ Source = this,
+ Path = new PropertyPath(nameof(ItemHeaderTemplate)),
+ Mode = BindingMode.OneWay
+ };
+ setItem.SetBinding(SetsViewItem.HeaderTemplateProperty, headertemplatebinding);
+ }
+
+ if (setItem.IsClosable != true && setItem.ReadLocalValue(SetsViewItem.IsClosableProperty) ==
+ DependencyProperty.UnsetValue)
+ {
+ var iscloseablebinding = new Binding()
+ {
+ Source = this,
+ Path = new PropertyPath(nameof(CanCloseSets)),
+ Mode = BindingMode.OneWay,
+ };
+ setItem.SetBinding(SetsViewItem.IsClosableProperty, iscloseablebinding);
+ }
+ }
+
+ private void SetItem_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ if (sender is SetsViewItem set)
+ {
+ set.HideRightSideSeparator();
+ var index = IndexFromContainer(set);
+ if (index > 0)
+ {
+ (ContainerFromIndex(index - 1) as SetsViewItem)?.HideRightSideSeparator();
+ }
+
+ set.HideLeftSideSeparator();
+ }
+ }
+
+ private void SetItem_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ if (sender is SetsViewItem set)
+ {
+ var index = IndexFromContainer(set);
+
+ if (index == 0 && SelectedIndex != index)
+ {
+ set.ShowLeftSideSeparator();
+ }
+
+ if (SelectedIndex == index - 1)
+ {
+ set.ShowRightSideSeparator();
+ }
+ else if (SelectedIndex == index + 1)
+ {
+ if (index > 0)
+ {
+ (ContainerFromIndex(index - 1) as SetsViewItem)?.ShowRightSideSeparator();
+ }
+ }
+ else if (SelectedIndex != index)
+ {
+ set.ShowRightSideSeparator();
+ if (index > 0)
+ {
+ (ContainerFromIndex(index - 1) as SetsViewItem)?.ShowRightSideSeparator();
+ }
+ }
+ }
+ }
+
+ private void SetsViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
+ {
+ var args = new SetSelectedEventArgs(((SetsViewItem)ContainerFromItem(SelectedItem))?.Content, (SetsViewItem)ContainerFromItem(SelectedItem));
+ SetDoubleTapped?.Invoke(sender, args);
+ }
+
+ private void SetsViewItem_Tapped(object sender, TappedRoutedEventArgs e)
+ {
+ var args = new SetSelectedEventArgs(((SetsViewItem)ContainerFromItem(SelectedItem))?.Content, (SetsViewItem)ContainerFromItem(SelectedItem));
+ SetTapped?.Invoke(sender, args);
+ }
+
+ private void SetsViewItem_Loaded(object sender, RoutedEventArgs e)
+ {
+ var setItem = sender as SetsViewItem;
+
+ setItem.Loaded -= SetsViewItem_Loaded;
+
+ // Only need to do this once.
+ if (!_hasLoaded)
+ {
+ _hasLoaded = true;
+
+ // Need to set a set's selection on load, otherwise ListView resets to null.
+ SetInitialSelection();
+
+ // Need to make sure ContentPresenter is set to content based on selection.
+ SetsView_SelectionChanged(this, null);
+
+ // Need to make sure we've registered our removal method.
+ ItemsSource_PropertyChanged(this, null);
+
+ // Make sure we complete layout now.
+ SetsView_SizeChanged(this, null);
+ }
+ }
+
+ private void SetsViewItem_Closing(object sender, SetClosingEventArgs e)
+ {
+ var item = ItemFromContainer(e.Set);
+
+ var args = new SetClosingEventArgs(item, e.Set);
+ SetClosing?.Invoke(this, args);
+
+ if (!args.Cancel)
+ {
+ e.Set.PrepareForClosing();
+ if (ItemsSource != null)
+ {
+ _removeItemsSourceMethod?.Invoke(ItemsSource, new object[] { item });
+ }
+ else
+ {
+ Items?.Remove(item);
+ }
+ }
+ }
+
+ private void SetsView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
+ {
+ // Keep track of drag so we don't modify content until done.
+ _isDragging = true;
+
+ _setsItemsScrollViewerLeftSideShadow.Visibility = Visibility.Collapsed;
+ _setsItemsScrollViewerRightSideShadow.Visibility = Visibility.Collapsed;
+ }
+
+ private void SetsView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
+ {
+ _isDragging = false;
+
+ // args.DropResult == None when outside of area (e.g. create new window)
+ if (args.DropResult == DataPackageOperation.None)
+ {
+ var item = args.Items.FirstOrDefault();
+ var set = ContainerFromItem(item) as SetsViewItem;
+
+ if (set == null && item is FrameworkElement fe)
+ {
+ set = fe.FindParent();
+ }
+
+ if (set == null)
+ {
+ // We still don't have a SetsViewItem, most likely is a static SetsViewItem in the template being dragged and not selected.
+ // This is a fallback scenario for static sets.
+ // Note: This can be wrong if two SetsViewItems share the exact same Content (i.e. a string), this should be unlikely in any practical scenario.
+ for (int i = 0; i < Items.Count; i++)
+ {
+ var setItem = ContainerFromIndex(i) as SetsViewItem;
+ if (ReferenceEquals(setItem.Content, item))
+ {
+ set = setItem;
+ break;
+ }
+ }
+ }
+
+ SetDraggedOutside?.Invoke(this, new SetDraggedOutsideEventArgs(item, set));
+
+ UpdateScrollViewerShadows();
+ }
+ else
+ {
+ // If dragging the active set, there's an issue with the CP blanking.
+ SetsView_SelectionChanged(this, null);
+ }
+ }
+
+ private void SetsScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
+ {
+ _scrollViewerHorizontalOffset = _setsScroller.HorizontalOffset;
+ UpdateScrollViewerShadows();
+ UpdateScrollViewerNavigateButtons();
+ }
+
+ public void ScrollToLastSet()
+ {
+ ScrollTo(double.MaxValue);
+ }
+
+ public void ScrollTo(double offset)
+ {
+ try
+ {
+ _setsScroller?.UpdateLayout();
+ _setsScroller?.ChangeView(offset, 0.0f, 1.0f);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"SetsView failed to scroll to offset: {(long)offset}, Exception: {ex}");
+ }
+ }
+
+ // HACK: Simulate left most and right most (tab) edge shadow
+ // since I am too lazy to figure out the "right way" to make shadow visible on scroll viewer edges
+ /// TODO This method should be removed when better solution is available in the future
+ private void UpdateScrollViewerShadows()
+ {
+ if (_setsItemsScrollViewerLeftSideShadow == null ||
+ _setsItemsScrollViewerRightSideShadow == null)
+ {
+ return;
+ }
+
+ if (Items?.Count == 1)
+ {
+ _setsItemsScrollViewerLeftSideShadow.Visibility = Visibility.Visible;
+ _setsItemsScrollViewerRightSideShadow.Visibility = Visibility.Visible;
+ return;
+ }
+
+ if (SelectedIndex == 0)
+ {
+ if (Math.Abs(_scrollViewerHorizontalOffset) < 3)
+ {
+ _setsItemsScrollViewerLeftSideShadow.Visibility = Visibility.Visible;
+ _setsItemsScrollViewerRightSideShadow.Visibility = Visibility.Collapsed;
+ return;
+ }
+ }
+ else if (SelectedIndex == Items?.Count - 1)
+ {
+ var offset = _setsScroller.ExtentWidth - _setsScroller.ViewportWidth - _scrollViewerHorizontalOffset;
+ if (Math.Abs(offset) < 3)
+ {
+ _setsItemsScrollViewerLeftSideShadow.Visibility = Visibility.Collapsed;
+ _setsItemsScrollViewerRightSideShadow.Visibility = Visibility.Visible;
+ return;
+ }
+ }
+
+ _setsItemsScrollViewerLeftSideShadow.Visibility = Visibility.Collapsed;
+ _setsItemsScrollViewerRightSideShadow.Visibility = Visibility.Collapsed;
+ }
+
+ private void UpdateScrollViewerNavigateButtons()
+ {
+ if (Math.Abs(_setsScroller.HorizontalOffset - _setsScroller.ScrollableWidth) < 0.1)
+ {
+ _setsScrollBackButton.IsEnabled = true;
+ _setsScrollForwardButton.IsEnabled = false;
+ }
+ else if (Math.Abs(_setsScroller.HorizontalOffset) < 0.1)
+ {
+ _setsScrollBackButton.IsEnabled = false;
+ _setsScrollForwardButton.IsEnabled = true;
+ }
+ else
+ {
+ _setsScrollBackButton.IsEnabled = true;
+ _setsScrollForwardButton.IsEnabled = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsView.xaml b/src/src/Notepads.Controls/SetsView/SetsView.xaml
new file mode 100644
index 0000000..1c4397a
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsView.xaml
@@ -0,0 +1,1063 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.55
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+ 0
+ 0,2,6,0
+ 0,1,0,0
+ 0,1,0,0
+ 10
+ 48
+ 40
+ NaN
+ 24
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.Controls/SetsView/SetsViewItem.Properties.cs b/src/src/Notepads.Controls/SetsView/SetsViewItem.Properties.cs
new file mode 100644
index 0000000..2a01fd1
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsViewItem.Properties.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+
+ ///
+ /// Item Container for a .
+ ///
+ public partial class SetsViewItem
+ {
+ ///
+ /// Gets or sets the header content for the set.
+ ///
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty HeaderProperty =
+ DependencyProperty.Register(nameof(Header), typeof(object), typeof(SetsViewItem), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the icon to appear in the set header.
+ ///
+ public IconElement Icon
+ {
+ get => (IconElement)GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty IconProperty =
+ DependencyProperty.Register(nameof(Icon), typeof(IconElement), typeof(SetsViewItem), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the template to override for the set header.
+ ///
+ public DataTemplate HeaderTemplate
+ {
+ get => (DataTemplate)GetValue(HeaderTemplateProperty);
+ set => SetValue(HeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty HeaderTemplateProperty =
+ DependencyProperty.Register(nameof(HeaderTemplate), typeof(DataTemplate), typeof(SetsViewItem), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets a value indicating whether the set can be closed by the user with the close button.
+ ///
+ public bool IsClosable
+ {
+ get => (bool)GetValue(IsClosableProperty);
+ set => SetValue(IsClosableProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty IsClosableProperty =
+ DependencyProperty.Register(nameof(IsClosable), typeof(bool), typeof(SetsViewItem), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the selection indicator brush
+ ///
+ public Brush SelectionIndicatorForeground
+ {
+ get => (Brush)GetValue(SelectionIndicatorForegroundProperty);
+ set => SetValue(SelectionIndicatorForegroundProperty, value);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty SelectionIndicatorForegroundProperty =
+ DependencyProperty.Register(nameof(SelectionIndicatorForeground), typeof(Brush), typeof(SetsViewItem), new PropertyMetadata(null));
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsViewItem.cs b/src/src/Notepads.Controls/SetsView/SetsViewItem.cs
new file mode 100644
index 0000000..51b8406
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsViewItem.cs
@@ -0,0 +1,183 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using System;
+ using Windows.Devices.Input;
+ using Windows.System;
+ using Windows.UI;
+ using Windows.UI.Core;
+ using Windows.UI.Input;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+ using Windows.UI.Xaml.Shapes;
+
+ ///
+ /// Item Container for a .
+ ///
+ [TemplatePart(Name = SetCloseButtonName, Type = typeof(ButtonBase))]
+ [TemplatePart(Name = SetLeftSideSeparatorName, Type = typeof(Border))]
+ [TemplatePart(Name = SetRightSideSeparatorName, Type = typeof(Border))]
+ [TemplatePart(Name = SetSelectionIndicatorName, Type = typeof(Rectangle))]
+ public partial class SetsViewItem : ListViewItem
+ {
+ private const string SetCloseButtonName = "CloseButton";
+
+ private const string SetLeftSideSeparatorName = "LeftSideSeparator";
+
+ private const string SetRightSideSeparatorName = "RightSideSeparator";
+
+ private const string SetSelectionIndicatorName = "SelectionIndicator";
+
+ private ButtonBase _setCloseButton;
+
+ private Border _setLeftSideSeparator;
+
+ private Border _setRightSideSeparator;
+
+ private Rectangle _setSelectionIndicator;
+
+ private bool _isMiddleClick;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SetsViewItem()
+ {
+ DefaultStyleKey = typeof(SetsViewItem);
+ }
+
+ ///
+ /// Fired when the Set's close button is clicked.
+ ///
+ public event EventHandler Closing;
+
+ public void ShowLeftSideSeparator()
+ {
+ if (_setLeftSideSeparator != null)
+ {
+ _setLeftSideSeparator.Visibility = Visibility.Visible;
+ }
+ }
+
+ public void HideLeftSideSeparator()
+ {
+ if (_setLeftSideSeparator != null)
+ {
+ _setLeftSideSeparator.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ public void ShowRightSideSeparator()
+ {
+ if (_setRightSideSeparator != null)
+ {
+ _setRightSideSeparator.Visibility = Visibility.Visible;
+ }
+ }
+
+ public void HideRightSideSeparator()
+ {
+ if (_setRightSideSeparator != null)
+ {
+ _setRightSideSeparator.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ if (_setCloseButton != null)
+ {
+ _setCloseButton.Click -= SetCloseButton_Click;
+ }
+
+ _setCloseButton = GetTemplateChild(SetCloseButtonName) as ButtonBase;
+
+ if (_setCloseButton != null)
+ {
+ _setCloseButton.Click += SetCloseButton_Click;
+ }
+
+ _setLeftSideSeparator = GetTemplateChild(SetLeftSideSeparatorName) as Border;
+ _setRightSideSeparator = GetTemplateChild(SetRightSideSeparatorName) as Border;
+ _setSelectionIndicator = GetTemplateChild(SetSelectionIndicatorName) as Rectangle;
+ }
+
+ ///
+ protected override void OnPointerPressed(PointerRoutedEventArgs e)
+ {
+ _isMiddleClick = false;
+
+ if (e?.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
+ {
+ PointerPoint pointerPoint = e.GetCurrentPoint(this);
+
+ // Record if middle button is pressed
+ if (pointerPoint.Properties.IsMiddleButtonPressed)
+ {
+ _isMiddleClick = true;
+ }
+
+ // Disable unwanted behaviour inherited by ListViewItem:
+ // Disable "Ctrl + Left click" to deselect tab
+ // Or variant like "Ctrl + Shift + Left click"
+ // Or "Ctrl + Alt + Left click"
+ if (pointerPoint.Properties.IsLeftButtonPressed)
+ {
+ var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
+ if (ctrl.HasFlag(CoreVirtualKeyStates.Down))
+ {
+ // return here so the event won't be picked up by the base class
+ // but keep this event unhandled so it can be picked up further
+ return;
+ }
+ }
+ }
+
+ base.OnPointerPressed(e);
+ }
+
+ ///
+ protected override void OnPointerReleased(PointerRoutedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+
+ // Close on Middle-Click
+ if (_isMiddleClick)
+ {
+ SetCloseButton_Click(this, null);
+ }
+
+ _isMiddleClick = false;
+ }
+
+ public void PrepareForClosing()
+ {
+ _setSelectionIndicator.Fill = new SolidColorBrush(Colors.Transparent);
+ }
+
+ public void Close()
+ {
+ if (IsClosable)
+ {
+ Closing?.Invoke(this, new SetClosingEventArgs(Content, this));
+ }
+ }
+
+ private void SetCloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (IsClosable)
+ {
+ Closing?.Invoke(this, new SetClosingEventArgs(Content, this));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/SetsView/SetsWidthMode.cs b/src/src/Notepads.Controls/SetsView/SetsWidthMode.cs
new file mode 100644
index 0000000..ead2b75
--- /dev/null
+++ b/src/src/Notepads.Controls/SetsView/SetsWidthMode.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Notepads.Controls
+{
+ using Windows.UI.Xaml;
+
+ ///
+ /// Possible modes for how to layout a Header's Width in the .
+ ///
+ public enum SetsWidthMode
+ {
+ ///
+ /// Each set header takes up as much space as it needs. This is similar to how WPF and Visual Studio Code behave.
+ /// Suggest to keep set to false.
+ /// is ignored.
+ /// In this scenario, set width behavior is effectively turned off. This can be useful when using custom styling or a custom panel for layout of as well.
+ ///
+ Actual,
+
+ ///
+ /// Each set header will use the minimal space set by on the .
+ /// Suggest to set the to show more content for the selected item.
+ ///
+ Compact,
+
+ ///
+ /// Each set header will fill to fit the available space. If is set, that will be used as a Maximum Width.
+ /// This is similar to how Microsoft Edge behaves when used with the .
+ /// Suggest to set to true.
+ /// Suggest to set to 200 and the SetsViewItemHeaderMinWidth Resource to 90.
+ ///
+ Equal,
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads.Controls/Themes/Generic.xaml b/src/src/Notepads.Controls/Themes/Generic.xaml
new file mode 100644
index 0000000..31a484f
--- /dev/null
+++ b/src/src/Notepads.Controls/Themes/Generic.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads.sln b/src/src/Notepads.sln
new file mode 100644
index 0000000..fad9f03
--- /dev/null
+++ b/src/src/Notepads.sln
@@ -0,0 +1,83 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28803.352
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notepads", "Notepads\Notepads.csproj", "{99274932-9E86-480C-8142-38525F80007D}"
+ ProjectSection(ProjectDependencies) = postProject
+ {7AA5E631-B663-420E-A08F-002CD81DF855} = {7AA5E631-B663-420E-A08F-002CD81DF855}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notepads.Controls", "Notepads.Controls\Notepads.Controls.csproj", "{7AA5E631-B663-420E-A08F-002CD81DF855}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FE6A4F54-39DC-40E3-8BFB-5B1962DED402}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ Production|ARM64 = Production|ARM64
+ Production|x64 = Production|x64
+ Production|x86 = Production|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.Build.0 = Debug|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x64.ActiveCfg = Debug|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x64.Build.0 = Debug|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x64.Deploy.0 = Debug|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.ActiveCfg = Debug|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.Build.0 = Debug|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.Deploy.0 = Debug|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.ActiveCfg = Release|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Build.0 = Release|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Deploy.0 = Release|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x64.ActiveCfg = Release|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Build.0 = Release|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Deploy.0 = Release|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x86.ActiveCfg = Release|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Build.0 = Release|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Deploy.0 = Release|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.ActiveCfg = Production|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.Build.0 = Production|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.Deploy.0 = Production|ARM64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x64.ActiveCfg = Production|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x64.Build.0 = Production|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x64.Deploy.0 = Production|x64
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x86.ActiveCfg = Production|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x86.Build.0 = Production|x86
+ {99274932-9E86-480C-8142-38525F80007D}.Production|x86.Deploy.0 = Production|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM64.Build.0 = Debug|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x64.ActiveCfg = Debug|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x64.Build.0 = Debug|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x86.ActiveCfg = Debug|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x86.Build.0 = Debug|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.ActiveCfg = Release|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.Build.0 = Release|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.ActiveCfg = Release|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.Build.0 = Release|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.ActiveCfg = Release|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.Build.0 = Release|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM64.ActiveCfg = Production|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM64.Build.0 = Production|ARM64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x64.ActiveCfg = Production|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x64.Build.0 = Production|x64
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x86.ActiveCfg = Production|x86
+ {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x86.Build.0 = Production|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {CFB87BE6-E5E9-49A2-9631-7DC3F44361B4}
+ EndGlobalSection
+EndGlobal
diff --git a/src/src/Notepads/App.xaml b/src/src/Notepads/App.xaml
new file mode 100644
index 0000000..af23eda
--- /dev/null
+++ b/src/src/Notepads/App.xaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/Notepads/App.xaml.cs b/src/src/Notepads/App.xaml.cs
new file mode 100644
index 0000000..82fb8df
--- /dev/null
+++ b/src/src/Notepads/App.xaml.cs
@@ -0,0 +1,327 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Toolkit.Uwp.Helpers;
+ using Notepads.Services;
+ using Notepads.Settings;
+ using Notepads.Utilities;
+ using Windows.ApplicationModel;
+ using Windows.ApplicationModel.Activation;
+ using Windows.ApplicationModel.Core;
+ using Windows.ApplicationModel.DataTransfer;
+ using Windows.ApplicationModel.Resources.Core;
+ using Windows.UI;
+ using Windows.UI.ViewManagement;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Navigation;
+
+ public sealed partial class App : Application
+ {
+ public static string ApplicationName = "Notepads";
+
+ public static Guid InstanceId { get; } = Guid.NewGuid();
+
+ public static bool IsPrimaryInstance = false;
+ public static bool IsGameBarWidget = false;
+
+ public static Mutex InstanceHandlerMutex { get; set; }
+
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ UnhandledException += OnUnhandledException;
+ TaskScheduler.UnobservedTaskException += OnUnobservedException;
+
+ InstanceHandlerMutex = new Mutex(true, App.ApplicationName, out bool isNew);
+ if (isNew)
+ {
+ IsPrimaryInstance = true;
+ ApplicationSettingsStore.Write(SettingsKey.ActiveInstanceIdStr, null);
+ }
+ else
+ {
+ InstanceHandlerMutex.Close();
+ }
+
+ LoggingService.LogInfo($"[{nameof(App)}] Started: Instance = {InstanceId} IsPrimaryInstance: {IsPrimaryInstance} IsGameBarWidget: {IsGameBarWidget}.");
+
+ ApplicationSettingsStore.Write(SettingsKey.ActiveInstanceIdStr, App.InstanceId.ToString());
+
+ InitializeComponent();
+
+ Suspending += OnSuspending;
+ }
+
+ ///
+ /// Invoked when the application is launched normally by the end user. Other entry points
+ /// will be used such as when the application is launched to open a specific file.
+ ///
+ /// Details about the launch request and process.
+ protected override async void OnLaunched(LaunchActivatedEventArgs e)
+ {
+ await ActivateAsync(e);
+ }
+
+ protected override async void OnFileActivated(FileActivatedEventArgs args)
+ {
+ await ActivateAsync(args);
+ base.OnFileActivated(args);
+ }
+
+ protected override async void OnActivated(IActivatedEventArgs args)
+ {
+ await ActivateAsync(args);
+ base.OnActivated(args);
+ }
+
+ private async Task ActivateAsync(IActivatedEventArgs e)
+ {
+ bool rootFrameCreated = false;
+
+ if (!(Window.Current.Content is Frame rootFrame))
+ {
+ rootFrame = CreateRootFrame(e);
+ Window.Current.Content = rootFrame;
+ rootFrameCreated = true;
+
+ ThemeSettingsService.Initialize();
+ AppSettingsService.Initialize();
+ }
+
+ var appLaunchSettings = new Dictionary()
+ {
+ { "OSArchitecture", SystemInformation.Instance.OperatingSystemArchitecture.ToString() },
+ { "OSVersion", $"{SystemInformation.Instance.OperatingSystemVersion.Major}.{SystemInformation.Instance.OperatingSystemVersion.Minor}.{SystemInformation.Instance.OperatingSystemVersion.Build}" },
+ { "UseWindowsTheme", ThemeSettingsService.UseWindowsTheme.ToString() },
+ { "ThemeMode", ThemeSettingsService.ThemeMode.ToString() },
+ { "UseWindowsAccentColor", ThemeSettingsService.UseWindowsAccentColor.ToString() },
+ { "AppBackgroundTintOpacity", $"{(int) (ThemeSettingsService.AppBackgroundPanelTintOpacity * 10.0) * 10}" },
+ { "ShowStatusBar", AppSettingsService.ShowStatusBar.ToString() },
+ { "IsSessionSnapshotEnabled", AppSettingsService.IsSessionSnapshotEnabled.ToString() },
+ { "IsShadowWindow", (!IsPrimaryInstance && !IsGameBarWidget).ToString() },
+ { "IsGameBarWidget", IsGameBarWidget.ToString() },
+ { "AlwaysOpenNewWindow", AppSettingsService.AlwaysOpenNewWindow.ToString() },
+ { "IsHighlightMisspelledWordsEnabled", AppSettingsService.IsHighlightMisspelledWordsEnabled.ToString() },
+ { "IsSmartCopyEnabled", AppSettingsService.IsSmartCopyEnabled.ToString() },
+ { "ExitWhenLastTabClosed", AppSettingsService.ExitWhenLastTabClosed.ToString() },
+ };
+
+ LoggingService.LogInfo($"[{nameof(App)}] Launch settings: \n{string.Join("\n", appLaunchSettings.Select(x => x.Key + "=" + x.Value).ToArray())}.");
+ AnalyticsService.TrackEvent("AppLaunch_Settings", appLaunchSettings);
+
+ var appLaunchEditorSettings = new Dictionary()
+ {
+ { "EditorDefaultLineEnding", AppSettingsService.EditorDefaultLineEnding.ToString() },
+ { "EditorDefaultEncoding", EncodingUtility.GetEncodingName(AppSettingsService.EditorDefaultEncoding) },
+ { "EditorDefaultTabIndents", AppSettingsService.EditorDefaultTabIndents.ToString() },
+ { "EditorDefaultDecoding", AppSettingsService.EditorDefaultDecoding == null ? "Auto" : EncodingUtility.GetEncodingName(AppSettingsService.EditorDefaultDecoding) },
+ { "EditorFontFamily", AppSettingsService.EditorFontFamily },
+ { "EditorFontSize", AppSettingsService.EditorFontSize.ToString() },
+ { "EditorFontStyle", AppSettingsService.EditorFontStyle.ToString() },
+ { "EditorFontWeight", AppSettingsService.EditorFontWeight.Weight.ToString() },
+ { "EditorDefaultSearchEngine", AppSettingsService.EditorDefaultSearchEngine.ToString() },
+ { "DisplayLineHighlighter", AppSettingsService.EditorDisplayLineHighlighter.ToString() },
+ { "DisplayLineNumbers", AppSettingsService.EditorDisplayLineNumbers.ToString() },
+ };
+
+ LoggingService.LogInfo($"[{nameof(App)}] Editor settings: \n{string.Join("\n", appLaunchEditorSettings.Select(x => x.Key + "=" + x.Value).ToArray())}.");
+ AnalyticsService.TrackEvent("AppLaunch_Editor_Settings", appLaunchEditorSettings);
+
+ try
+ {
+ await ActivationService.ActivateAsync(rootFrame, e);
+ }
+ catch (Exception ex)
+ {
+ var diagnosticInfo = new Dictionary()
+ {
+ { "Message", ex?.Message },
+ { "Exception", ex?.ToString() },
+ };
+ AnalyticsService.TrackEvent("AppFailedToActivate", diagnosticInfo);
+ AnalyticsService.TrackError(ex, diagnosticInfo);
+ throw;
+ }
+
+ try
+ {
+ if (Windows.Foundation.Metadata.ApiInformation.IsMethodPresent("Windows.ApplicationModel.Core.CoreApplication", "EnablePrelaunch"))
+ {
+ // Only enable prelaunch when AlwaysOpenNewWindow is set to false
+ CoreApplication.EnablePrelaunch(!AppSettingsService.AlwaysOpenNewWindow);
+ }
+ }
+ catch (Exception)
+ {
+ // Best efforts
+ }
+
+ if (rootFrameCreated)
+ {
+ ExtendViewIntoTitleBar();
+ Window.Current.Activate();
+ }
+ }
+
+ private Frame CreateRootFrame(IActivatedEventArgs e)
+ {
+ Frame rootFrame = new Frame();
+
+ var flowDirectionSetting = ResourceContext.GetForCurrentView().QualifierValues["LayoutDirection"];
+ if (flowDirectionSetting == "RTL" || flowDirectionSetting == "TTBRTL")
+ {
+ rootFrame.FlowDirection = FlowDirection.RightToLeft;
+ }
+ else
+ {
+ rootFrame.FlowDirection = FlowDirection.LeftToRight;
+ }
+ rootFrame.NavigationFailed += OnNavigationFailed;
+
+ if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
+ {
+ // TODO: Load state from previously suspended application
+ }
+
+ return rootFrame;
+ }
+
+ ///
+ /// Invoked when Navigation to a certain page fails
+ ///
+ /// The Frame which failed navigation
+ /// Details about the navigation failure
+ void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
+ {
+ var exception = new Exception($"[{nameof(App)}] Failed to load Page: {e.SourcePageType.FullName} Exception: {e.Exception.Message}");
+ LoggingService.LogException(exception);
+ AnalyticsService.TrackEvent("FailedToLoadPage", new Dictionary()
+ {
+ { "Page", e.SourcePageType.FullName },
+ { "Exception", e.Exception.Message }
+ });
+ throw exception;
+ }
+
+ ///
+ /// Invoked when application execution is being suspended. Application state is saved
+ /// without knowing whether the application will be terminated or resumed with the contents
+ /// of memory still intact.
+ ///
+ /// The source of the suspend request.
+ /// Details about the suspend request.
+ private void OnSuspending(object sender, SuspendingEventArgs args)
+ {
+ var deferral = args.SuspendingOperation.GetDeferral();
+
+ try
+ {
+ // Here we flush the Clipboard again to make sure content in clipboard to remain available
+ // after the application shuts down.
+ Clipboard.Flush();
+ }
+ catch (Exception)
+ {
+ // Best efforts
+ }
+ finally
+ {
+ deferral.Complete();
+ }
+ }
+
+ // Occurs when an exception is not handled on the UI thread.
+ private static void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
+ {
+ LoggingService.LogError($"[{nameof(App)}] OnUnhandledException: {e.Exception}");
+
+ var diagnosticInfo = new Dictionary()
+ {
+ { "Message", e.Message },
+ { "Exception", e.Exception?.ToString() },
+ { "Culture", SystemInformation.Instance.Culture.EnglishName },
+ { "AvailableMemory", SystemInformation.Instance.AvailableMemory.ToString("F0") },
+ { "FirstUseTimeUTC", SystemInformation.Instance.FirstUseTime.ToUniversalTime().ToString("MM/dd/yyyy HH:mm:ss") },
+ { "OSArchitecture", SystemInformation.Instance.OperatingSystemArchitecture.ToString() },
+ { "OSVersion", SystemInformation.Instance.OperatingSystemVersion.ToString() },
+ { "IsShadowWindow", (!IsPrimaryInstance && !IsGameBarWidget).ToString() },
+ { "IsGameBarWidget", IsGameBarWidget.ToString() }
+ };
+
+ AnalyticsService.TrackEvent("OnUnhandledException", diagnosticInfo);
+ AnalyticsService.TrackError(e.Exception, diagnosticInfo);
+
+ // suppress and handle it manually.
+ e.Handled = true;
+ }
+
+ // Occurs when an exception is not handled on a background thread.
+ // ie. A task is fired and forgotten Task.Run(() => {...})
+ private static void OnUnobservedException(object sender, UnobservedTaskExceptionEventArgs e)
+ {
+ LoggingService.LogError($"[{nameof(App)}] OnUnobservedException: {e.Exception}");
+
+ var diagnosticInfo = new Dictionary()
+ {
+ { "Message", e.Exception?.Message },
+ { "Exception", e.Exception?.ToString() },
+ { "InnerException", e.Exception?.InnerException?.ToString() },
+ { "InnerExceptionMessage", e.Exception?.InnerException?.Message }
+ };
+
+ AnalyticsService.TrackEvent("OnUnobservedException", diagnosticInfo);
+ AnalyticsService.TrackError(e.Exception, diagnosticInfo);
+
+ // suppress and handle it manually.
+ e.SetObserved();
+ }
+
+ private static void ExtendViewIntoTitleBar()
+ {
+ if (!IsGameBarWidget)
+ {
+ CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true;
+ ApplicationViewTitleBar titleBar = ApplicationView.GetForCurrentView().TitleBar;
+ titleBar.ButtonBackgroundColor = Colors.Transparent;
+ titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
+ }
+ }
+
+ //private static void UpdateAppVersion()
+ //{
+ // var packageVer = Package.Current.Id.Version;
+ // string oldVer = ApplicationSettingsStore.Read(SettingsKey.AppVersionStr) as string ?? "";
+ // string currentVer = $"{packageVer.Major}.{packageVer.Minor}.{packageVer.Build}.{packageVer.Revision}";
+
+ // if (currentVer != oldVer)
+ // {
+ // JumpListService.IsJumpListOutOfDate = true;
+ // ApplicationSettingsStore.Write(SettingsKey.AppVersionStr, currentVer);
+ // }
+ //}
+
+ //private static async Task UpdateJumpListAsync()
+ //{
+ // if (JumpListService.IsJumpListOutOfDate)
+ // {
+ // if (await JumpListService.UpdateJumpListAsync())
+ // {
+ // JumpListService.IsJumpListOutOfDate = false;
+ // }
+ // }
+ //}
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Assets/FileIcons/asp.png b/src/src/Notepads/Assets/FileIcons/asp.png
new file mode 100644
index 0000000..b7c0fcc
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/asp.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/asp.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/asp.targetsize-16.png
new file mode 100644
index 0000000..eff2e65
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/asp.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/asp.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/asp.targetsize-32.png
new file mode 100644
index 0000000..bf8644f
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/asp.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/asp.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/asp.targetsize-48.png
new file mode 100644
index 0000000..9196a73
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/asp.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/asp.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/asp.targetsize-512.png
new file mode 100644
index 0000000..b7c0fcc
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/asp.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ass.png b/src/src/Notepads/Assets/FileIcons/ass.png
new file mode 100644
index 0000000..b21bfeb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ass.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ass.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/ass.targetsize-16.png
new file mode 100644
index 0000000..e6414f9
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ass.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ass.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/ass.targetsize-32.png
new file mode 100644
index 0000000..c4856fe
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ass.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ass.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/ass.targetsize-48.png
new file mode 100644
index 0000000..a4c6759
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ass.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ass.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/ass.targetsize-512.png
new file mode 100644
index 0000000..b21bfeb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ass.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bash.png b/src/src/Notepads/Assets/FileIcons/bash.png
new file mode 100644
index 0000000..f3d1423
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bash.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bash.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/bash.targetsize-16.png
new file mode 100644
index 0000000..7f4a238
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bash.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bash.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/bash.targetsize-32.png
new file mode 100644
index 0000000..7919d94
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bash.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bash.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/bash.targetsize-48.png
new file mode 100644
index 0000000..125db08
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bash.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bash.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/bash.targetsize-512.png
new file mode 100644
index 0000000..f3d1423
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bash.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bib.png b/src/src/Notepads/Assets/FileIcons/bib.png
new file mode 100644
index 0000000..b940965
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bib.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bib.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/bib.targetsize-16.png
new file mode 100644
index 0000000..dab4df5
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bib.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bib.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/bib.targetsize-32.png
new file mode 100644
index 0000000..71072b9
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bib.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bib.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/bib.targetsize-48.png
new file mode 100644
index 0000000..291dd9d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bib.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/bib.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/bib.targetsize-512.png
new file mode 100644
index 0000000..b940965
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/bib.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/c.png b/src/src/Notepads/Assets/FileIcons/c.png
new file mode 100644
index 0000000..723315d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/c.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/c.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/c.targetsize-16.png
new file mode 100644
index 0000000..e1335ab
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/c.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/c.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/c.targetsize-32.png
new file mode 100644
index 0000000..d280aac
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/c.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/c.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/c.targetsize-48.png
new file mode 100644
index 0000000..dbb5031
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/c.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/c.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/c.targetsize-512.png
new file mode 100644
index 0000000..723315d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/c.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cfg.png b/src/src/Notepads/Assets/FileIcons/cfg.png
new file mode 100644
index 0000000..4c4fee3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cfg.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cfg.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-16.png
new file mode 100644
index 0000000..43f68c4
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cfg.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-32.png
new file mode 100644
index 0000000..8ddc037
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cfg.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-48.png
new file mode 100644
index 0000000..9e3d83a
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cfg.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-512.png
new file mode 100644
index 0000000..4c4fee3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cfg.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cpp.png b/src/src/Notepads/Assets/FileIcons/cpp.png
new file mode 100644
index 0000000..a4fdf3a
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cpp.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cpp.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-16.png
new file mode 100644
index 0000000..61dba89
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cpp.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-32.png
new file mode 100644
index 0000000..b113443
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cpp.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-48.png
new file mode 100644
index 0000000..3b40662
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cpp.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-512.png
new file mode 100644
index 0000000..a4fdf3a
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cpp.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cs.png b/src/src/Notepads/Assets/FileIcons/cs.png
new file mode 100644
index 0000000..d3247b2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cs.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cs.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/cs.targetsize-16.png
new file mode 100644
index 0000000..95cb22e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cs.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cs.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/cs.targetsize-32.png
new file mode 100644
index 0000000..a6f7237
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cs.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cs.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/cs.targetsize-48.png
new file mode 100644
index 0000000..1793c36
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cs.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/cs.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/cs.targetsize-512.png
new file mode 100644
index 0000000..d3247b2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/cs.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/css.png b/src/src/Notepads/Assets/FileIcons/css.png
new file mode 100644
index 0000000..23579ca
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/css.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/css.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/css.targetsize-16.png
new file mode 100644
index 0000000..06021af
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/css.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/css.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/css.targetsize-32.png
new file mode 100644
index 0000000..60d13e3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/css.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/css.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/css.targetsize-48.png
new file mode 100644
index 0000000..0330da2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/css.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/css.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/css.targetsize-512.png
new file mode 100644
index 0000000..23579ca
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/css.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/file.png b/src/src/Notepads/Assets/FileIcons/file.png
new file mode 100644
index 0000000..257cdfd
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/file.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/file.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/file.targetsize-16.png
new file mode 100644
index 0000000..9555e0c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/file.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/file.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/file.targetsize-32.png
new file mode 100644
index 0000000..3c04e2a
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/file.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/file.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/file.targetsize-48.png
new file mode 100644
index 0000000..0f4f398
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/file.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/file.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/file.targetsize-512.png
new file mode 100644
index 0000000..d19ec29
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/file.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/h.png b/src/src/Notepads/Assets/FileIcons/h.png
new file mode 100644
index 0000000..6b22c4b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/h.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/h.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/h.targetsize-16.png
new file mode 100644
index 0000000..375b76d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/h.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/h.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/h.targetsize-32.png
new file mode 100644
index 0000000..cd02ed1
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/h.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/h.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/h.targetsize-48.png
new file mode 100644
index 0000000..1ad06fc
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/h.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/h.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/h.targetsize-512.png
new file mode 100644
index 0000000..0610d00
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/h.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/html.png b/src/src/Notepads/Assets/FileIcons/html.png
new file mode 100644
index 0000000..b755ccb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/html.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/html.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/html.targetsize-16.png
new file mode 100644
index 0000000..6730e6c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/html.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/html.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/html.targetsize-32.png
new file mode 100644
index 0000000..7433703
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/html.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/html.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/html.targetsize-48.png
new file mode 100644
index 0000000..c80a90e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/html.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/html.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/html.targetsize-512.png
new file mode 100644
index 0000000..b755ccb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/html.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ini.png b/src/src/Notepads/Assets/FileIcons/ini.png
new file mode 100644
index 0000000..10f6df1
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ini.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ini.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/ini.targetsize-16.png
new file mode 100644
index 0000000..d90bf38
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ini.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ini.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/ini.targetsize-32.png
new file mode 100644
index 0000000..7c35246
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ini.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ini.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/ini.targetsize-48.png
new file mode 100644
index 0000000..3ce31a5
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ini.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ini.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/ini.targetsize-512.png
new file mode 100644
index 0000000..10f6df1
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ini.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/java.png b/src/src/Notepads/Assets/FileIcons/java.png
new file mode 100644
index 0000000..8bb81e2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/java.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/java.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/java.targetsize-16.png
new file mode 100644
index 0000000..4bc8d82
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/java.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/java.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/java.targetsize-32.png
new file mode 100644
index 0000000..daa099d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/java.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/java.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/java.targetsize-48.png
new file mode 100644
index 0000000..9bd339b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/java.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/java.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/java.targetsize-512.png
new file mode 100644
index 0000000..8bb81e2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/java.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/js.png b/src/src/Notepads/Assets/FileIcons/js.png
new file mode 100644
index 0000000..51037d7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/js.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/js.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/js.targetsize-16.png
new file mode 100644
index 0000000..cb0dd48
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/js.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/js.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/js.targetsize-32.png
new file mode 100644
index 0000000..383dfa4
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/js.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/js.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/js.targetsize-48.png
new file mode 100644
index 0000000..aae166d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/js.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/js.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/js.targetsize-512.png
new file mode 100644
index 0000000..51037d7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/js.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/json.png b/src/src/Notepads/Assets/FileIcons/json.png
new file mode 100644
index 0000000..d4a3608
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/json.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/json.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/json.targetsize-16.png
new file mode 100644
index 0000000..bc1e82e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/json.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/json.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/json.targetsize-32.png
new file mode 100644
index 0000000..c7d0e6c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/json.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/json.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/json.targetsize-48.png
new file mode 100644
index 0000000..cdcbe24
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/json.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/json.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/json.targetsize-512.png
new file mode 100644
index 0000000..d4a3608
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/json.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/jsp.png b/src/src/Notepads/Assets/FileIcons/jsp.png
new file mode 100644
index 0000000..f1652d2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/jsp.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/jsp.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-16.png
new file mode 100644
index 0000000..092f961
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/jsp.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-32.png
new file mode 100644
index 0000000..f319389
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/jsp.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-48.png
new file mode 100644
index 0000000..8425554
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/jsp.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-512.png
new file mode 100644
index 0000000..f1652d2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/jsp.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/log.png b/src/src/Notepads/Assets/FileIcons/log.png
new file mode 100644
index 0000000..14d7dcf
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/log.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/log.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/log.targetsize-16.png
new file mode 100644
index 0000000..a4f030b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/log.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/log.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/log.targetsize-32.png
new file mode 100644
index 0000000..da800ae
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/log.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/log.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/log.targetsize-48.png
new file mode 100644
index 0000000..d64ce9d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/log.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/log.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/log.targetsize-512.png
new file mode 100644
index 0000000..14d7dcf
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/log.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/md.png b/src/src/Notepads/Assets/FileIcons/md.png
new file mode 100644
index 0000000..51cb6d7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/md.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/md.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/md.targetsize-16.png
new file mode 100644
index 0000000..d74f9f8
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/md.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/md.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/md.targetsize-32.png
new file mode 100644
index 0000000..a465ac5
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/md.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/md.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/md.targetsize-48.png
new file mode 100644
index 0000000..795fd3d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/md.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/md.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/md.targetsize-512.png
new file mode 100644
index 0000000..51cb6d7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/md.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/perl.png b/src/src/Notepads/Assets/FileIcons/perl.png
new file mode 100644
index 0000000..ebc544e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/perl.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/perl.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/perl.targetsize-16.png
new file mode 100644
index 0000000..df8ffc6
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/perl.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/perl.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/perl.targetsize-32.png
new file mode 100644
index 0000000..f80d8ca
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/perl.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/perl.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/perl.targetsize-48.png
new file mode 100644
index 0000000..c9b858e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/perl.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/perl.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/perl.targetsize-512.png
new file mode 100644
index 0000000..ebc544e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/perl.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/php.png b/src/src/Notepads/Assets/FileIcons/php.png
new file mode 100644
index 0000000..af12db3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/php.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/php.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/php.targetsize-16.png
new file mode 100644
index 0000000..8f8c4b7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/php.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/php.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/php.targetsize-32.png
new file mode 100644
index 0000000..53f280c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/php.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/php.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/php.targetsize-48.png
new file mode 100644
index 0000000..a35e6dc
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/php.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/php.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/php.targetsize-512.png
new file mode 100644
index 0000000..af12db3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/php.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/py.png b/src/src/Notepads/Assets/FileIcons/py.png
new file mode 100644
index 0000000..e9f5412
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/py.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/py.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/py.targetsize-16.png
new file mode 100644
index 0000000..cdb9179
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/py.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/py.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/py.targetsize-32.png
new file mode 100644
index 0000000..f8f977c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/py.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/py.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/py.targetsize-48.png
new file mode 100644
index 0000000..ea01b3f
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/py.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/py.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/py.targetsize-512.png
new file mode 100644
index 0000000..e9f5412
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/py.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rb.png b/src/src/Notepads/Assets/FileIcons/rb.png
new file mode 100644
index 0000000..539a75b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rb.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rb.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/rb.targetsize-16.png
new file mode 100644
index 0000000..c5f97b1
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rb.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rb.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/rb.targetsize-32.png
new file mode 100644
index 0000000..d793ddc
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rb.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rb.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/rb.targetsize-48.png
new file mode 100644
index 0000000..e1066d3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rb.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rb.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/rb.targetsize-512.png
new file mode 100644
index 0000000..539a75b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rb.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rc.png b/src/src/Notepads/Assets/FileIcons/rc.png
new file mode 100644
index 0000000..4ac2f3c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rc.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rc.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/rc.targetsize-16.png
new file mode 100644
index 0000000..f5a154b
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rc.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rc.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/rc.targetsize-32.png
new file mode 100644
index 0000000..c1629e2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rc.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rc.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/rc.targetsize-48.png
new file mode 100644
index 0000000..1093ea5
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rc.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/rc.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/rc.targetsize-512.png
new file mode 100644
index 0000000..4ac2f3c
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/rc.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sh.png b/src/src/Notepads/Assets/FileIcons/sh.png
new file mode 100644
index 0000000..0e87ee8
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sh.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sh.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/sh.targetsize-16.png
new file mode 100644
index 0000000..bbd70fd
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sh.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sh.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/sh.targetsize-32.png
new file mode 100644
index 0000000..519f6ce
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sh.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sh.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/sh.targetsize-48.png
new file mode 100644
index 0000000..dab7fc7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sh.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sh.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/sh.targetsize-512.png
new file mode 100644
index 0000000..0e87ee8
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sh.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sql.png b/src/src/Notepads/Assets/FileIcons/sql.png
new file mode 100644
index 0000000..de8a307
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sql.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sql.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/sql.targetsize-16.png
new file mode 100644
index 0000000..74cd842
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sql.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sql.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/sql.targetsize-32.png
new file mode 100644
index 0000000..9fabf63
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sql.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sql.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/sql.targetsize-48.png
new file mode 100644
index 0000000..dc663a1
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sql.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/sql.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/sql.targetsize-512.png
new file mode 100644
index 0000000..de8a307
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/sql.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/srt.png b/src/src/Notepads/Assets/FileIcons/srt.png
new file mode 100644
index 0000000..8834548
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/srt.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/srt.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/srt.targetsize-16.png
new file mode 100644
index 0000000..a758f76
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/srt.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/srt.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/srt.targetsize-32.png
new file mode 100644
index 0000000..685e5b9
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/srt.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/srt.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/srt.targetsize-48.png
new file mode 100644
index 0000000..df74ac6
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/srt.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/srt.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/srt.targetsize-512.png
new file mode 100644
index 0000000..8834548
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/srt.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ssa.png b/src/src/Notepads/Assets/FileIcons/ssa.png
new file mode 100644
index 0000000..3904483
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ssa.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ssa.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-16.png
new file mode 100644
index 0000000..b00ff5f
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ssa.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-32.png
new file mode 100644
index 0000000..83e7887
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ssa.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-48.png
new file mode 100644
index 0000000..75f939a
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/ssa.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-512.png
new file mode 100644
index 0000000..3904483
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/ssa.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/txt.png b/src/src/Notepads/Assets/FileIcons/txt.png
new file mode 100644
index 0000000..db35980
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/txt.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/txt.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/txt.targetsize-16.png
new file mode 100644
index 0000000..5f018c7
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/txt.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/txt.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/txt.targetsize-32.png
new file mode 100644
index 0000000..4a434eb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/txt.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/txt.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/txt.targetsize-48.png
new file mode 100644
index 0000000..3d25eb2
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/txt.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/txt.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/txt.targetsize-512.png
new file mode 100644
index 0000000..db35980
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/txt.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vb.png b/src/src/Notepads/Assets/FileIcons/vb.png
new file mode 100644
index 0000000..48673e3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vb.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vb.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/vb.targetsize-16.png
new file mode 100644
index 0000000..6593ceb
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vb.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vb.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/vb.targetsize-32.png
new file mode 100644
index 0000000..aa84eb4
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vb.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vb.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/vb.targetsize-48.png
new file mode 100644
index 0000000..e06c97e
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vb.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vb.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/vb.targetsize-512.png
new file mode 100644
index 0000000..48673e3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vb.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vue.png b/src/src/Notepads/Assets/FileIcons/vue.png
new file mode 100644
index 0000000..03ec7be
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vue.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vue.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/vue.targetsize-16.png
new file mode 100644
index 0000000..a207302
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vue.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vue.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/vue.targetsize-32.png
new file mode 100644
index 0000000..2aa7258
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vue.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vue.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/vue.targetsize-48.png
new file mode 100644
index 0000000..e0d0a1d
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vue.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/vue.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/vue.targetsize-512.png
new file mode 100644
index 0000000..03ec7be
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/vue.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/xml.png b/src/src/Notepads/Assets/FileIcons/xml.png
new file mode 100644
index 0000000..dfdd1c0
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/xml.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/xml.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/xml.targetsize-16.png
new file mode 100644
index 0000000..f2ae073
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/xml.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/xml.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/xml.targetsize-32.png
new file mode 100644
index 0000000..b343427
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/xml.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/xml.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/xml.targetsize-48.png
new file mode 100644
index 0000000..0332152
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/xml.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/xml.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/xml.targetsize-512.png
new file mode 100644
index 0000000..dfdd1c0
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/xml.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/yml.png b/src/src/Notepads/Assets/FileIcons/yml.png
new file mode 100644
index 0000000..c4df07f
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/yml.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/yml.targetsize-16.png b/src/src/Notepads/Assets/FileIcons/yml.targetsize-16.png
new file mode 100644
index 0000000..5675ae6
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/yml.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/yml.targetsize-32.png b/src/src/Notepads/Assets/FileIcons/yml.targetsize-32.png
new file mode 100644
index 0000000..56eb3e3
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/yml.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/yml.targetsize-48.png b/src/src/Notepads/Assets/FileIcons/yml.targetsize-48.png
new file mode 100644
index 0000000..2e5d892
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/yml.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/FileIcons/yml.targetsize-512.png b/src/src/Notepads/Assets/FileIcons/yml.targetsize-512.png
new file mode 100644
index 0000000..c4df07f
Binary files /dev/null and b/src/src/Notepads/Assets/FileIcons/yml.targetsize-512.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16-dev.png
new file mode 100644
index 0000000..9ca9b88
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16.png
new file mode 100644
index 0000000..7b07e1f
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20-dev.png
new file mode 100644
index 0000000..cb6cf59
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20.png
new file mode 100644
index 0000000..e2a5858
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-20.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24-dev.png
new file mode 100644
index 0000000..f8475d5
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24.png
new file mode 100644
index 0000000..70e0300
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-24.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256-dev.png
new file mode 100644
index 0000000..924047b
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256.png
new file mode 100644
index 0000000..4dfc2a7
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-256.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32-dev.png
new file mode 100644
index 0000000..0a02dc9
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32.png
new file mode 100644
index 0000000..041fe06
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44-dev.png
new file mode 100644
index 0000000..35ad50a
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44.png b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44.png
new file mode 100644
index 0000000..076234d
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.light.targetsize-44.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16-dev.png
new file mode 100644
index 0000000..47098f4
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16.png
new file mode 100644
index 0000000..acbcbdd
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20-dev.png
new file mode 100644
index 0000000..e66d648
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20.png
new file mode 100644
index 0000000..e1438ca
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-20.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24-dev.png
new file mode 100644
index 0000000..1fcac07
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24.png
new file mode 100644
index 0000000..854f12c
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-24.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256-dev.png
new file mode 100644
index 0000000..9bcab79
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256.png
new file mode 100644
index 0000000..78bb190
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-256.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32-dev.png
new file mode 100644
index 0000000..163faa6
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32.png
new file mode 100644
index 0000000..1b57f04
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44-dev.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44-dev.png
new file mode 100644
index 0000000..9657add
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44-dev.png differ
diff --git a/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44.png b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44.png
new file mode 100644
index 0000000..9d261d1
Binary files /dev/null and b/src/src/Notepads/Assets/GameBar/Icons/icon.targetsize-44.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-100-dev.png b/src/src/Notepads/Assets/LargeTile.scale-100-dev.png
new file mode 100644
index 0000000..d51102a
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-100.png b/src/src/Notepads/Assets/LargeTile.scale-100.png
new file mode 100644
index 0000000..71fe8e8
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-100.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..9873fd9
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light.png b/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light.png
new file mode 100644
index 0000000..3d723f8
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-100_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-125-dev.png b/src/src/Notepads/Assets/LargeTile.scale-125-dev.png
new file mode 100644
index 0000000..3aa5265
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-125.png b/src/src/Notepads/Assets/LargeTile.scale-125.png
new file mode 100644
index 0000000..c29f3e0
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-125.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..6b97218
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light.png b/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light.png
new file mode 100644
index 0000000..9401a5e
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-125_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-150-dev.png b/src/src/Notepads/Assets/LargeTile.scale-150-dev.png
new file mode 100644
index 0000000..41b86e0
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-150.png b/src/src/Notepads/Assets/LargeTile.scale-150.png
new file mode 100644
index 0000000..9429d54
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-150.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..39a21de
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light.png b/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light.png
new file mode 100644
index 0000000..9505c7b
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-150_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-200-dev.png b/src/src/Notepads/Assets/LargeTile.scale-200-dev.png
new file mode 100644
index 0000000..e9672c1
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-200.png b/src/src/Notepads/Assets/LargeTile.scale-200.png
new file mode 100644
index 0000000..7e628de
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-200.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..b77cbd1
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light.png b/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light.png
new file mode 100644
index 0000000..aae8d00
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-200_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-400-dev.png b/src/src/Notepads/Assets/LargeTile.scale-400-dev.png
new file mode 100644
index 0000000..7d7a4be
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-400.png b/src/src/Notepads/Assets/LargeTile.scale-400.png
new file mode 100644
index 0000000..4782864
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-400.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..6602805
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light.png b/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light.png
new file mode 100644
index 0000000..35ad483
Binary files /dev/null and b/src/src/Notepads/Assets/LargeTile.scale-400_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/LockScreenLogo.scale-200.png b/src/src/Notepads/Assets/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000..6cd2110
Binary files /dev/null and b/src/src/Notepads/Assets/LockScreenLogo.scale-200.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-100-dev.png b/src/src/Notepads/Assets/SmallTile.scale-100-dev.png
new file mode 100644
index 0000000..be93fe1
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-100.png b/src/src/Notepads/Assets/SmallTile.scale-100.png
new file mode 100644
index 0000000..c609e22
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-100.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..48a0bec
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light.png b/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light.png
new file mode 100644
index 0000000..da51be5
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-100_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-125-dev.png b/src/src/Notepads/Assets/SmallTile.scale-125-dev.png
new file mode 100644
index 0000000..d4f4147
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-125.png b/src/src/Notepads/Assets/SmallTile.scale-125.png
new file mode 100644
index 0000000..6e3b987
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-125.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..88d1d04
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light.png b/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light.png
new file mode 100644
index 0000000..6ef87e4
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-125_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-150-dev.png b/src/src/Notepads/Assets/SmallTile.scale-150-dev.png
new file mode 100644
index 0000000..4e69a8f
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-150.png b/src/src/Notepads/Assets/SmallTile.scale-150.png
new file mode 100644
index 0000000..1605284
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-150.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..b0abb1f
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light.png b/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light.png
new file mode 100644
index 0000000..112109f
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-150_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-200-dev.png b/src/src/Notepads/Assets/SmallTile.scale-200-dev.png
new file mode 100644
index 0000000..8c0f299
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-200.png b/src/src/Notepads/Assets/SmallTile.scale-200.png
new file mode 100644
index 0000000..9a793f3
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-200.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..1705bae
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light.png b/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light.png
new file mode 100644
index 0000000..b71a227
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-200_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-400-dev.png b/src/src/Notepads/Assets/SmallTile.scale-400-dev.png
new file mode 100644
index 0000000..8e8315c
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-400.png b/src/src/Notepads/Assets/SmallTile.scale-400.png
new file mode 100644
index 0000000..b50ec12
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-400.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..a626051
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light.png b/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light.png
new file mode 100644
index 0000000..14b9420
Binary files /dev/null and b/src/src/Notepads/Assets/SmallTile.scale-400_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-100-dev.png b/src/src/Notepads/Assets/SplashScreen.scale-100-dev.png
new file mode 100644
index 0000000..08190da
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-100.png b/src/src/Notepads/Assets/SplashScreen.scale-100.png
new file mode 100644
index 0000000..4661406
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-100.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-125-dev.png b/src/src/Notepads/Assets/SplashScreen.scale-125-dev.png
new file mode 100644
index 0000000..b68bf57
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-125.png b/src/src/Notepads/Assets/SplashScreen.scale-125.png
new file mode 100644
index 0000000..5a342c3
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-125.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-150-dev.png b/src/src/Notepads/Assets/SplashScreen.scale-150-dev.png
new file mode 100644
index 0000000..0beb67e
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-150.png b/src/src/Notepads/Assets/SplashScreen.scale-150.png
new file mode 100644
index 0000000..8dd6d82
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-150.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-200-dev.png b/src/src/Notepads/Assets/SplashScreen.scale-200-dev.png
new file mode 100644
index 0000000..1a1f769
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-200.png b/src/src/Notepads/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000..6847769
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-200.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-400-dev.png b/src/src/Notepads/Assets/SplashScreen.scale-400-dev.png
new file mode 100644
index 0000000..3bba7e0
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/SplashScreen.scale-400.png b/src/src/Notepads/Assets/SplashScreen.scale-400.png
new file mode 100644
index 0000000..cb942b1
Binary files /dev/null and b/src/src/Notepads/Assets/SplashScreen.scale-400.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-100-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-100-dev.png
new file mode 100644
index 0000000..a3f82ac
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-100.png b/src/src/Notepads/Assets/Square150x150Logo.scale-100.png
new file mode 100644
index 0000000..4d32cf8
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-100.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..39829b6
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light.png
new file mode 100644
index 0000000..507a77e
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-100_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-125-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-125-dev.png
new file mode 100644
index 0000000..0b72ee3
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-125.png b/src/src/Notepads/Assets/Square150x150Logo.scale-125.png
new file mode 100644
index 0000000..2a9a88d
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-125.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..f94c216
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light.png
new file mode 100644
index 0000000..cfae816
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-125_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-150-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-150-dev.png
new file mode 100644
index 0000000..98626c2
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-150.png b/src/src/Notepads/Assets/Square150x150Logo.scale-150.png
new file mode 100644
index 0000000..bd5ba18
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-150.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..befea4a
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light.png
new file mode 100644
index 0000000..2aa04be
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-150_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-200-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-200-dev.png
new file mode 100644
index 0000000..a24f707
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-200.png b/src/src/Notepads/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000..69b91ca
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-200.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..c0e656c
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light.png
new file mode 100644
index 0000000..1782628
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-200_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-400-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-400-dev.png
new file mode 100644
index 0000000..a53c30f
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-400.png b/src/src/Notepads/Assets/Square150x150Logo.scale-400.png
new file mode 100644
index 0000000..ea8ce4c
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-400.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..54abd89
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light.png
new file mode 100644
index 0000000..ca81f70
Binary files /dev/null and b/src/src/Notepads/Assets/Square150x150Logo.scale-400_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16-dev.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16-dev.png
new file mode 100644
index 0000000..963d114
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16.png
new file mode 100644
index 0000000..32360c7
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256-dev.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256-dev.png
new file mode 100644
index 0000000..9bcab79
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256.png
new file mode 100644
index 0000000..055c1b4
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32-dev.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32-dev.png
new file mode 100644
index 0000000..2507603
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32.png
new file mode 100644
index 0000000..437e8d3
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48-dev.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48-dev.png
new file mode 100644
index 0000000..818650a
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48.png
new file mode 100644
index 0000000..8027739
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-100-dev.png b/src/src/Notepads/Assets/Square44x44Logo.scale-100-dev.png
new file mode 100644
index 0000000..6d102c4
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-100.png b/src/src/Notepads/Assets/Square44x44Logo.scale-100.png
new file mode 100644
index 0000000..8a13c4a
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-100.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-125-dev.png b/src/src/Notepads/Assets/Square44x44Logo.scale-125-dev.png
new file mode 100644
index 0000000..a968778
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-125.png b/src/src/Notepads/Assets/Square44x44Logo.scale-125.png
new file mode 100644
index 0000000..ea69cbd
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-125.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-150-dev.png b/src/src/Notepads/Assets/Square44x44Logo.scale-150-dev.png
new file mode 100644
index 0000000..27e7869
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-150.png b/src/src/Notepads/Assets/Square44x44Logo.scale-150.png
new file mode 100644
index 0000000..4f1d463
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-150.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-200-dev.png b/src/src/Notepads/Assets/Square44x44Logo.scale-200-dev.png
new file mode 100644
index 0000000..e0f4b26
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-200.png b/src/src/Notepads/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000..84403d2
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-200.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-400-dev.png b/src/src/Notepads/Assets/Square44x44Logo.scale-400-dev.png
new file mode 100644
index 0000000..e57a085
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.scale-400.png b/src/src/Notepads/Assets/Square44x44Logo.scale-400.png
new file mode 100644
index 0000000..78b05b0
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.scale-400.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-16-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16-dev.png
new file mode 100644
index 0000000..096dec8
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-16.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16.png
new file mode 100644
index 0000000..ff6464d
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated-dev.png
new file mode 100644
index 0000000..916e527
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated.png
new file mode 100644
index 0000000..c7bfb09
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-16_altform-lightunplated.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24-dev.png
new file mode 100644
index 0000000..36e162f
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24.png
new file mode 100644
index 0000000..0079d4f
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated-dev.png
new file mode 100644
index 0000000..5ed1847
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated.png
new file mode 100644
index 0000000..5a69c6c
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-lightunplated.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated-dev.png
new file mode 100644
index 0000000..cd3b73c
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000..ccb186e
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-256-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256-dev.png
new file mode 100644
index 0000000..9bcab79
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-256.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256.png
new file mode 100644
index 0000000..78bb190
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated-dev.png
new file mode 100644
index 0000000..924047b
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated.png
new file mode 100644
index 0000000..576e087
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-256_altform-lightunplated.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-32-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32-dev.png
new file mode 100644
index 0000000..a1ef3b0
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-32.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32.png
new file mode 100644
index 0000000..5ab493c
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated-dev.png
new file mode 100644
index 0000000..67890e0
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated.png
new file mode 100644
index 0000000..0d2faa8
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-32_altform-lightunplated.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-48-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48-dev.png
new file mode 100644
index 0000000..0da0d25
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-48.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48.png
new file mode 100644
index 0000000..27f611d
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated-dev.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated-dev.png
new file mode 100644
index 0000000..8d4529c
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated-dev.png differ
diff --git a/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png
new file mode 100644
index 0000000..17c17c8
Binary files /dev/null and b/src/src/Notepads/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.backup.png b/src/src/Notepads/Assets/StoreLogo.backup.png
new file mode 100644
index 0000000..44d9114
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.backup.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-100-dev.png b/src/src/Notepads/Assets/StoreLogo.scale-100-dev.png
new file mode 100644
index 0000000..bcd2708
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-100.png b/src/src/Notepads/Assets/StoreLogo.scale-100.png
new file mode 100644
index 0000000..d002c41
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-100.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-125-dev.png b/src/src/Notepads/Assets/StoreLogo.scale-125-dev.png
new file mode 100644
index 0000000..fe0977e
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-125.png b/src/src/Notepads/Assets/StoreLogo.scale-125.png
new file mode 100644
index 0000000..594c8cb
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-125.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-150-dev.png b/src/src/Notepads/Assets/StoreLogo.scale-150-dev.png
new file mode 100644
index 0000000..dd3c644
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-150.png b/src/src/Notepads/Assets/StoreLogo.scale-150.png
new file mode 100644
index 0000000..6799fb4
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-150.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-200-dev.png b/src/src/Notepads/Assets/StoreLogo.scale-200-dev.png
new file mode 100644
index 0000000..0b35047
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-200.png b/src/src/Notepads/Assets/StoreLogo.scale-200.png
new file mode 100644
index 0000000..ba5fc24
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-200.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-400-dev.png b/src/src/Notepads/Assets/StoreLogo.scale-400-dev.png
new file mode 100644
index 0000000..c13340f
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/StoreLogo.scale-400.png b/src/src/Notepads/Assets/StoreLogo.scale-400.png
new file mode 100644
index 0000000..8c8f283
Binary files /dev/null and b/src/src/Notepads/Assets/StoreLogo.scale-400.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-100-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-100-dev.png
new file mode 100644
index 0000000..ed7fb37
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-100-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-100.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-100.png
new file mode 100644
index 0000000..3456b2c
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-100.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..6ca1b2d
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light.png
new file mode 100644
index 0000000..8ba0d84
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-100_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-125-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-125-dev.png
new file mode 100644
index 0000000..36fa4bf
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-125-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-125.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-125.png
new file mode 100644
index 0000000..52520bd
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-125.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..ca329c6
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light.png
new file mode 100644
index 0000000..26ed923
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-125_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-150-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-150-dev.png
new file mode 100644
index 0000000..c2cd368
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-150-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-150.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-150.png
new file mode 100644
index 0000000..ad4497b
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-150.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..6520fb3
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light.png
new file mode 100644
index 0000000..aa38e0c
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-150_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-200-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-200-dev.png
new file mode 100644
index 0000000..5256f7e
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-200-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-200.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000..16e50f5
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..5a91b67
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light.png
new file mode 100644
index 0000000..3ad4cad
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-200_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-400-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-400-dev.png
new file mode 100644
index 0000000..1f032fb
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-400-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-400.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-400.png
new file mode 100644
index 0000000..f7b6fc4
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-400.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light-dev.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light-dev.png
new file mode 100644
index 0000000..c35b0f9
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light-dev.png differ
diff --git a/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light.png b/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light.png
new file mode 100644
index 0000000..5c15b4c
Binary files /dev/null and b/src/src/Notepads/Assets/Wide310x150Logo.scale-400_altform-colorful_theme-light.png differ
diff --git a/src/src/Notepads/Assets/appicon_b-dev.png b/src/src/Notepads/Assets/appicon_b-dev.png
new file mode 100644
index 0000000..89c9c1d
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_b-dev.png differ
diff --git a/src/src/Notepads/Assets/appicon_b.png b/src/src/Notepads/Assets/appicon_b.png
new file mode 100644
index 0000000..f20cb34
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_b.png differ
diff --git a/src/src/Notepads/Assets/appicon_bs-dev.png b/src/src/Notepads/Assets/appicon_bs-dev.png
new file mode 100644
index 0000000..9540e9e
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_bs-dev.png differ
diff --git a/src/src/Notepads/Assets/appicon_bs.png b/src/src/Notepads/Assets/appicon_bs.png
new file mode 100644
index 0000000..161d5db
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_bs.png differ
diff --git a/src/src/Notepads/Assets/appicon_w-dev.png b/src/src/Notepads/Assets/appicon_w-dev.png
new file mode 100644
index 0000000..2c455da
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_w-dev.png differ
diff --git a/src/src/Notepads/Assets/appicon_w.png b/src/src/Notepads/Assets/appicon_w.png
new file mode 100644
index 0000000..2be8aee
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_w.png differ
diff --git a/src/src/Notepads/Assets/appicon_ws-dev.gif b/src/src/Notepads/Assets/appicon_ws-dev.gif
new file mode 100644
index 0000000..e4b2049
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_ws-dev.gif differ
diff --git a/src/src/Notepads/Assets/appicon_ws-dev.png b/src/src/Notepads/Assets/appicon_ws-dev.png
new file mode 100644
index 0000000..b8f392d
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_ws-dev.png differ
diff --git a/src/src/Notepads/Assets/appicon_ws.gif b/src/src/Notepads/Assets/appicon_ws.gif
new file mode 100644
index 0000000..ced6b19
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_ws.gif differ
diff --git a/src/src/Notepads/Assets/appicon_ws.png b/src/src/Notepads/Assets/appicon_ws.png
new file mode 100644
index 0000000..76a007a
Binary files /dev/null and b/src/src/Notepads/Assets/appicon_ws.png differ
diff --git a/src/src/Notepads/Assets/no_noise.png b/src/src/Notepads/Assets/no_noise.png
new file mode 100644
index 0000000..f7ff7dd
Binary files /dev/null and b/src/src/Notepads/Assets/no_noise.png differ
diff --git a/src/src/Notepads/Assets/noise_high.png b/src/src/Notepads/Assets/noise_high.png
new file mode 100644
index 0000000..434bc9e
Binary files /dev/null and b/src/src/Notepads/Assets/noise_high.png differ
diff --git a/src/src/Notepads/Assets/noise_low.png b/src/src/Notepads/Assets/noise_low.png
new file mode 100644
index 0000000..35f703f
Binary files /dev/null and b/src/src/Notepads/Assets/noise_low.png differ
diff --git a/src/src/Notepads/Assets/search_bing.png b/src/src/Notepads/Assets/search_bing.png
new file mode 100644
index 0000000..3c3ac22
Binary files /dev/null and b/src/src/Notepads/Assets/search_bing.png differ
diff --git a/src/src/Notepads/Assets/search_custom.png b/src/src/Notepads/Assets/search_custom.png
new file mode 100644
index 0000000..0a816cc
Binary files /dev/null and b/src/src/Notepads/Assets/search_custom.png differ
diff --git a/src/src/Notepads/Assets/search_duckduckgo.png b/src/src/Notepads/Assets/search_duckduckgo.png
new file mode 100644
index 0000000..7fd5a71
Binary files /dev/null and b/src/src/Notepads/Assets/search_duckduckgo.png differ
diff --git a/src/src/Notepads/Assets/search_google.png b/src/src/Notepads/Assets/search_google.png
new file mode 100644
index 0000000..80a2a2b
Binary files /dev/null and b/src/src/Notepads/Assets/search_google.png differ
diff --git a/src/src/Notepads/Brushes/HostBackdropAcrylicBrush.cs b/src/src/Notepads/Brushes/HostBackdropAcrylicBrush.cs
new file mode 100644
index 0000000..20105ee
--- /dev/null
+++ b/src/src/Notepads/Brushes/HostBackdropAcrylicBrush.cs
@@ -0,0 +1,355 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Brushes
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Numerics;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Windows.Foundation;
+ using Windows.Graphics.DirectX;
+ using Windows.Graphics.Display;
+ using Windows.Graphics.Effects;
+ using Windows.Graphics.Imaging;
+ using Windows.System;
+ using Windows.System.Power;
+ using Windows.UI;
+ using Windows.UI.Composition;
+ using Windows.UI.ViewManagement;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media;
+ using Microsoft.Graphics.Canvas;
+ using Microsoft.Graphics.Canvas.Effects;
+ using Microsoft.Graphics.Canvas.UI.Composition;
+ using Controls.Helpers;
+ using Notepads.Services;
+
+ public sealed class HostBackdropAcrylicBrush : XamlCompositionBrushBase, IDisposable
+ {
+ private static readonly DependencyProperty TintOpacityProperty = DependencyProperty.Register(
+ nameof(TintOpacity),
+ typeof(float),
+ typeof(HostBackdropAcrylicBrush),
+ new PropertyMetadata(0.0f, OnTintOpacityChanged)
+ );
+
+ public float TintOpacity
+ {
+ get => (float)GetValue(TintOpacityProperty);
+ set => SetValue(TintOpacityProperty, value);
+ }
+
+ private static void OnTintOpacityChanged(DependencyObject dependencyObject,
+ DependencyPropertyChangedEventArgs args)
+ {
+ if (!(dependencyObject is HostBackdropAcrylicBrush brush)) return;
+
+ if (brush.CompositionBrush is CompositionEffectBrush)
+ {
+ TintOpacityToArithmeticCompositeEffectSourceAmount((float)args.NewValue,
+ _acrylicTintOpacityMinThreshold,
+ out var source1Amount,
+ out var source2Amount);
+ brush.CompositionBrush?.Properties.InsertScalar("LuminosityBlender.Source1Amount", source1Amount);
+ brush.CompositionBrush?.Properties.InsertScalar("LuminosityBlender.Source2Amount", source2Amount);
+ }
+ else if (brush.CompositionBrush is CompositionColorBrush)
+ {
+ // Do nothing since we are falling back to CompositionColorBrush here
+ // TintOpacity only applies to the CompositionEffectBrush we created
+ }
+ }
+
+ private static readonly DependencyProperty LuminosityColorProperty = DependencyProperty.Register(
+ nameof(LuminosityColor),
+ typeof(Color),
+ typeof(HostBackdropAcrylicBrush),
+ new PropertyMetadata(Colors.Transparent, OnLuminosityColorChanged)
+ );
+
+ public Color LuminosityColor
+ {
+ get => (Color)GetValue(LuminosityColorProperty);
+ set => SetValue(LuminosityColorProperty, value);
+ }
+
+ private static void OnLuminosityColorChanged(DependencyObject dependencyObject,
+ DependencyPropertyChangedEventArgs args)
+ {
+ if (!(dependencyObject is HostBackdropAcrylicBrush brush)) return;
+
+ switch (brush.CompositionBrush)
+ {
+ case CompositionEffectBrush _
+ when brush.CompositionBrush?.Properties.TryGetColor("LuminosityColor.Color", out var currentColor)
+ == CompositionGetValueStatus.Succeeded:
+ {
+ var easing = Window.Current.Compositor.CreateLinearEasingFunction();
+ var animation = Window.Current.Compositor.CreateColorKeyFrameAnimation();
+ animation.InsertKeyFrame(0.0f, currentColor);
+ animation.InsertKeyFrame(1.0f, (Color)args.NewValue, easing);
+ animation.Duration = TimeSpan.FromMilliseconds(167);
+ brush.CompositionBrush.StartAnimation("LuminosityColor.Color", animation);
+ break;
+ }
+ case CompositionEffectBrush _:
+ brush.CompositionBrush?.Properties.InsertColor("LuminosityColor.Color", (Color)args.NewValue);
+ break;
+ case CompositionColorBrush colorBrush:
+ colorBrush.Color = (Color)args.NewValue;
+ break;
+ }
+ }
+
+ public Uri NoiseTextureUri { get; set; }
+
+ private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1);
+
+ private readonly UISettings UISettings = new UISettings();
+
+ private const float _acrylicTintOpacityMinThreshold = 0.35f;
+
+ private readonly DispatcherQueue _dispatcherQueue;
+
+ private CompositionSurfaceBrush _noiseBrush;
+
+ public HostBackdropAcrylicBrush()
+ {
+ _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ }
+
+ protected override async void OnConnected()
+ {
+ if (CompositionBrush == null)
+ {
+ await BuildInternalAsync();
+ }
+ base.OnConnected();
+ }
+
+ private async Task BuildInternalAsync()
+ {
+ await _semaphoreSlim.WaitAsync();
+ try
+ {
+ if (PowerManager.EnergySaverStatus == EnergySaverStatus.On || !UISettings.AdvancedEffectsEnabled)
+ {
+ CompositionBrush = Window.Current.Compositor.CreateColorBrush(LuminosityColor);
+ }
+ else
+ {
+ CompositionBrush = await BuildHostBackdropAcrylicBrushInternalAsync();
+ }
+
+ // Register energy saver event
+ PowerManager.EnergySaverStatusChanged -= OnEnergySaverStatusChanged;
+ PowerManager.EnergySaverStatusChanged += OnEnergySaverStatusChanged;
+
+ // Register system level transparency effects settings change event
+ UISettings.AdvancedEffectsEnabledChanged -= OnAdvancedEffectsEnabledChanged;
+ UISettings.AdvancedEffectsEnabledChanged += OnAdvancedEffectsEnabledChanged;
+ }
+ catch (Exception)
+ {
+ // Fallback to color brush if unable to create HostBackdropAcrylicBrush
+ CompositionBrush = Window.Current.Compositor.CreateColorBrush(LuminosityColor);
+ }
+ finally
+ {
+ _semaphoreSlim.Release();
+ }
+ }
+
+ private async void OnEnergySaverStatusChanged(object sender, object e)
+ {
+ await _dispatcherQueue.ExecuteOnUIThreadAsync(async () =>
+ {
+ await BuildInternalAsync();
+ });
+ }
+
+ private async void OnAdvancedEffectsEnabledChanged(UISettings sender, object args)
+ {
+ await _dispatcherQueue.ExecuteOnUIThreadAsync(async () =>
+ {
+ await BuildInternalAsync();
+ });
+ }
+
+ protected override async void OnDisconnected()
+ {
+ await _semaphoreSlim.WaitAsync();
+ if (CompositionBrush != null)
+ {
+ PowerManager.EnergySaverStatusChanged -= OnEnergySaverStatusChanged;
+ UISettings.AdvancedEffectsEnabledChanged -= OnAdvancedEffectsEnabledChanged;
+
+ CompositionBrush.Dispose();
+ CompositionBrush = null;
+ }
+ _semaphoreSlim.Release();
+ base.OnDisconnected();
+ }
+
+ private async Task BuildHostBackdropAcrylicBrushInternalAsync()
+ {
+ int stage = 0;
+
+ try
+ {
+ stage = 1;
+ var luminosityColorEffect = new ColorSourceEffect()
+ {
+ Name = "LuminosityColor",
+ Color = LuminosityColor
+ };
+
+ TintOpacityToArithmeticCompositeEffectSourceAmount(TintOpacity, _acrylicTintOpacityMinThreshold,
+ out var source1Amount,
+ out var source2Amount);
+
+ stage = 2;
+ var luminosityBlendingEffect = new ArithmeticCompositeEffect
+ {
+ Name = "LuminosityBlender",
+ Source1 = new CompositionEffectSourceParameter("Backdrop"),
+ Source2 = luminosityColorEffect,
+ MultiplyAmount = 0,
+ Source1Amount = source1Amount,
+ Source2Amount = source2Amount,
+ Offset = 0
+ };
+
+ stage = 3;
+ var noiseBorderEffect = new BorderEffect()
+ {
+ ExtendX = CanvasEdgeBehavior.Wrap,
+ ExtendY = CanvasEdgeBehavior.Wrap,
+ Source = new CompositionEffectSourceParameter("Noise"),
+ };
+
+ stage = 4;
+ var noiseBlendingEffect = new BlendEffect()
+ {
+ Name = "NoiseBlender",
+ Mode = BlendEffectMode.Overlay,
+ Background = luminosityBlendingEffect,
+ Foreground = noiseBorderEffect
+ };
+
+ stage = 5;
+ _noiseBrush = _noiseBrush ?? await LoadImageBrushAsync(NoiseTextureUri);
+
+ IGraphicsEffect finalEffect;
+
+ if (_noiseBrush == null)
+ {
+ finalEffect = luminosityBlendingEffect;
+ }
+ else
+ {
+ finalEffect = noiseBlendingEffect;
+ }
+
+ stage = 6;
+ CompositionEffectFactory effectFactory = Window.Current.Compositor.CreateEffectFactory(finalEffect,
+ new[]
+ {
+ "LuminosityColor.Color",
+ "LuminosityBlender.Source1Amount",
+ "LuminosityBlender.Source2Amount"
+ });
+
+ stage = 7;
+ CompositionEffectBrush brush = effectFactory.CreateBrush();
+
+ stage = 8;
+ var hostBackdropBrush = Window.Current.Compositor.CreateHostBackdropBrush();
+ brush.SetSourceParameter("Backdrop", hostBackdropBrush);
+
+ stage = 9;
+ if (_noiseBrush != null)
+ {
+ brush.SetSourceParameter("Noise", _noiseBrush);
+ }
+
+ return brush;
+ }
+ catch (Exception ex)
+ {
+ AnalyticsService.TrackEvent("FailedToBuildAcrylicBrushInternal",
+ new Dictionary
+ {
+ { "Exception", ex.ToString() },
+ { "FailedAtStage", stage.ToString() },
+ });
+ throw; // rethrow here
+ }
+ }
+
+ private static async Task LoadImageBrushAsync(Uri textureUri)
+ {
+ try
+ {
+ using (CanvasDevice sharedDevice = CanvasDevice.GetSharedDevice())
+ {
+ DisplayInformation display = DisplayInformation.GetForCurrentView();
+ float dpi = display.LogicalDpi;
+
+ CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(sharedDevice, textureUri, dpi >= 96 ? dpi : 96);
+
+ CompositionGraphicsDevice device = CanvasComposition.CreateCompositionGraphicsDevice(Window.Current.Compositor, sharedDevice);
+ CompositionDrawingSurface surface = device.CreateDrawingSurface(default, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
+
+ Size size = bitmap.Size;
+ Size sizeInPixels = new Size(bitmap.SizeInPixels.Width, bitmap.SizeInPixels.Height);
+ CanvasComposition.Resize(surface, sizeInPixels);
+
+ using (CanvasDrawingSession session = CanvasComposition.CreateDrawingSession(surface, new Rect(0, 0, sizeInPixels.Width, sizeInPixels.Height), dpi))
+ {
+ session.Clear(Color.FromArgb(0, 0, 0, 0));
+ session.DrawImage(bitmap, new Rect(0, 0, size.Width, size.Height), new Rect(0, 0, size.Width, size.Height));
+ session.EffectTileSize = new BitmapSize { Width = (uint)size.Width, Height = (uint)size.Height };
+
+ CompositionSurfaceBrush brush = surface.Compositor.CreateSurfaceBrush(surface);
+ brush.Stretch = CompositionStretch.None;
+ double pixels = display.RawPixelsPerViewPixel;
+ if (pixels > 1)
+ {
+ brush.Scale = new Vector2((float)(1 / pixels));
+ brush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.NearestNeighbor;
+ }
+ return brush;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ AnalyticsService.TrackEvent("FailedToLoadImageBrush", new Dictionary { { "Exception", ex.ToString() } });
+ return null;
+ }
+ }
+
+ private static void TintOpacityToArithmeticCompositeEffectSourceAmount(float tintOpacity, float minThreshold,
+ out float source1Amount, out float source2Amount)
+ {
+ minThreshold = Math.Clamp(minThreshold, 0, 1);
+ var adjustedTintOpacity = Math.Clamp(tintOpacity, 0, 1);
+ adjustedTintOpacity = ((1 - minThreshold) * adjustedTintOpacity) + minThreshold;
+ source1Amount = 1 - adjustedTintOpacity;
+ source2Amount = adjustedTintOpacity;
+ }
+
+ public void Dispose()
+ {
+ PowerManager.EnergySaverStatusChanged -= OnEnergySaverStatusChanged;
+ UISettings.AdvancedEffectsEnabledChanged -= OnAdvancedEffectsEnabledChanged;
+
+ _semaphoreSlim?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/CommandHandlerResult.cs b/src/src/Notepads/Commands/CommandHandlerResult.cs
new file mode 100644
index 0000000..3ecfeb6
--- /dev/null
+++ b/src/src/Notepads/Commands/CommandHandlerResult.cs
@@ -0,0 +1,30 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ public class CommandHandlerResult
+ {
+ public CommandHandlerResult(bool shouldHandle, bool shouldSwallow)
+ {
+ ShouldHandle = shouldHandle;
+ ShouldSwallow = shouldSwallow;
+ }
+
+ ///
+ /// "ShouldHandle == true" means the keyboard command event should be handled after execution
+ /// Meaning you should set KeyRoutedEventArgs.Handled to true after execution
+ /// All child OnKeyDown event will not be received and should not be triggered if it is true
+ ///
+ public bool ShouldHandle { get; }
+
+ ///
+ /// "ShouldSwallow == true" means the keyboard command event should not go to it's children
+ /// Meaning you should not call base.OnKeyDown after execution
+ /// All parent OnKeyDown event will not be received and should not be triggered if it is true
+ ///
+ public bool ShouldSwallow { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/ICommandHandler.cs b/src/src/Notepads/Commands/ICommandHandler.cs
new file mode 100644
index 0000000..771c3c0
--- /dev/null
+++ b/src/src/Notepads/Commands/ICommandHandler.cs
@@ -0,0 +1,12 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ public interface ICommandHandler
+ {
+ CommandHandlerResult Handle(T args);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/IKeyboardCommand.cs b/src/src/Notepads/Commands/IKeyboardCommand.cs
new file mode 100644
index 0000000..6935d16
--- /dev/null
+++ b/src/src/Notepads/Commands/IKeyboardCommand.cs
@@ -0,0 +1,22 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ using Windows.System;
+
+ public interface IKeyboardCommand
+ {
+ bool Hit(bool ctrlDown, bool altDown, bool shiftDown, VirtualKey key);
+
+ bool ShouldExecute(IKeyboardCommand lastCommand);
+
+ bool ShouldHandleAfterExecution();
+
+ bool ShouldSwallowAfterExecution();
+
+ void Execute(T args);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/IMouseCommand.cs b/src/src/Notepads/Commands/IMouseCommand.cs
new file mode 100644
index 0000000..62b4b1e
--- /dev/null
+++ b/src/src/Notepads/Commands/IMouseCommand.cs
@@ -0,0 +1,24 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ public interface IMouseCommand
+ {
+ bool Hit(
+ bool ctrlDown,
+ bool altDown,
+ bool shiftDown,
+ bool leftButtonDown,
+ bool middleButtonDown,
+ bool rightButtonDown);
+
+ bool ShouldHandleAfterExecution();
+
+ bool ShouldSwallowAfterExecution();
+
+ void Execute(T args);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/KeyboardCommand.cs b/src/src/Notepads/Commands/KeyboardCommand.cs
new file mode 100644
index 0000000..70f1ea1
--- /dev/null
+++ b/src/src/Notepads/Commands/KeyboardCommand.cs
@@ -0,0 +1,115 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.System;
+
+ public sealed class KeyboardCommand : IKeyboardCommand
+ {
+ private static readonly TimeSpan ConsecutiveHitsInterval = TimeSpan.FromMilliseconds(500);
+
+ private readonly bool _ctrl;
+ private readonly bool _alt;
+ private readonly bool _shift;
+ private readonly IList _keys;
+ private readonly Action _action;
+ private readonly bool _shouldHandle;
+ private readonly bool _shouldSwallow;
+ private readonly int _requiredHits;
+ private int _hits;
+ private DateTime _lastHitTimestamp;
+
+ public KeyboardCommand(
+ VirtualKey key,
+ Action action,
+ bool shouldHandle = true,
+ bool shouldSwallow = true) :
+ this(false, false, false, key, action, shouldHandle, shouldSwallow)
+ {
+ }
+
+ public KeyboardCommand(
+ bool ctrlDown,
+ bool altDown,
+ bool shiftDown,
+ VirtualKey key,
+ Action action,
+ bool shouldHandle = true,
+ bool shouldSwallow = true,
+ int requiredHits = 1) :
+ this(ctrlDown, altDown, shiftDown, new List() { key }, action, shouldHandle, shouldSwallow, requiredHits)
+ {
+ }
+
+ public KeyboardCommand(
+ bool ctrlDown,
+ bool altDown,
+ bool shiftDown,
+ IList keys,
+ Action action,
+ bool shouldHandle,
+ bool shouldSwallow,
+ int requiredHits = 1)
+ {
+ _ctrl = ctrlDown;
+ _alt = altDown;
+ _shift = shiftDown;
+ _keys = keys ?? new List();
+ _action = action;
+ _shouldHandle = shouldHandle;
+ _shouldSwallow = shouldSwallow;
+ _requiredHits = requiredHits;
+ _hits = 0;
+ _lastHitTimestamp = DateTime.MinValue;
+ }
+
+ public bool Hit(bool ctrlDown, bool altDown, bool shiftDown, VirtualKey key)
+ {
+ return _ctrl == ctrlDown && _alt == altDown && _shift == shiftDown && _keys.Contains(key);
+ }
+
+ public bool ShouldExecute(IKeyboardCommand lastCommand)
+ {
+ DateTime now = DateTime.UtcNow;
+
+ if (lastCommand == this && now - _lastHitTimestamp < ConsecutiveHitsInterval)
+ {
+ _hits++;
+ }
+ else
+ {
+ _hits = 1;
+ }
+
+ _lastHitTimestamp = now;
+
+ if (_hits >= _requiredHits)
+ {
+ _hits = 0;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool ShouldHandleAfterExecution()
+ {
+ return _shouldHandle;
+ }
+
+ public bool ShouldSwallowAfterExecution()
+ {
+ return _shouldSwallow;
+ }
+
+ public void Execute(T args)
+ {
+ _action?.Invoke(args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/KeyboardCommandHandler.cs b/src/src/Notepads/Commands/KeyboardCommandHandler.cs
new file mode 100644
index 0000000..7ee4a2b
--- /dev/null
+++ b/src/src/Notepads/Commands/KeyboardCommandHandler.cs
@@ -0,0 +1,65 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ using System.Collections.Generic;
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Input;
+
+ public sealed class KeyboardCommandHandler : ICommandHandler
+ {
+ public readonly ICollection> Commands;
+
+ private IKeyboardCommand _lastCommand;
+
+ public KeyboardCommandHandler(ICollection> commands)
+ {
+ Commands = commands;
+ }
+
+ public CommandHandlerResult Handle(KeyRoutedEventArgs args)
+ {
+ var ctrlDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var shouldHandle = false;
+ var shouldSwallow = false;
+
+ foreach (var command in Commands)
+ {
+ if (command.Hit(ctrlDown, altDown, shiftDown, args.Key))
+ {
+ if (command.ShouldExecute(_lastCommand))
+ {
+ command.Execute(args);
+ }
+
+ if (command.ShouldSwallowAfterExecution())
+ {
+ shouldSwallow = true;
+ }
+
+ if (command.ShouldHandleAfterExecution())
+ {
+ shouldHandle = true;
+ }
+
+ _lastCommand = command;
+ break;
+ }
+ }
+
+ if (!shouldHandle)
+ {
+ _lastCommand = null;
+ }
+
+ return new CommandHandlerResult(shouldHandle, shouldSwallow);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/MouseCommand.cs b/src/src/Notepads/Commands/MouseCommand.cs
new file mode 100644
index 0000000..e461e72
--- /dev/null
+++ b/src/src/Notepads/Commands/MouseCommand.cs
@@ -0,0 +1,86 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ using System;
+
+ public sealed class MouseCommand : IMouseCommand
+ {
+ private readonly bool _ctrl;
+ private readonly bool _alt;
+ private readonly bool _shift;
+ private readonly bool _leftButton;
+ private readonly bool _middleButton;
+ private readonly bool _rightButton;
+ private readonly Action _action;
+ private readonly bool _shouldHandle;
+ private readonly bool _shouldSwallow;
+
+ public MouseCommand(
+ bool leftButtonDown,
+ bool middleButtonDown,
+ bool rightButtonDown,
+ Action action,
+ bool shouldHandle = true,
+ bool shouldSwallow = true) :
+ this(false, false, false, leftButtonDown, middleButtonDown, rightButtonDown, action, shouldHandle, shouldSwallow)
+ {
+ }
+
+ public MouseCommand(
+ bool ctrlDown,
+ bool altDown,
+ bool shiftDown,
+ bool leftButtonDown,
+ bool middleButtonDown,
+ bool rightButtonDown,
+ Action action,
+ bool shouldHandle = true,
+ bool shouldSwallow = true)
+ {
+ _ctrl = ctrlDown;
+ _alt = altDown;
+ _shift = shiftDown;
+ _leftButton = leftButtonDown;
+ _middleButton = middleButtonDown;
+ _rightButton = rightButtonDown;
+ _action = action;
+ _shouldHandle = shouldHandle;
+ _shouldSwallow = shouldSwallow;
+ }
+
+ public bool Hit(
+ bool ctrlDown,
+ bool altDown,
+ bool shiftDown,
+ bool leftButtonDown,
+ bool middleButtonDown,
+ bool rightButtonDown)
+ {
+ return _ctrl == ctrlDown &&
+ _alt == altDown &&
+ _shift == shiftDown &&
+ _leftButton == leftButtonDown &&
+ _middleButton == middleButtonDown &&
+ _rightButton == rightButtonDown;
+ }
+
+ public bool ShouldHandleAfterExecution()
+ {
+ return _shouldHandle;
+ }
+
+ public bool ShouldSwallowAfterExecution()
+ {
+ return _shouldSwallow;
+ }
+
+ public void Execute(T args)
+ {
+ _action?.Invoke(args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Commands/MouseCommandHandler.cs b/src/src/Notepads/Commands/MouseCommandHandler.cs
new file mode 100644
index 0000000..68813f5
--- /dev/null
+++ b/src/src/Notepads/Commands/MouseCommandHandler.cs
@@ -0,0 +1,64 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Commands
+{
+ using System.Collections.Generic;
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Input;
+
+ public sealed class MouseCommandHandler : ICommandHandler
+ {
+ public readonly ICollection> Commands;
+
+ private readonly UIElement _relativeTo;
+
+ public MouseCommandHandler(ICollection> commands, UIElement relativeTo)
+ {
+ Commands = commands;
+ _relativeTo = relativeTo;
+ }
+
+ public CommandHandlerResult Handle(PointerRoutedEventArgs args)
+ {
+ var ctrlDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var point = args.GetCurrentPoint(_relativeTo).Properties;
+ var shouldHandle = false;
+ var shouldSwallow = false;
+
+ foreach (var command in Commands)
+ {
+ if (command.Hit(
+ ctrlDown,
+ altDown,
+ shiftDown,
+ point.IsLeftButtonPressed,
+ point.IsMiddleButtonPressed,
+ point.IsRightButtonPressed))
+ {
+ command.Execute(args);
+
+ if (command.ShouldSwallowAfterExecution())
+ {
+ shouldSwallow = true;
+ }
+
+ if (command.ShouldHandleAfterExecution())
+ {
+ shouldHandle = true;
+ }
+
+ break;
+ }
+ }
+
+ return new CommandHandlerResult(shouldHandle, shouldSwallow);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/AppCloseSaveReminderDialog.cs b/src/src/Notepads/Controls/Dialog/AppCloseSaveReminderDialog.cs
new file mode 100644
index 0000000..e70d282
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/AppCloseSaveReminderDialog.cs
@@ -0,0 +1,29 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using System;
+ using Windows.UI;
+ using Windows.UI.Xaml;
+
+ public sealed class AppCloseSaveReminderDialog : NotepadsDialog
+ {
+ public AppCloseSaveReminderDialog(Action saveAndExitAction, Action discardAndExitAction, Action cancelAction)
+ {
+ Title = ResourceLoader.GetString("AppCloseSaveReminderDialog_Title");
+ HorizontalAlignment = HorizontalAlignment.Center;
+ Content = ResourceLoader.GetString("AppCloseSaveReminderDialog_Content");
+ PrimaryButtonText = ResourceLoader.GetString("AppCloseSaveReminderDialog_PrimaryButtonText");
+ SecondaryButtonText = ResourceLoader.GetString("AppCloseSaveReminderDialog_SecondaryButtonText");
+ CloseButtonText = ResourceLoader.GetString("AppCloseSaveReminderDialog_CloseButtonText");
+ PrimaryButtonStyle = GetButtonStyle(Color.FromArgb(255, 38, 114, 201));
+
+ PrimaryButtonClick += (dialog, eventArgs) => saveAndExitAction();
+ SecondaryButtonClick += (dialog, eventArgs) => discardAndExitAction();
+ CloseButtonClick += (dialog, eventArgs) => cancelAction();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/FileOpenErrorDialog.cs b/src/src/Notepads/Controls/Dialog/FileOpenErrorDialog.cs
new file mode 100644
index 0000000..64f7df9
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/FileOpenErrorDialog.cs
@@ -0,0 +1,17 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ public sealed class FileOpenErrorDialog : NotepadsDialog
+ {
+ public FileOpenErrorDialog(string filePath, string errorMsg)
+ {
+ Title = ResourceLoader.GetString("FileOpenErrorDialog_Title");
+ Content = string.IsNullOrEmpty(filePath) ? errorMsg : string.Format(ResourceLoader.GetString("FileOpenErrorDialog_Content"), filePath, errorMsg);
+ PrimaryButtonText = ResourceLoader.GetString("FileOpenErrorDialog_PrimaryButtonText");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/FileRenameDialog.cs b/src/src/Notepads/Controls/Dialog/FileRenameDialog.cs
new file mode 100644
index 0000000..627b10d
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/FileRenameDialog.cs
@@ -0,0 +1,153 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.System;
+ using Windows.UI;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+ using Notepads.Services;
+ using Notepads.Utilities;
+
+ public sealed class FileRenameDialog : NotepadsDialog
+ {
+ private readonly TextBox _fileNameTextBox;
+
+ private readonly TextBlock _errorMessageTextBlock;
+
+ private readonly Action _confirmedAction;
+
+ private readonly string _originalFilename;
+
+ private readonly bool _fileExists;
+
+ public FileRenameDialog(string filename, bool fileExists, Action confirmedAction)
+ {
+ _originalFilename = filename;
+ _fileExists = fileExists;
+ _confirmedAction = confirmedAction;
+
+ _fileNameTextBox = new TextBox
+ {
+ Style = (Style)Application.Current.Resources["TransparentTextBoxStyle"],
+ Text = filename,
+ IsSpellCheckEnabled = false,
+ AcceptsReturn = false,
+ SelectionStart = 0,
+ SelectionLength = filename.Contains(".") ? filename.LastIndexOf(".", StringComparison.Ordinal) : filename.Length,
+ Height = 35,
+ };
+
+ _errorMessageTextBlock = new TextBlock()
+ {
+ Visibility = Visibility.Collapsed,
+ Margin = new Thickness(4, 10, 4, 0),
+ FontSize = Math.Clamp(_fileNameTextBox.FontSize - 2, 1, Double.PositiveInfinity),
+ TextWrapping = TextWrapping.Wrap
+ };
+
+ var contentStack = new StackPanel();
+ contentStack.Children.Add(_fileNameTextBox);
+ contentStack.Children.Add(_errorMessageTextBlock);
+
+ Title = ResourceLoader.GetString("FileRenameDialog_Title");
+ Content = contentStack;
+ PrimaryButtonText = ResourceLoader.GetString("FileRenameDialog_PrimaryButtonText");
+ CloseButtonText = ResourceLoader.GetString("FileRenameDialog_CloseButtonText");
+ IsPrimaryButtonEnabled = false;
+
+ _fileNameTextBox.TextChanging += OnTextChanging;
+ _fileNameTextBox.KeyDown += OnKeyDown;
+
+ PrimaryButtonClick += (sender, args) => TryRename();
+
+ AnalyticsService.TrackEvent("FileRenameDialogOpened", new Dictionary()
+ {
+ { "FileExists", fileExists.ToString() },
+ });
+ }
+
+ private bool TryRename()
+ {
+ var newFileName = _fileNameTextBox.Text;
+
+ if (string.Compare(_originalFilename, newFileName, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return false;
+ }
+
+ if (!FileSystemUtility.IsFilenameValid(newFileName, out var error))
+ {
+ return false;
+ }
+
+ if (_fileExists && !FileExtensionProvider.IsFileExtensionSupported(FileTypeUtility.GetFileExtension(newFileName)))
+ {
+ return false;
+ }
+
+ _confirmedAction(newFileName.Trim());
+ return true;
+ }
+
+ private void OnKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Enter)
+ {
+ if (TryRename())
+ {
+ Hide();
+ }
+ }
+ }
+
+ private void OnTextChanging(TextBox sender, TextBoxTextChangingEventArgs args)
+ {
+ if (!args.IsContentChanging) return;
+
+ var newFilename = sender.Text;
+ var isFilenameValid = FileSystemUtility.IsFilenameValid(newFilename, out var error);
+ var nameChanged = string.Compare(_originalFilename, newFilename, StringComparison.OrdinalIgnoreCase) != 0;
+ var isExtensionSupported = false;
+ var fileExtension = FileTypeUtility.GetFileExtension(newFilename);
+
+ if (!_fileExists) // User can rename whatever they want for new file
+ {
+ isExtensionSupported = true;
+ }
+ else if (FileExtensionProvider.IsFileExtensionSupported(fileExtension))
+ {
+ // User can only rename an existing file if extension is supported by the app
+ // This is a Windows 10 UWP limitation
+ isExtensionSupported = true;
+ }
+
+ if (!isFilenameValid)
+ {
+ _errorMessageTextBlock.Foreground = new SolidColorBrush(Colors.Red);
+ _errorMessageTextBlock.Text = ResourceLoader.GetString($"InvalidFilenameError_{error}");
+ _errorMessageTextBlock.Visibility = Visibility.Visible;
+ }
+ else if (!isExtensionSupported)
+ {
+ _errorMessageTextBlock.Foreground = new SolidColorBrush(Colors.OrangeRed);
+ _errorMessageTextBlock.Text = string.IsNullOrEmpty(fileExtension)
+ ? string.Format(ResourceLoader.GetString("FileRenameError_EmptyFileExtension"))
+ : string.Format(ResourceLoader.GetString("FileRenameError_UnsupportedFileExtension"), fileExtension);
+ _errorMessageTextBlock.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ _errorMessageTextBlock.Visibility = Visibility.Collapsed;
+ }
+
+ IsPrimaryButtonEnabled = isFilenameValid && nameChanged && isExtensionSupported;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/FileSaveErrorDialog.cs b/src/src/Notepads/Controls/Dialog/FileSaveErrorDialog.cs
new file mode 100644
index 0000000..c5a7ff5
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/FileSaveErrorDialog.cs
@@ -0,0 +1,18 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ public sealed class FileSaveErrorDialog : NotepadsDialog
+ {
+ public FileSaveErrorDialog(string filePath, string errorMsg)
+ {
+ var content = string.IsNullOrEmpty(filePath) ? errorMsg : string.Format(ResourceLoader.GetString("FileSaveErrorDialog_Content"), filePath, errorMsg);
+ Title = ResourceLoader.GetString("FileSaveErrorDialog_Title");
+ Content = content;
+ PrimaryButtonText = ResourceLoader.GetString("FileSaveErrorDialog_PrimaryButtonText");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/NotepadsDialog.cs b/src/src/Notepads/Controls/Dialog/NotepadsDialog.cs
new file mode 100644
index 0000000..0a03a96
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/NotepadsDialog.cs
@@ -0,0 +1,50 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using Windows.ApplicationModel.Resources;
+ using Windows.UI;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+ using Notepads.Services;
+ using Microsoft.Toolkit.Uwp.Helpers;
+
+ public class NotepadsDialog : ContentDialog
+ {
+ public bool IsAborted = false;
+
+ private readonly SolidColorBrush _darkModeBackgroundBrush = new SolidColorBrush("#101010".ToColor());
+ private readonly SolidColorBrush _lightModeBackgroundBrush = new SolidColorBrush(Colors.White);
+
+ public NotepadsDialog()
+ {
+ RequestedTheme = ThemeSettingsService.ThemeMode;
+ Background = ThemeSettingsService.ThemeMode == ElementTheme.Dark
+ ? _darkModeBackgroundBrush
+ : _lightModeBackgroundBrush;
+
+ ActualThemeChanged += NotepadsDialog_ActualThemeChanged;
+ }
+
+ private void NotepadsDialog_ActualThemeChanged(FrameworkElement sender, object args)
+ {
+ Background = ActualTheme == ElementTheme.Dark
+ ? _darkModeBackgroundBrush
+ : _lightModeBackgroundBrush;
+ }
+
+ internal readonly ResourceLoader ResourceLoader = ResourceLoader.GetForCurrentView();
+
+ internal static Style GetButtonStyle(Color backgroundColor)
+ {
+ var buttonStyle = new Windows.UI.Xaml.Style(typeof(Button));
+ buttonStyle.Setters.Add(new Setter(Control.BackgroundProperty, backgroundColor));
+ buttonStyle.Setters.Add(new Setter(Control.ForegroundProperty, Colors.White));
+ return buttonStyle;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/RevertAllChangesConfirmationDialog.cs b/src/src/Notepads/Controls/Dialog/RevertAllChangesConfirmationDialog.cs
new file mode 100644
index 0000000..f5e199f
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/RevertAllChangesConfirmationDialog.cs
@@ -0,0 +1,21 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using System;
+
+ public sealed class RevertAllChangesConfirmationDialog : NotepadsDialog
+ {
+ public RevertAllChangesConfirmationDialog(string fileNameOrPath, Action confirmedAction)
+ {
+ Title = ResourceLoader.GetString("RevertAllChangesConfirmationDialog_Title");
+ Content = string.Format(ResourceLoader.GetString("RevertAllChangesConfirmationDialog_Content"), fileNameOrPath);
+ PrimaryButtonText = ResourceLoader.GetString("RevertAllChangesConfirmationDialog_PrimaryButtonText");
+ CloseButtonText = ResourceLoader.GetString("RevertAllChangesConfirmationDialog_CloseButtonText");
+ PrimaryButtonClick += (dialog, args) => { confirmedAction(); };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/SessionCorruptionErrorDialog.cs b/src/src/Notepads/Controls/Dialog/SessionCorruptionErrorDialog.cs
new file mode 100644
index 0000000..47d08ab
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/SessionCorruptionErrorDialog.cs
@@ -0,0 +1,23 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using System;
+ using Windows.UI;
+
+ public sealed class SessionCorruptionErrorDialog : NotepadsDialog
+ {
+ public SessionCorruptionErrorDialog(Action recoveryAction)
+ {
+ Title = ResourceLoader.GetString("SessionCorruptionErrorDialog_Title");
+ Content = ResourceLoader.GetString("SessionCorruptionErrorDialog_Content");
+ PrimaryButtonText = ResourceLoader.GetString("SessionCorruptionErrorDialog_PrimaryButtonText");
+ CloseButtonText = ResourceLoader.GetString("SessionCorruptionErrorDialog_CloseButtonText");
+ PrimaryButtonStyle = GetButtonStyle(Color.FromArgb(255, 255, 69, 0));
+ PrimaryButtonClick += (dialog, args) => { recoveryAction(); };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Dialog/SetCloseSaveReminderDialog.cs b/src/src/Notepads/Controls/Dialog/SetCloseSaveReminderDialog.cs
new file mode 100644
index 0000000..76219a8
--- /dev/null
+++ b/src/src/Notepads/Controls/Dialog/SetCloseSaveReminderDialog.cs
@@ -0,0 +1,24 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Dialog
+{
+ using System;
+
+ public sealed class SetCloseSaveReminderDialog : NotepadsDialog
+ {
+ public SetCloseSaveReminderDialog(string fileNameOrPath, Action saveAction, Action skipSavingAction)
+ {
+ Title = ResourceLoader.GetString("SetCloseSaveReminderDialog_Title");
+ Content = string.Format(ResourceLoader.GetString("SetCloseSaveReminderDialog_Content"), fileNameOrPath);
+ PrimaryButtonText = ResourceLoader.GetString("SetCloseSaveReminderDialog_PrimaryButtonText");
+ SecondaryButtonText = ResourceLoader.GetString("SetCloseSaveReminderDialog_SecondaryButtonText");
+ CloseButtonText = ResourceLoader.GetString("SetCloseSaveReminderDialog_CloseButtonText");
+
+ PrimaryButtonClick += (dialog, args) => { saveAction(); };
+ SecondaryButtonClick += (dialog, args) => { skipSavingAction(); };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/BrushFactory.cs b/src/src/Notepads/Controls/DiffViewer/BrushFactory.cs
new file mode 100644
index 0000000..d03525d
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/BrushFactory.cs
@@ -0,0 +1,27 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using System.Collections.Generic;
+ using Windows.UI;
+ using Windows.UI.Xaml.Media;
+
+ public sealed class BrushFactory
+ {
+ private readonly Dictionary _brushes = new Dictionary();
+
+ public SolidColorBrush GetOrCreateSolidColorBrush(Color color)
+ {
+ if (_brushes.TryGetValue(color, out var brush))
+ {
+ return brush;
+ }
+
+ _brushes[color] = new SolidColorBrush(color);
+ return _brushes[color];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/ISideBySideDiffViewer.cs b/src/src/Notepads/Controls/DiffViewer/ISideBySideDiffViewer.cs
new file mode 100644
index 0000000..27fc339
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/ISideBySideDiffViewer.cs
@@ -0,0 +1,14 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using Windows.UI.Xaml;
+
+ public interface ISideBySideDiffViewer
+ {
+ void RenderDiff(string left, string right, ElementTheme theme);
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffContext.cs b/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffContext.cs
new file mode 100644
index 0000000..11dff3c
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffContext.cs
@@ -0,0 +1,87 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using Windows.UI;
+ using Windows.UI.Xaml.Documents;
+
+ public class RichTextBlockDiffContext
+ {
+ private bool _hasPendingHighlighter;
+ private int _lastStart;
+ private int _lastEnd;
+ private Color _lastHighlightColor;
+
+ private readonly BrushFactory _brushFactory;
+ private readonly Dictionary _textHighlighters = new Dictionary();
+
+ public RichTextBlockDiffContext(BrushFactory brushFactory)
+ {
+ Blocks = new List();
+ _brushFactory = brushFactory;
+ }
+
+ public IList Blocks { get; set; }
+
+ public void QueuePendingHighlighter(TextRange textRange, Color backgroundColor)
+ {
+ _lastStart = textRange.StartIndex;
+ _lastEnd = _lastStart + textRange.Length;
+ _lastHighlightColor = backgroundColor;
+ _hasPendingHighlighter = true;
+ }
+
+ public void AddTextHighlighter(TextRange textRange, Color backgroundColor)
+ {
+ if (!_hasPendingHighlighter)
+ {
+ QueuePendingHighlighter(textRange, backgroundColor);
+ }
+ else
+ {
+ if (_lastEnd == textRange.StartIndex && _lastHighlightColor == backgroundColor)
+ {
+ _lastEnd += textRange.Length;
+ }
+ else
+ {
+ TextRange range = new TextRange() { StartIndex = _lastStart, Length = _lastEnd - _lastStart };
+ AddOrUpdateTextHighlighterInternal(_lastHighlightColor, range);
+ QueuePendingHighlighter(textRange, backgroundColor);
+ }
+ }
+ }
+
+ public IList GetTextHighlighters()
+ {
+ if (_hasPendingHighlighter)
+ {
+ TextRange range = new TextRange() { StartIndex = _lastStart, Length = _lastEnd - _lastStart };
+ AddOrUpdateTextHighlighterInternal(_lastHighlightColor, range);
+ _hasPendingHighlighter = false;
+ }
+ return _textHighlighters.Values.ToList();
+ }
+
+ private void AddOrUpdateTextHighlighterInternal(Color backgroundColor, TextRange range)
+ {
+ if (_textHighlighters.ContainsKey(backgroundColor))
+ {
+ _textHighlighters[backgroundColor].Ranges.Add(range);
+ }
+ else
+ {
+ _textHighlighters[backgroundColor] = new TextHighlighter()
+ {
+ Background = _brushFactory.GetOrCreateSolidColorBrush(backgroundColor),
+ Ranges = { range }
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffRenderer.cs b/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffRenderer.cs
new file mode 100644
index 0000000..a27e305
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/RichTextBlockDiffRenderer.cs
@@ -0,0 +1,163 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using DiffPlex;
+ using DiffPlex.DiffBuilder;
+ using DiffPlex.DiffBuilder.Model;
+ using Windows.UI;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Media;
+
+ public sealed class RichTextBlockDiffRenderer
+ {
+ //private const char ImaginaryLineCharacter = '\u202B';
+ private readonly SideBySideDiffBuilder differ;
+ private readonly object mutex = new object();
+ private bool inDiff;
+ private readonly BrushFactory _brushFactory;
+
+ public RichTextBlockDiffRenderer()
+ {
+ differ = new SideBySideDiffBuilder(new Differ());
+ _brushFactory = new BrushFactory();
+ }
+
+ private const char BreakingSpace = '-';
+ private Brush _defaultForeground;
+
+ public Tuple GenerateDiffViewData(string leftText, string rightText, Brush defaultForeground)
+ {
+ if (inDiff) return null;
+ lock (mutex)
+ {
+ if (inDiff) return null;
+ inDiff = true;
+ }
+
+ _defaultForeground = defaultForeground;
+
+ var diff = differ.BuildDiffModel(leftText, rightText, ignoreWhitespace: false);
+ var zippedDiffs = diff.OldText.Lines.Zip(diff.NewText.Lines, (oldLine, newLine) => new OldNew { Old = oldLine, New = newLine }).ToList();
+ var leftContext = RenderDiff(zippedDiffs, line => line.Old, piece => piece.Old);
+ var rightContext = RenderDiff(zippedDiffs, line => line.New, piece => piece.New);
+
+ inDiff = false;
+ return new Tuple(leftContext, rightContext);
+ }
+
+ private RichTextBlockDiffContext RenderDiff(System.Collections.Generic.List> lines, Func, DiffPiece> lineSelector, Func, DiffPiece> pieceSelector)
+ {
+ var context = new RichTextBlockDiffContext(_brushFactory);
+ int index = 0;
+ foreach (var line in lines)
+ {
+ var lineLength = Math.Max(line.Old.Text?.Length ?? 0, line.New.Text?.Length ?? 0);
+ var lineSubPieces = line.Old.SubPieces.Zip(line.New.SubPieces, (oldPiece, newPiece) => new OldNew { Old = oldPiece, New = newPiece, Length = Math.Max(oldPiece.Text?.Length ?? 0, newPiece.Text?.Length ?? 0) });
+
+ var oldNewLine = lineSelector(line);
+ switch (oldNewLine.Type)
+ {
+ case ChangeType.Unchanged:
+ AppendParagraph(context, oldNewLine.Text ?? string.Empty, ref index, null);
+ break;
+ case ChangeType.Imaginary:
+ AppendParagraph(context, new string(BreakingSpace, lineLength), ref index, Colors.Gray, Colors.LightCyan);
+ break;
+ case ChangeType.Inserted:
+ AppendParagraph(context, oldNewLine.Text ?? string.Empty, ref index, Colors.LightGreen);
+ break;
+ case ChangeType.Deleted:
+ AppendParagraph(context, oldNewLine.Text ?? string.Empty, ref index, Colors.OrangeRed);
+ break;
+ case ChangeType.Modified:
+ context.Blocks.Add(ConstructModifiedParagraph(pieceSelector, lineSubPieces, context, ref index));
+ break;
+ }
+ }
+ return context;
+ }
+
+ private Paragraph ConstructModifiedParagraph(Func, DiffPiece> pieceSelector, IEnumerable> lineSubPieces, RichTextBlockDiffContext context, ref int index)
+ {
+ var paragraph = new Paragraph()
+ {
+ LineStackingStrategy = LineStackingStrategy.BlockLineHeight,
+ Foreground = _defaultForeground,
+ };
+
+ paragraph.LineHeight = paragraph.FontSize + 6;
+
+ foreach (var subPiece in lineSubPieces)
+ {
+ var oldNewPiece = pieceSelector(subPiece);
+ switch (oldNewPiece.Type)
+ {
+ case ChangeType.Unchanged: paragraph.Inlines.Add(NewRun(context, oldNewPiece.Text ?? string.Empty, ref index, Colors.Yellow)); break;
+ case ChangeType.Imaginary: paragraph.Inlines.Add(NewRun(context, oldNewPiece.Text ?? string.Empty, ref index)); break;
+ case ChangeType.Inserted: paragraph.Inlines.Add(NewRun(context, oldNewPiece.Text ?? string.Empty, ref index, Colors.LightGreen)); break;
+ case ChangeType.Deleted: paragraph.Inlines.Add(NewRun(context, oldNewPiece.Text ?? string.Empty, ref index, Colors.OrangeRed)); break;
+ case ChangeType.Modified: paragraph.Inlines.Add(NewRun(context, oldNewPiece.Text ?? string.Empty, ref index, Colors.Yellow)); break;
+ }
+ paragraph.Inlines.Add(NewRun(context, new string(BreakingSpace, subPiece.Length - (oldNewPiece.Text ?? string.Empty).Length), ref index, Colors.Gray, Colors.LightCyan));
+ }
+
+ return paragraph;
+ }
+
+ private Inline NewRun(RichTextBlockDiffContext richTextBlockData, string text, ref int index, Color? background = null, Color? foreground = null)
+ {
+ var run = new Run
+ {
+ Text = text,
+ Foreground = foreground.HasValue
+ ? _brushFactory.GetOrCreateSolidColorBrush(foreground.Value)
+ : _defaultForeground
+ };
+
+ if (background != null)
+ {
+ richTextBlockData.AddTextHighlighter(new TextRange() { StartIndex = index, Length = text.Length }, background.Value);
+ }
+ index += text.Length;
+ return run;
+ }
+
+ private void AppendParagraph(RichTextBlockDiffContext richTextBlockData, string text, ref int index, Color? background = null, Color? foreground = null)
+ {
+ var paragraph = new Paragraph
+ {
+ LineStackingStrategy = LineStackingStrategy.BlockLineHeight,
+ Foreground = foreground.HasValue
+ ? _brushFactory.GetOrCreateSolidColorBrush(foreground.Value)
+ : _defaultForeground,
+ };
+ paragraph.LineHeight = paragraph.FontSize + 6;
+
+ var run = new Run { Text = text };
+ paragraph.Inlines.Add(run);
+
+ richTextBlockData.Blocks.Add(paragraph);
+
+ if (background != null)
+ {
+ richTextBlockData.AddTextHighlighter(new TextRange() { StartIndex = index, Length = text.Length }, background.Value);
+ }
+ index += text.Length;
+ }
+
+ private class OldNew
+ {
+ public T Old { get; set; }
+ public T New { get; set; }
+ public int Length { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/ScrollViewerSynchronizer.cs b/src/src/Notepads/Controls/DiffViewer/ScrollViewerSynchronizer.cs
new file mode 100644
index 0000000..0084da2
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/ScrollViewerSynchronizer.cs
@@ -0,0 +1,172 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Media;
+
+ public sealed class ScrollViewerSynchronizer : IDisposable
+ {
+ private readonly List _scrollViewers;
+ private readonly Dictionary _horizontalScrollBars = new Dictionary();
+ private readonly Dictionary _verticalScrollBars = new Dictionary();
+ private double _verticalScrollOffset = .0f;
+ private double _horizontalScrollOffset = .0f;
+
+ public ScrollViewerSynchronizer(List scrollViewers)
+ {
+ _scrollViewers = scrollViewers;
+ scrollViewers.ForEach(scrollViewer => scrollViewer.Loaded += ScrollViewerLoaded);
+ scrollViewers.ForEach(scrollViewer => scrollViewer.Unloaded += ScrollViewerUnloaded);
+ }
+
+ private void ScrollViewerLoaded(object sender, RoutedEventArgs e)
+ {
+ if (!(sender is ScrollViewer scrollViewer)) return;
+
+ scrollViewer.ChangeView(_horizontalScrollOffset, _verticalScrollOffset, null, true);
+
+ scrollViewer.ApplyTemplate();
+
+ var scrollViewerRoot = (FrameworkElement)VisualTreeHelper.GetChild(scrollViewer, 0);
+ var horizontalScrollBar = (ScrollBar)scrollViewerRoot.FindName("HorizontalScrollBar");
+ var verticalScrollBar = (ScrollBar)scrollViewerRoot.FindName("VerticalScrollBar");
+
+ if (horizontalScrollBar != null)
+ {
+ if (!_horizontalScrollBars.Keys.Contains(horizontalScrollBar))
+ {
+ _horizontalScrollBars.Add(horizontalScrollBar, scrollViewer);
+ }
+
+ horizontalScrollBar.Scroll += HorizontalScrollBar_Scroll;
+ horizontalScrollBar.ValueChanged += HorizontalScrollBar_ValueChanged;
+ }
+
+ if (verticalScrollBar != null)
+ {
+ if (!_verticalScrollBars.Keys.Contains(verticalScrollBar))
+ {
+ _verticalScrollBars.Add(verticalScrollBar, scrollViewer);
+ }
+
+ verticalScrollBar.Scroll += VerticalScrollBar_Scroll;
+ verticalScrollBar.ValueChanged += VerticalScrollBar_ValueChanged;
+ }
+ }
+
+ private void ScrollViewerUnloaded(object sender, RoutedEventArgs e)
+ {
+ if (!(sender is ScrollViewer scrollViewer)) return;
+
+ scrollViewer.ApplyTemplate();
+
+ var scrollViewerRoot = (FrameworkElement)VisualTreeHelper.GetChild(scrollViewer, 0);
+ var horizontalScrollBar = (ScrollBar)scrollViewerRoot.FindName("HorizontalScrollBar");
+ var verticalScrollBar = (ScrollBar)scrollViewerRoot.FindName("VerticalScrollBar");
+
+ if (horizontalScrollBar != null)
+ {
+ horizontalScrollBar.Scroll -= HorizontalScrollBar_Scroll;
+ horizontalScrollBar.ValueChanged -= HorizontalScrollBar_ValueChanged;
+
+ if (_horizontalScrollBars.Keys.Contains(horizontalScrollBar))
+ {
+ _horizontalScrollBars.Remove(horizontalScrollBar);
+ }
+ }
+
+ if (verticalScrollBar != null)
+ {
+ verticalScrollBar.Scroll -= VerticalScrollBar_Scroll;
+ verticalScrollBar.ValueChanged -= VerticalScrollBar_ValueChanged;
+
+ if (_verticalScrollBars.Keys.Contains(verticalScrollBar))
+ {
+ _verticalScrollBars.Remove(verticalScrollBar);
+ }
+ }
+ }
+
+ private void VerticalScrollBar_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
+ {
+ if (!(sender is ScrollBar changedScrollBar)) return;
+ var changedScrollViewer = _verticalScrollBars[changedScrollBar];
+ Scroll(changedScrollViewer);
+ }
+
+ private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
+ {
+ if (!(sender is ScrollBar changedScrollBar)) return;
+ var changedScrollViewer = _verticalScrollBars[changedScrollBar];
+ Scroll(changedScrollViewer);
+ }
+
+ private void HorizontalScrollBar_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
+ {
+ if (!(sender is ScrollBar changedScrollBar)) return;
+ var changedScrollViewer = _horizontalScrollBars[changedScrollBar];
+ Scroll(changedScrollViewer);
+ }
+
+ private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
+ {
+ if (!(sender is ScrollBar changedScrollBar)) return;
+ var changedScrollViewer = _horizontalScrollBars[changedScrollBar];
+ Scroll(changedScrollViewer);
+ }
+
+ private void Scroll(ScrollViewer changedScrollViewer)
+ {
+ _verticalScrollOffset = changedScrollViewer.VerticalOffset;
+ _horizontalScrollOffset = changedScrollViewer.HorizontalOffset;
+
+ foreach (var scrollViewer in _scrollViewers.Where(s => s != changedScrollViewer))
+ {
+ if (Math.Abs(scrollViewer.VerticalOffset - changedScrollViewer.VerticalOffset) > 0.01)
+ {
+ scrollViewer.ChangeView(null, changedScrollViewer.VerticalOffset, null, true);
+ }
+
+ if (Math.Abs(scrollViewer.HorizontalOffset - changedScrollViewer.HorizontalOffset) > 0.01)
+ {
+ scrollViewer.ChangeView(changedScrollViewer.HorizontalOffset, null, null, true);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ var horizontalScrollBars = new List(_horizontalScrollBars.Keys);
+ horizontalScrollBars.ForEach(horizontalScrollBar =>
+ {
+ horizontalScrollBar.Scroll -= HorizontalScrollBar_Scroll;
+ horizontalScrollBar.ValueChanged -= HorizontalScrollBar_ValueChanged;
+ });
+ horizontalScrollBars.Clear();
+
+ var verticalScrollBars = new List(_verticalScrollBars.Keys);
+ verticalScrollBars.ForEach(verticalScrollBar =>
+ {
+ verticalScrollBar.Scroll -= VerticalScrollBar_Scroll;
+ verticalScrollBar.ValueChanged -= VerticalScrollBar_ValueChanged;
+ });
+ verticalScrollBars.Clear();
+
+ _horizontalScrollBars.Clear();
+ _verticalScrollBars.Clear();
+
+ _scrollViewers?.ForEach(scrollViewer => scrollViewer.Loaded -= ScrollViewerLoaded);
+ _scrollViewers?.ForEach(scrollViewer => scrollViewer.Unloaded -= ScrollViewerUnloaded);
+ _scrollViewers?.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml b/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml
new file mode 100644
index 0000000..75cdd14
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml.cs b/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml.cs
new file mode 100644
index 0000000..22a6e8d
--- /dev/null
+++ b/src/src/Notepads/Controls/DiffViewer/SideBySideDiffViewer.xaml.cs
@@ -0,0 +1,364 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.DiffViewer
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Notepads.Commands;
+ using Notepads.Extensions;
+ using Notepads.Services;
+ using Windows.System;
+ using Windows.UI;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class SideBySideDiffViewer : UserControl, ISideBySideDiffViewer, IDisposable
+ {
+ public event EventHandler OnCloseEvent;
+
+ private readonly RichTextBlockDiffRenderer _diffRenderer;
+
+ private readonly ICommandHandler _keyboardCommandHandler;
+ private readonly ICommandHandler _mouseCommandHandler;
+
+ private CancellationTokenSource _cancellationTokenSource;
+
+ private readonly ScrollViewerSynchronizer _scrollSynchronizer;
+
+ public SideBySideDiffViewer()
+ {
+ InitializeComponent();
+
+ _scrollSynchronizer = new ScrollViewerSynchronizer(new List { LeftScroller, RightScroller });
+
+ _diffRenderer = new RichTextBlockDiffRenderer();
+
+ _keyboardCommandHandler = GetKeyboardCommandHandler();
+ _mouseCommandHandler = GetMouseCommandHandler();
+
+ LeftTextBlock.SelectionHighlightColor = new SolidColorBrush(ThemeSettingsService.AppAccentColor);
+ RightTextBlock.SelectionHighlightColor = new SolidColorBrush(ThemeSettingsService.AppAccentColor);
+
+ LeftTextBlockBorder.PointerWheelChanged += LeftTextBlockBorder_PointerWheelChanged;
+ RightTextBlockBorder.PointerWheelChanged += RightTextBlockBorder_PointerWheelChanged;
+
+ ThemeSettingsService.OnAccentColorChanged += ThemeSettingsService_OnAccentColorChanged;
+
+ DismissButton.Click += DismissButton_OnClick;
+ LayoutRoot.KeyDown += OnKeyDown;
+ KeyDown += OnKeyDown;
+ LeftTextBlock.KeyDown += OnKeyDown;
+ RightTextBlock.KeyDown += OnKeyDown;
+ Loaded += SideBySideDiffViewer_Loaded;
+ }
+
+ public void Dispose()
+ {
+ StopRenderingAndClearCache();
+
+ ThemeSettingsService.OnAccentColorChanged -= ThemeSettingsService_OnAccentColorChanged;
+
+ DismissButton.Click -= DismissButton_OnClick;
+ LayoutRoot.KeyDown -= OnKeyDown;
+ KeyDown -= OnKeyDown;
+ LeftTextBlock.KeyDown -= OnKeyDown;
+ RightTextBlock.KeyDown -= OnKeyDown;
+ Loaded -= SideBySideDiffViewer_Loaded;
+
+ LeftTextBlockBorder.PointerWheelChanged -= LeftTextBlockBorder_PointerWheelChanged;
+ RightTextBlockBorder.PointerWheelChanged -= RightTextBlockBorder_PointerWheelChanged;
+
+ _scrollSynchronizer.Dispose();
+ }
+
+ private void SideBySideDiffViewer_Loaded(object sender, RoutedEventArgs e)
+ {
+ Focus();
+ }
+
+ private async void ThemeSettingsService_OnAccentColorChanged(object sender, Color color)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ LeftTextBlock.SelectionHighlightColor = new SolidColorBrush(color);
+ RightTextBlock.SelectionHighlightColor = new SolidColorBrush(color);
+ });
+ }
+
+ private KeyboardCommandHandler GetKeyboardCommandHandler()
+ {
+ return new KeyboardCommandHandler(new List>
+ {
+ new KeyboardCommand(VirtualKey.Escape, (args) =>
+ {
+ DismissButton_OnClick(this, new RoutedEventArgs());
+ }),
+ new KeyboardCommand(false, true, false, VirtualKey.D, (args) =>
+ {
+ DismissButton_OnClick(this, new RoutedEventArgs());
+ }),
+ });
+ }
+
+ private ICommandHandler GetMouseCommandHandler()
+ {
+ return new MouseCommandHandler(new List>()
+ {
+ new MouseCommand(false, false, false, ChangeVerticalScrollingBasedOnMouseInput),
+ new MouseCommand(true, false, true, false, false, false, ChangeHorizontalScrollingBasedOnMouseInput),
+ new MouseCommand(false, false, true, false, false, false, ChangeHorizontalScrollingBasedOnMouseInput),
+ new MouseCommand(false, true, false, ChangeHorizontalScrollingBasedOnMouseInput)
+ }, this);
+ }
+
+ private void ChangeVerticalScrollingBasedOnMouseInput(PointerRoutedEventArgs args)
+ {
+ var mouseWheelDelta = args.GetCurrentPoint(this).Properties.MouseWheelDelta;
+ RightScroller.ChangeView(null, RightScroller.VerticalOffset + (-1 * mouseWheelDelta), null, false);
+ }
+
+ // Ctrl + Shift + Wheel -> horizontal scrolling
+ private void ChangeHorizontalScrollingBasedOnMouseInput(PointerRoutedEventArgs args)
+ {
+ var mouseWheelDelta = args.GetCurrentPoint(this).Properties.MouseWheelDelta;
+ RightScroller.ChangeView(RightScroller.HorizontalOffset + (-1 * mouseWheelDelta), null, null, false);
+ }
+
+ private void OnKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs args)
+ {
+ var result = _keyboardCommandHandler.Handle(args);
+ if (result.ShouldHandle)
+ {
+ args.Handled = true;
+ }
+ }
+
+ public void Focus()
+ {
+ RightTextBlock.Focus(FocusState.Programmatic);
+ }
+
+ public void StopRenderingAndClearCache()
+ {
+ if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
+ {
+ _cancellationTokenSource.Cancel();
+ }
+
+ LeftTextBlock.TextHighlighters.Clear();
+ LeftTextBlock.Blocks.Clear();
+ RightTextBlock.TextHighlighters.Clear();
+ RightTextBlock.Blocks.Clear();
+ }
+
+ public void RenderDiff(string left, string right, ElementTheme theme)
+ {
+ StopRenderingAndClearCache();
+
+ var foregroundBrush = (theme == ElementTheme.Dark)
+ ? new SolidColorBrush(Colors.White)
+ : new SolidColorBrush(Colors.Black);
+
+ var diffContext = _diffRenderer.GenerateDiffViewData(left, right, foregroundBrush);
+ var leftContext = diffContext.Item1;
+ var rightContext = diffContext.Item2;
+ var leftHighlighters = leftContext.GetTextHighlighters();
+ var rightHighlighters = rightContext.GetTextHighlighters();
+
+ CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
+
+ Task.Factory.StartNew(async () =>
+ {
+ var leftCount = leftContext.Blocks.Count;
+ var rightCount = rightContext.Blocks.Count;
+
+ var leftStartIndex = 0;
+ var rightStartIndex = 0;
+ var threshold = 1;
+
+ while (true)
+ {
+ Thread.Sleep(10);
+ if (leftStartIndex < leftCount)
+ {
+ var end = leftStartIndex + threshold;
+ if (end >= leftCount) end = leftCount;
+ var start = leftStartIndex;
+
+ await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ {
+ for (int x = start; x < end; x++)
+ {
+ if (cancellationTokenSource.IsCancellationRequested) return;
+ LeftTextBlock.Blocks.Add(leftContext.Blocks[x]);
+ }
+ });
+ }
+
+ if (rightStartIndex < rightCount)
+ {
+ var end = rightStartIndex + threshold;
+ if (end >= rightCount) end = rightCount;
+ var start = rightStartIndex;
+
+ await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ {
+ for (int x = start; x < end; x++)
+ {
+ if (cancellationTokenSource.IsCancellationRequested) return;
+ RightTextBlock.Blocks.Add(rightContext.Blocks[x]);
+ }
+ });
+ }
+
+ leftStartIndex += threshold;
+ rightStartIndex += threshold;
+ threshold *= 5;
+
+ if (leftStartIndex >= leftCount && rightStartIndex >= rightCount)
+ {
+ break;
+ }
+ }
+ }, cancellationTokenSource.Token);
+
+ Task.Factory.StartNew(async () =>
+ {
+ var leftCount = leftHighlighters.Count;
+ var rightCount = rightHighlighters.Count;
+
+ var leftStartIndex = 0;
+ var rightStartIndex = 0;
+ var threshold = 5;
+
+ while (true)
+ {
+ Thread.Sleep(10);
+ if (leftStartIndex < leftCount)
+ {
+ var end = leftStartIndex + threshold;
+ if (end >= leftCount) end = leftCount;
+ var start = leftStartIndex;
+
+ await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ {
+ for (int x = start; x < end; x++)
+ {
+ if (cancellationTokenSource.IsCancellationRequested) return;
+ LeftTextBlock.TextHighlighters.Add(leftHighlighters[x]);
+ }
+ });
+ }
+
+ if (rightStartIndex < rightCount)
+ {
+ var end = rightStartIndex + threshold;
+ if (end >= rightCount) end = rightCount;
+ var start = rightStartIndex;
+
+ await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ {
+ for (int x = start; x < end; x++)
+ {
+ if (cancellationTokenSource.IsCancellationRequested) return;
+ RightTextBlock.TextHighlighters.Add(rightHighlighters[x]);
+ }
+ });
+ }
+
+ leftStartIndex += threshold;
+ rightStartIndex += threshold;
+ threshold *= 5;
+
+ if (leftStartIndex >= leftCount && rightStartIndex >= rightCount)
+ {
+ break;
+ }
+ }
+ }, cancellationTokenSource.Token);
+
+ _cancellationTokenSource = cancellationTokenSource;
+
+ //Task.Factory.StartNew(async () =>
+ //{
+ // var count = rightCount > leftCount ? rightCount : leftCount;
+
+ // for (int i = 0; i < count; i++)
+ // {
+ // if (i < leftCount)
+ // {
+ // var j = i;
+ // await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ // {
+ // Thread.Sleep(20);
+ // LeftTextBlock.Blocks.Add(leftContext.Blocks[j]);
+ // });
+ // }
+ // if (i < rightCount)
+ // {
+ // var j = i;
+ // await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
+ // {
+ // Thread.Sleep(20);
+ // RightTextBlock.Blocks.Add(rightContext.Blocks[j]);
+ // });
+ // }
+ // }
+ //});
+
+ //Task.Factory.StartNew(async () =>
+ //{
+ // var leftCount = leftHighlighters.Count;
+ // var rightCount = rightHighlighters.Count;
+
+ // var count = rightCount > leftCount ? rightCount : leftCount;
+
+ // for (int i = 0; i < count; i++)
+ // {
+ // if (i < leftCount)
+ // {
+ // var j = i;
+ // await Dispatcher.RunAsync(CoreDispatcherPriority.Low,
+ // () => LeftTextBlock.TextHighlighters.Add(leftHighlighters[j]));
+ // }
+ // if (i < rightCount)
+ // {
+ // var j = i;
+ // await Dispatcher.RunAsync(CoreDispatcherPriority.Low,
+ // () => RightTextBlock.TextHighlighters.Add(rightHighlighters[j]));
+ // }
+ // }
+ //});
+ }
+
+ private void DismissButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ StopRenderingAndClearCache();
+ OnCloseEvent?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void LeftTextBlockBorder_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
+ {
+ _mouseCommandHandler.Handle(e);
+
+ // Always handle it so that left ScrollViewer won't pick up the event
+ e.Handled = true;
+ }
+
+ private void RightTextBlockBorder_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
+ {
+ _mouseCommandHandler.Handle(e);
+
+ // Always handle it so that right ScrollViewer won't pick up the event
+ e.Handled = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/FilePicker/FilePickerFactory.cs b/src/src/Notepads/Controls/FilePicker/FilePickerFactory.cs
new file mode 100644
index 0000000..a7d478e
--- /dev/null
+++ b/src/src/Notepads/Controls/FilePicker/FilePickerFactory.cs
@@ -0,0 +1,92 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.FilePicker
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Services;
+ using TextEditor;
+ using Utilities;
+ using Windows.Storage.Pickers;
+
+ public static class FilePickerFactory
+ {
+ private static IList _allSupportedExtensions;
+
+ private static IList AllSupportedExtensions
+ {
+ get
+ {
+ if (_allSupportedExtensions != null) return _allSupportedExtensions;
+
+ var allSupportedExtensions = FileExtensionProvider.AllSupportedFileExtensions.ToList();
+
+ foreach (var extension in FileExtensionProvider.TextDocumentFileExtensions)
+ {
+ allSupportedExtensions.Remove(extension);
+ }
+
+ allSupportedExtensions.Sort();
+ allSupportedExtensions.InsertRange(0, FileExtensionProvider.TextDocumentFileExtensions);
+
+ _allSupportedExtensions = allSupportedExtensions;
+
+ return _allSupportedExtensions;
+ }
+ }
+
+ public static FileOpenPicker GetFileOpenPicker()
+ {
+ var fileOpenPicker = new FileOpenPicker
+ {
+ SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.DocumentsLibrary
+ };
+
+ fileOpenPicker.FileTypeFilter.Add("*"); // All files
+
+ foreach (var extension in AllSupportedExtensions)
+ {
+ fileOpenPicker.FileTypeFilter.Add(extension);
+ }
+
+ return fileOpenPicker;
+ }
+
+ public static FileSavePicker GetFileSavePicker(ITextEditor textEditor)
+ {
+ FileSavePicker savePicker = new FileSavePicker
+ {
+ SuggestedStartLocation = PickerLocationId.DocumentsLibrary
+ };
+
+ var fileName = textEditor.EditingFileName ?? textEditor.FileNamePlaceholder;
+ var extension = FileTypeUtility.GetFileExtension(fileName).ToLower();
+
+ if (FileExtensionProvider.TextDocumentFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ {
+ savePicker.FileTypeChoices.Add("Text Documents", FileExtensionProvider.TextDocumentFileExtensions.ToList());
+ savePicker.FileTypeChoices.Add("All Supported Files", AllSupportedExtensions);
+ savePicker.FileTypeChoices.Add("Unknown", new List() { "." });
+ }
+ else if (AllSupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ {
+ savePicker.FileTypeChoices.Add("All Supported Files", AllSupportedExtensions);
+ savePicker.FileTypeChoices.Add("Text Documents", FileExtensionProvider.TextDocumentFileExtensions.ToList());
+ savePicker.FileTypeChoices.Add("Unknown", new List() { "." });
+ }
+ else
+ {
+ savePicker.FileTypeChoices.Add("Unknown", new List() { "." });
+ savePicker.FileTypeChoices.Add("Text Documents", FileExtensionProvider.TextDocumentFileExtensions.ToList());
+ savePicker.FileTypeChoices.Add("All Supported Files", AllSupportedExtensions);
+ }
+
+ savePicker.SuggestedFileName = fileName;
+ return savePicker;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml
new file mode 100644
index 0000000..b4cc714
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml.cs b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml.cs
new file mode 100644
index 0000000..18cf21b
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceControl.xaml.cs
@@ -0,0 +1,312 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.FindAndReplace
+{
+ using System;
+ using System.Collections.Generic;
+ using Notepads.Commands;
+ using Notepads.Extensions;
+ using Notepads.Services;
+ using Windows.System;
+ using Windows.UI;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class FindAndReplaceControl : UserControl
+ {
+ public event EventHandler OnDismissKeyDown;
+ public event EventHandler OnFindAndReplaceButtonClicked;
+ public event EventHandler OnToggleReplaceModeButtonClicked;
+ public event EventHandler OnFindReplaceControlKeyDown;
+
+ private readonly IList> _nativeKeyboardCommands = new List>
+ {
+ new KeyboardCommand(VirtualKey.F3, null),
+ new KeyboardCommand(false, false, true, VirtualKey.F3, null),
+ new KeyboardCommand(false, true, false, VirtualKey.E, null),
+ new KeyboardCommand(false, true, false, VirtualKey.R, null),
+ new KeyboardCommand(false, true, false, VirtualKey.W, null),
+ new KeyboardCommand(true, true, false, VirtualKey.Enter, null)
+ };
+
+ //When enter key is pressed focus is returned to control
+ //This variable is used to remove flicker in text selection
+ private bool _enterPressed = false;
+
+ private bool _shouldUpdateSearchString = true;
+
+ public FindAndReplaceControl()
+ {
+ InitializeComponent();
+
+ SetSelectionHighlightColor(ThemeSettingsService.AppAccentColor);
+ ThemeSettingsService.OnAccentColorChanged += ThemeSettingsService_OnAccentColorChanged;
+
+ Loaded += FindAndReplaceControl_Loaded;
+ }
+
+ public void Dispose()
+ {
+ Loaded -= FindAndReplaceControl_Loaded;
+ ThemeSettingsService.OnAccentColorChanged -= ThemeSettingsService_OnAccentColorChanged;
+ }
+
+ private SearchContext GetSearchContext()
+ {
+ return new SearchContext(FindBar.Text, MatchCaseToggle.IsChecked, MatchWholeWordToggle.IsChecked, UseRegexToggle.IsChecked);
+ }
+
+ private void FindAndReplaceControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ Focus(string.Empty, FindAndReplaceMode.FindOnly);
+ }
+
+ private async void ThemeSettingsService_OnAccentColorChanged(object sender, Color color)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ SetSelectionHighlightColor(color);
+ });
+ }
+
+ public double GetHeight(bool showReplaceBar)
+ {
+ if (showReplaceBar)
+ {
+ return FindBarPlaceHolder.Height + ReplaceBarPlaceHolder.Height;
+ }
+ else
+ {
+ return FindBarPlaceHolder.Height;
+ }
+ }
+
+ private void SetSelectionHighlightColor(Color color)
+ {
+ FindBar.SelectionHighlightColor = new SolidColorBrush(color);
+ FindBar.SelectionHighlightColorWhenNotFocused = new SolidColorBrush(color);
+ ReplaceBar.SelectionHighlightColor = new SolidColorBrush(color);
+ ReplaceBar.SelectionHighlightColorWhenNotFocused = new SolidColorBrush(color);
+ }
+
+ public void Focus(string searchString, FindAndReplaceMode mode)
+ {
+ if (_shouldUpdateSearchString && !string.IsNullOrEmpty(searchString)) FindBar.Text = searchString;
+
+ if (mode == FindAndReplaceMode.FindOnly)
+ {
+ FindBar.Focus(FocusState.Programmatic);
+ }
+ else
+ {
+ ReplaceBar.Focus(FocusState.Programmatic);
+ }
+
+ FindBar_OnTextChanged(null, null);
+ }
+
+ public void ShowReplaceBar(bool showReplaceBar)
+ {
+ if (showReplaceBar)
+ {
+ ToggleReplaceModeButtonGrid.SetValue(Grid.RowSpanProperty, 2);
+ ToggleReplaceModeButton.Content = new FontIcon { Glyph = "\xE011", FontSize = 12 };
+ ReplaceBarPlaceHolder.Visibility = Visibility.Visible;
+ if (!string.IsNullOrEmpty(FindBar.Text))
+ {
+ ReplaceButton.IsEnabled = true;
+ ReplaceAllButton.IsEnabled = true;
+ }
+ }
+ else
+ {
+ ToggleReplaceModeButtonGrid.SetValue(Grid.RowSpanProperty, 1);
+ ToggleReplaceModeButton.Content = new FontIcon { Glyph = "\xE00F", FontSize = 12 };
+ ReplaceBarPlaceHolder.Visibility = Visibility.Collapsed;
+ ReplaceButton.IsEnabled = false;
+ ReplaceAllButton.IsEnabled = false;
+ }
+ }
+
+ private void DismissButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ OnDismissKeyDown?.Invoke(sender, e);
+ }
+
+ private void FindBar_OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (!string.IsNullOrEmpty(FindBar.Text))
+ {
+ SearchForwardButton.IsEnabled = true;
+ SearchBackwardButton.IsEnabled = true;
+ if (ReplaceBarPlaceHolder.Visibility == Visibility.Visible)
+ {
+ ReplaceButton.IsEnabled = true;
+ ReplaceAllButton.IsEnabled = true;
+ }
+ }
+ else
+ {
+ SearchForwardButton.IsEnabled = false;
+ SearchBackwardButton.IsEnabled = false;
+ ReplaceButton.IsEnabled = false;
+ ReplaceAllButton.IsEnabled = false;
+ }
+ }
+
+ private void SearchForwardButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is MenuFlyout) return;
+
+ OnFindAndReplaceButtonClicked?.Invoke(sender, new FindAndReplaceEventArgs(GetSearchContext(), null, FindAndReplaceMode.FindOnly, SearchDirection.Next));
+ }
+
+ private void SearchBackwardButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is MenuFlyout) return;
+
+ OnFindAndReplaceButtonClicked?.Invoke(sender, new FindAndReplaceEventArgs(GetSearchContext(), null, FindAndReplaceMode.FindOnly, SearchDirection.Previous));
+ }
+
+ private void FindBar_OnKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var shiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+
+ if (e.Key == VirtualKey.Enter && !string.IsNullOrEmpty(FindBar.Text))
+ {
+ _enterPressed = true;
+ if (shiftDown)
+ {
+ SearchBackwardButton_OnClick(sender, e);
+ }
+ else
+ {
+ SearchForwardButton_OnClick(sender, e);
+ }
+ }
+ else if (e.Key == VirtualKey.Tab)
+ {
+ e.Handled = true;
+ if (ReplaceBarPlaceHolder.Visibility == Visibility.Visible) ReplaceBar.Focus(FocusState.Programmatic);
+ }
+ }
+
+ private void FindBar_GotFocus(object sender, RoutedEventArgs e)
+ {
+ _enterPressed = false;
+ _shouldUpdateSearchString = false;
+ FindBar.SelectionStart = 0;
+ FindBar.SelectionLength = FindBar.Text.Length;
+ }
+
+ private void FindBar_LostFocus(object sender, RoutedEventArgs e)
+ {
+ _shouldUpdateSearchString = true;
+ if (_enterPressed) return;
+ FindBar.SelectionStart = FindBar.Text.Length;
+ }
+
+ private void ReplaceBar_OnKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var shiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+
+ if (e.Key == VirtualKey.Enter && !string.IsNullOrEmpty(FindBar.Text))
+ {
+ _enterPressed = true;
+ if (shiftDown)
+ {
+ OnFindAndReplaceButtonClicked?.Invoke(sender,
+ new FindAndReplaceEventArgs(GetSearchContext(), ReplaceBar.Text, FindAndReplaceMode.Replace, SearchDirection.Previous));
+ }
+ else
+ {
+ ReplaceButton_OnClick(sender, e);
+ }
+ }
+ else if (e.Key == VirtualKey.Tab)
+ {
+ e.Handled = true;
+ if (ReplaceBarPlaceHolder.Visibility == Visibility.Visible) FindBar.Focus(FocusState.Programmatic);
+ }
+ }
+
+ private void ReplaceBar_GotFocus(object sender, RoutedEventArgs e)
+ {
+ _enterPressed = false;
+ _shouldUpdateSearchString = false;
+ ReplaceBar.SelectionStart = 0;
+ ReplaceBar.SelectionLength = ReplaceBar.Text.Length;
+ }
+
+ private void ReplaceBar_LostFocus(object sender, RoutedEventArgs e)
+ {
+ _shouldUpdateSearchString = true;
+ if (_enterPressed) return;
+ ReplaceBar.SelectionStart = ReplaceBar.Text.Length;
+ }
+
+ private void ReplaceBar_OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ }
+
+ private void ReplaceButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ OnFindAndReplaceButtonClicked?.Invoke(sender, new FindAndReplaceEventArgs(GetSearchContext(), ReplaceBar.Text, FindAndReplaceMode.Replace, SearchDirection.Next));
+ }
+
+ private void ReplaceAllButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ OnFindAndReplaceButtonClicked?.Invoke(sender, new FindAndReplaceEventArgs(GetSearchContext(), ReplaceBar.Text, FindAndReplaceMode.ReplaceAll));
+ }
+
+ private void OptionButtonFlyoutItem_OnClick(object sender, RoutedEventArgs e)
+ {
+ MatchWholeWordToggle.IsEnabled = !UseRegexToggle.IsChecked;
+ UseRegexToggle.IsEnabled = !MatchWholeWordToggle.IsChecked;
+
+ if (MatchCaseToggle.IsChecked || MatchWholeWordToggle.IsChecked || UseRegexToggle.IsChecked)
+ {
+ OptionButtonSelectionIndicator.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ OptionButtonSelectionIndicator.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void FindAndReplaceRootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var ctrlDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+
+ var isNativeKeyboardCommand = false;
+
+ foreach (var keyboardCommand in _nativeKeyboardCommands)
+ {
+ if (keyboardCommand.Hit(ctrlDown, altDown, shiftDown, e.Key))
+ {
+ isNativeKeyboardCommand = true;
+ break;
+ }
+ }
+
+ if (!isNativeKeyboardCommand && !e.Handled)
+ {
+ OnFindReplaceControlKeyDown?.Invoke(sender, e);
+ }
+ }
+
+ private void ToggleReplaceModeButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ _shouldUpdateSearchString = false;
+ OnToggleReplaceModeButtonClicked?.Invoke(sender, ReplaceBarPlaceHolder.Visibility == Visibility.Collapsed ? true : false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceEventArgs.cs b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceEventArgs.cs
new file mode 100644
index 0000000..010188f
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceEventArgs.cs
@@ -0,0 +1,45 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.FindAndReplace
+{
+ using System;
+
+ public enum FindAndReplaceMode
+ {
+ FindOnly,
+ Replace,
+ ReplaceAll
+ }
+
+ public enum SearchDirection
+ {
+ Previous,
+ Next
+ }
+
+ public sealed class FindAndReplaceEventArgs : EventArgs
+ {
+ public FindAndReplaceEventArgs(
+ SearchContext searchContext,
+ string replaceText,
+ FindAndReplaceMode findAndReplaceMode,
+ SearchDirection searchDirection = SearchDirection.Next)
+ {
+ SearchContext = searchContext;
+ ReplaceText = replaceText;
+ FindAndReplaceMode = findAndReplaceMode;
+ SearchDirection = searchDirection;
+ }
+
+ public SearchContext SearchContext { get; }
+
+ public string ReplaceText { get; }
+
+ public FindAndReplaceMode FindAndReplaceMode { get; }
+
+ public SearchDirection SearchDirection { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/FindAndReplace/FindAndReplacePlaceHolder.xaml b/src/src/Notepads/Controls/FindAndReplace/FindAndReplacePlaceHolder.xaml
new file mode 100644
index 0000000..1e28862
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/FindAndReplacePlaceHolder.xaml
@@ -0,0 +1,111 @@
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceTextBox.cs b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceTextBox.cs
new file mode 100644
index 0000000..0d7b930
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/FindAndReplaceTextBox.cs
@@ -0,0 +1,38 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.FindAndReplace
+{
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+
+ public sealed class FindAndReplaceTextBox : TextBox
+ {
+ protected override void OnKeyDown(KeyRoutedEventArgs e)
+ {
+ CoreVirtualKeyStates ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
+ CoreVirtualKeyStates alt = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu);
+ CoreVirtualKeyStates shift = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift);
+
+ // By default, TextBox toggles case when user hit "Shift + F3"
+ // This should be restricted
+ if (!ctrl.HasFlag(CoreVirtualKeyStates.Down) &&
+ !alt.HasFlag(CoreVirtualKeyStates.Down) &&
+ shift.HasFlag(CoreVirtualKeyStates.Down)
+ && e.Key == VirtualKey.F3)
+ {
+ return;
+ }
+
+ if (!e.Handled)
+ {
+ base.OnKeyDown(e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/FindAndReplace/SearchContext.cs b/src/src/Notepads/Controls/FindAndReplace/SearchContext.cs
new file mode 100644
index 0000000..a52ee6d
--- /dev/null
+++ b/src/src/Notepads/Controls/FindAndReplace/SearchContext.cs
@@ -0,0 +1,30 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.FindAndReplace
+{
+ public sealed class SearchContext
+ {
+ public SearchContext(
+ string searchText,
+ bool matchCase = false,
+ bool matchWholeWord = false,
+ bool useRegex = false)
+ {
+ SearchText = searchText;
+ MatchCase = matchCase;
+ MatchWholeWord = matchWholeWord;
+ UseRegex = useRegex;
+ }
+
+ public string SearchText { get; }
+
+ public bool MatchCase { get; }
+
+ public bool MatchWholeWord { get; }
+
+ public bool UseRegex { get; }
+ }
+}
diff --git a/src/src/Notepads/Controls/GoTo/GoToControl.xaml b/src/src/Notepads/Controls/GoTo/GoToControl.xaml
new file mode 100644
index 0000000..2967937
--- /dev/null
+++ b/src/src/Notepads/Controls/GoTo/GoToControl.xaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/GoTo/GoToControl.xaml.cs b/src/src/Notepads/Controls/GoTo/GoToControl.xaml.cs
new file mode 100644
index 0000000..4b61b95
--- /dev/null
+++ b/src/src/Notepads/Controls/GoTo/GoToControl.xaml.cs
@@ -0,0 +1,147 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.GoTo
+{
+ using Extensions;
+ using System;
+ using Services;
+ using Windows.ApplicationModel.Resources;
+ using Windows.System;
+ using Windows.UI;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class GoToControl : UserControl
+ {
+ public event EventHandler OnDismissKeyDown;
+ public event EventHandler OnGoToButtonClicked;
+
+ public event EventHandler OnGoToControlKeyDown;
+
+ private int _currentLine;
+ private int _maxLine;
+ private readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView();
+
+ public void SetLineData(int currentLine, int maxLine)
+ {
+ _currentLine = currentLine;
+ _maxLine = maxLine;
+ }
+
+ public GoToControl()
+ {
+ InitializeComponent();
+
+ SetSelectionHighlightColor(ThemeSettingsService.AppAccentColor);
+ ThemeSettingsService.OnAccentColorChanged += ThemeSettingsService_OnAccentColorChanged;
+
+ Loaded += GoToControl_Loaded;
+ }
+
+ public void Dispose()
+ {
+ Loaded -= GoToControl_Loaded;
+ ThemeSettingsService.OnAccentColorChanged -= ThemeSettingsService_OnAccentColorChanged;
+ }
+
+ private void GoToControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ Focus();
+ }
+
+ private async void ThemeSettingsService_OnAccentColorChanged(object sender, Color color)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ SetSelectionHighlightColor(color);
+ });
+ }
+
+ public double GetHeight()
+ {
+ return GoToRootGrid.Height;
+ }
+
+ private void SetSelectionHighlightColor(Color color)
+ {
+ GoToBar.SelectionHighlightColor = new SolidColorBrush(color);
+ GoToBar.SelectionHighlightColorWhenNotFocused = new SolidColorBrush(color);
+ }
+
+ public void Focus()
+ {
+ GoToBar.Text = _currentLine.ToString();
+ GoToBar.Focus(FocusState.Programmatic);
+ }
+
+ private void GoToBar_OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ SearchButton.Visibility = !string.IsNullOrEmpty(GoToBar.Text) ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void SearchButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is MenuFlyout) return;
+
+ OnGoToButtonClicked?.Invoke(sender, new GoToEventArgs(GoToBar.Text));
+ }
+
+ private void GoToBar_OnKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Enter && !string.IsNullOrEmpty(GoToBar.Text))
+ {
+ SearchButton_OnClick(sender, e);
+ }
+
+ if (e.Key == VirtualKey.Tab)
+ {
+ e.Handled = true;
+ }
+ }
+
+ private void GoToBar_GotFocus(object sender, RoutedEventArgs e)
+ {
+ GoToBar.SelectionStart = 0;
+ GoToBar.SelectionLength = GoToBar.Text.Length;
+ }
+
+ private void GoToBar_LostFocus(object sender, RoutedEventArgs e)
+ {
+ GoToBar.SelectionStart = GoToBar.Text.Length;
+ }
+
+ private void DismissButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ OnDismissKeyDown?.Invoke(sender, e);
+ }
+
+ private void GoToBar_BeforeTextChanging(TextBox sender, TextBoxBeforeTextChangingEventArgs args)
+ {
+ if (string.IsNullOrEmpty(args.NewText)) return;
+
+ if (!int.TryParse(args.NewText, out var line) || args.NewText.Contains(" "))
+ {
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("GoTo_NotificationMsg_InputError_InvalidInput"), 1500);
+ args.Cancel = true;
+ }
+ else if (line > _maxLine || line <= 0)
+ {
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("GoTo_NotificationMsg_InputError_ExceedInputLimit"), 1500);
+ args.Cancel = true;
+ }
+ }
+
+ private void GoToRootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (!e.Handled)
+ {
+ OnGoToControlKeyDown?.Invoke(sender, e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/GoTo/GoToEventArgs.cs b/src/src/Notepads/Controls/GoTo/GoToEventArgs.cs
new file mode 100644
index 0000000..c3fa13f
--- /dev/null
+++ b/src/src/Notepads/Controls/GoTo/GoToEventArgs.cs
@@ -0,0 +1,19 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.GoTo
+{
+ using System;
+
+ public sealed class GoToEventArgs : EventArgs
+ {
+ public GoToEventArgs(string searchLine)
+ {
+ SearchLine = searchLine;
+ }
+
+ public string SearchLine { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml b/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml
new file mode 100644
index 0000000..580d035
--- /dev/null
+++ b/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml.cs b/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml.cs
new file mode 100644
index 0000000..3dbf300
--- /dev/null
+++ b/src/src/Notepads/Controls/Markdown/MarkdownExtensionView.xaml.cs
@@ -0,0 +1,230 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Markdown
+{
+ using Controls;
+ using Extensions;
+ using Microsoft.Toolkit.Uwp.UI;
+ using Services;
+ using System;
+ using System.IO;
+ using System.Threading.Tasks;
+ using TextEditor;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class MarkdownExtensionView : UserControl, IContentPreviewExtension
+ {
+ private readonly ImageCache _imageCache = new ImageCache();
+
+ private bool _isExtensionEnabled;
+
+ public bool IsExtensionEnabled
+ {
+ get => _isExtensionEnabled;
+ set
+ {
+ _isExtensionEnabled = value;
+ if (_isExtensionEnabled)
+ {
+ UpdateFontSize();
+ UpdateTextWrapping();
+ UpdateText();
+ }
+ }
+ }
+
+ private TextEditorCore _editorCore;
+
+ public MarkdownExtensionView()
+ {
+ InitializeComponent();
+
+ MarkdownTextBlock.LinkClicked += MarkdownTextBlock_OnLinkClicked;
+ MarkdownTextBlock.ImageClicked += MarkdownTextBlock_OnLinkClicked;
+ MarkdownTextBlock.ImageResolving += MarkdownTextBlock_ImageResolving;
+
+ ThemeSettingsService.OnThemeChanged += OnThemeChanged;
+ }
+
+ private async void OnThemeChanged(object sender, ElementTheme theme)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ MarkdownTextBlock.RequestedTheme = theme;
+ });
+ }
+
+ public void Dispose()
+ {
+ IsExtensionEnabled = false;
+
+ MarkdownTextBlock.LinkClicked -= MarkdownTextBlock_OnLinkClicked;
+ MarkdownTextBlock.ImageClicked -= MarkdownTextBlock_OnLinkClicked;
+ MarkdownTextBlock.ImageResolving -= MarkdownTextBlock_ImageResolving;
+ MarkdownTextBlock.Text = string.Empty;
+
+ _editorCore.TextChanged -= OnTextChanged;
+ _editorCore.TextWrappingChanged -= OnTextWrappingChanged;
+ _editorCore.FontSizeChanged -= OnFontSizeChanged;
+
+ ThemeSettingsService.OnThemeChanged -= OnThemeChanged;
+
+ Task.Run(async () => { await _imageCache.ClearAsync(); });
+ }
+
+ private async void MarkdownTextBlock_ImageResolving(object sender, ImageResolvingEventArgs e)
+ {
+ var deferral = e.GetDeferral();
+
+ try
+ {
+ var imageUri = new Uri(e.Url);
+ if (Path.GetExtension(imageUri.AbsolutePath)?.ToLowerInvariant() == ".svg")
+ {
+ // SvgImageSource is not working properly when width and height are not set in uri
+ // I am disabling Svg parsing here.
+ // e.Image = await GetImageAsync(e.Url);
+ e.Handled = true;
+ }
+ else
+ {
+ e.Image = await GetImageAsync(e.Url);
+ e.Handled = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(MarkdownExtensionView)}] Failed to resolve Markdown image [{e.Url}]: {ex.Message}");
+ e.Handled = false;
+ }
+
+ deferral.Complete();
+ }
+
+ private async Task GetImageAsync(string url)
+ {
+ var imageUri = new Uri(url);
+
+ return await _imageCache.GetFromCacheAsync(imageUri);
+
+ //var feed = await Downloader.GetDataFeed(url);
+ //feed.Seek(0, SeekOrigin.Begin);
+
+ //using (InMemoryRandomAccessStream ms = new InMemoryRandomAccessStream())
+ //{
+ // using (DataWriter writer = new DataWriter(ms.GetOutputStreamAt(0)))
+ // {
+ // writer.WriteBytes(feed.ToArray());
+ // writer.StoreAsync().GetResults();
+ // }
+
+ // if (Path.GetExtension(imageUri.AbsolutePath)?.ToLowerInvariant() == ".svg")
+ // {
+ // var image = new SvgImageSource();
+ // await image.SetSourceAsync(ms);
+ // return image;
+ // }
+ // else
+ // {
+ // var image = new BitmapImage();
+ // await image.SetSourceAsync(ms);
+ // return image;
+ // }
+ //}
+ }
+
+ public void Bind(TextEditorCore editorCore)
+ {
+ if (_editorCore != null)
+ {
+ _editorCore.TextChanged -= OnTextChanged;
+ _editorCore.TextWrappingChanged -= OnTextWrappingChanged;
+ _editorCore.FontSizeChanged -= OnFontSizeChanged;
+ }
+
+ _editorCore = editorCore;
+ _editorCore.TextChanged += OnTextChanged;
+ _editorCore.TextWrappingChanged += OnTextWrappingChanged;
+ _editorCore.FontSizeChanged += OnFontSizeChanged;
+ }
+
+ private void OnTextChanged(object sender, RoutedEventArgs e)
+ {
+ if (IsExtensionEnabled)
+ {
+ UpdateText();
+ }
+ }
+
+ private void OnTextWrappingChanged(object sender, TextWrapping textWrapping)
+ {
+ if (IsExtensionEnabled)
+ {
+ UpdateTextWrapping();
+ }
+ }
+
+ private void OnFontSizeChanged(object sender, double fontSize)
+ {
+ if (IsExtensionEnabled)
+ {
+ UpdateFontSize();
+ }
+ }
+
+ private void UpdateFontSize()
+ {
+ if (_editorCore != null)
+ {
+ MarkdownTextBlock.FontSize = _editorCore.FontSize;
+ MarkdownTextBlock.Header1FontSize = MarkdownTextBlock.FontSize + 5;
+ MarkdownTextBlock.Header2FontSize = MarkdownTextBlock.FontSize + 5;
+ MarkdownTextBlock.Header3FontSize = MarkdownTextBlock.FontSize + 2;
+ MarkdownTextBlock.Header4FontSize = MarkdownTextBlock.FontSize + 2;
+ MarkdownTextBlock.Header5FontSize = MarkdownTextBlock.FontSize + 1;
+ MarkdownTextBlock.Header6FontSize = MarkdownTextBlock.FontSize + 1;
+ }
+ }
+
+ private void UpdateText()
+ {
+ if (_editorCore != null)
+ {
+ MarkdownTextBlock.ImageMaxWidth = ActualWidth;
+ MarkdownTextBlock.Text = _editorCore.GetText();
+ }
+ }
+
+ private void UpdateTextWrapping()
+ {
+ if (_editorCore != null)
+ {
+ MarkdownTextBlock.TextWrapping = _editorCore.TextWrapping;
+ MarkdownScrollViewer.HorizontalScrollBarVisibility = MarkdownTextBlock.TextWrapping == TextWrapping.Wrap ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Visible;
+ }
+ }
+
+ private async void MarkdownTextBlock_OnLinkClicked(object sender, LinkClickedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(e.Link))
+ {
+ return;
+ }
+
+ try
+ {
+ var uri = new Uri(e.Link);
+ await Windows.System.Launcher.LaunchUriAsync(uri);
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(MarkdownExtensionView)}] Failed to open Markdown Link: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml b/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml
new file mode 100644
index 0000000..bdee7a2
--- /dev/null
+++ b/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml.cs b/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml.cs
new file mode 100644
index 0000000..ed1fab4
--- /dev/null
+++ b/src/src/Notepads/Controls/Print/ContinuationPageFormat.xaml.cs
@@ -0,0 +1,50 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Print
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class ContinuationPageFormat : Page
+ {
+ public ContinuationPageFormat(RichTextBlockOverflow textLinkContainer, FontFamily textEditorFontFamily, double textEditorFontSize, string headerText, string footerText)
+ {
+ InitializeComponent();
+
+ if (!string.IsNullOrEmpty(headerText))
+ {
+ Header.Visibility = Visibility.Visible;
+ Header.Margin = new Thickness(0, 0, 0, textEditorFontSize + 6);
+ HeaderTextBlock.Text = headerText;
+ HeaderTextBlock.FontFamily = textEditorFontFamily;
+ HeaderTextBlock.FontSize = textEditorFontSize + 4;
+ }
+ else
+ {
+ Header.Visibility = Visibility.Collapsed;
+ HeaderTextBlock.FontFamily = textEditorFontFamily;
+ HeaderTextBlock.FontSize = textEditorFontSize + 4;
+ }
+
+ if (!string.IsNullOrEmpty(footerText))
+ {
+ Footer.Visibility = Visibility.Visible;
+ FooterTextBlock.Text = footerText;
+ FooterTextBlock.FontFamily = textEditorFontFamily;
+ FooterTextBlock.FontSize = textEditorFontSize;
+ }
+ else
+ {
+ Footer.Visibility = Visibility.Collapsed;
+ FooterTextBlock.FontFamily = textEditorFontFamily;
+ FooterTextBlock.FontSize = textEditorFontSize;
+ }
+
+ textLinkContainer.OverflowContentTarget = ContinuationPageLinkedContainer;
+ }
+ }
+}
diff --git a/src/src/Notepads/Controls/Print/PrintArgs.cs b/src/src/Notepads/Controls/Print/PrintArgs.cs
new file mode 100644
index 0000000..4f3949b
--- /dev/null
+++ b/src/src/Notepads/Controls/Print/PrintArgs.cs
@@ -0,0 +1,499 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Print
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using Windows.ApplicationModel.Core;
+ using Windows.ApplicationModel.Resources;
+ using Windows.Graphics.Printing;
+ using Windows.Graphics.Printing.OptionDetails;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Printing;
+ using Windows.UI.Xaml.Media;
+ using Services;
+ using TextEditor;
+
+ public static class PrintArgs
+ {
+ ///
+ /// The text that appears at the top of every page
+ ///
+ private static string _headerText = string.Empty;
+
+ ///
+ /// The text that appears at the bottom of every page
+ ///
+ private static string _footerText = string.Empty;
+
+ ///
+ /// The percent of app's margin width, content is set at 85% (0.85) of the area's width
+ ///
+ private static double _applicationContentMarginLeft = 0.075;
+
+ ///
+ /// The percent of app's margin height, content is set at 94% (0.94) of tha area's height
+ ///
+ private static double _applicationContentMarginTop = 0.03;
+
+ ///
+ /// PrintDocument is used to prepare the pages for printing.
+ /// Prepare the pages to print in the handlers for the Paginate, GetPreviewPage, and AddPages events.
+ ///
+ private static PrintDocument _printDocument;
+
+ ///
+ /// Marker interface for document source
+ ///
+ private static IPrintDocumentSource _printDocumentSource;
+
+ ///
+ /// A list of UIElements used to store the print preview pages. This gives easy access
+ /// to any desired preview page.
+ ///
+ private static List _printPreviewPages;
+
+ ///
+ /// First page in the printing-content series
+ /// From this "virtual sized" paged content is split(text is flowing) to "printing pages"
+ ///
+ private static List _firstPage;
+
+ ///
+ /// A reference back to the source page used to access XAML elements on the source page
+ ///
+ private static Page _sourcePage;
+
+ ///
+ /// A hidden canvas used to hold pages we wish to print
+ ///
+ private static Canvas PrintCanvas => _sourcePage.FindName("PrintCanvas") as Canvas;
+
+ private static readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView();
+
+ ///
+ /// Method that will generate print content for the scenario
+ /// It will create the first page from which content will flow
+ ///
+ /// Array of Notepads ITextEditors to print
+ public static void PreparePrintContent(ITextEditor[] textEditors)
+ {
+ // Clear the cache of preview pages
+ _printPreviewPages.Clear();
+
+ // Clear cache of first pages of each editor
+ _firstPage.Clear();
+
+ // Clear the print canvas of preview pages
+ PrintCanvas.Children.Clear();
+
+ foreach (var textEditor in textEditors)
+ {
+ var textDocument = textEditor.GetText();
+ if (!string.IsNullOrEmpty(textDocument))
+ {
+ var page = new PrintPageFormat(textDocument,
+ new FontFamily(AppSettingsService.EditorFontFamily),
+ AppSettingsService.EditorFontSize,
+ _headerText,
+ _footerText);
+
+ _firstPage.Add(page);
+
+ // Add the (newly created) page to the print canvas which is part of the visual tree and force it to go
+ // through layout so that the linked containers correctly distribute the content inside them.
+ PrintCanvas.Children.Add(page);
+ PrintCanvas.InvalidateMeasure();
+ PrintCanvas.UpdateLayout();
+ }
+ }
+ }
+
+ ///
+ /// This function registers the app for printing with Windows and sets up the necessary event handlers for the print process.
+ ///
+ public static void RegisterForPrinting(Page sourcePage)
+ {
+ _sourcePage = sourcePage;
+ _printPreviewPages = new List();
+ _firstPage = new List();
+
+ _printDocument = new PrintDocument();
+ _printDocumentSource = _printDocument.DocumentSource;
+ _printDocument.Paginate += CreatePrintPreviewPages;
+ _printDocument.GetPreviewPage += GetPrintPreviewPage;
+ _printDocument.AddPages += AddPrintPages;
+
+ PrintManager printMan = PrintManager.GetForCurrentView();
+ printMan.PrintTaskRequested += PrintTaskRequested;
+ }
+
+ ///
+ /// This function un-registers the app for printing with Windows.
+ ///
+ public static void UnregisterForPrinting()
+ {
+ if (_printDocument == null)
+ {
+ return;
+ }
+
+ _printDocument.Paginate -= CreatePrintPreviewPages;
+ _printDocument.GetPreviewPage -= GetPrintPreviewPage;
+ _printDocument.AddPages -= AddPrintPages;
+
+ // Remove the handler for printing initialization.
+ PrintManager printMan = PrintManager.GetForCurrentView();
+ printMan.PrintTaskRequested -= PrintTaskRequested;
+
+ PrintCanvas.Children.Clear();
+ }
+
+ ///
+ /// This is the event handler for PrintDocument.Paginate. It creates print preview pages for the app.
+ ///
+ /// PrintDocument
+ /// Paginate Event Arguments
+ private static void CreatePrintPreviewPages(object sender, PaginateEventArgs e)
+ {
+ lock (_printPreviewPages)
+ {
+ // Clear the cache of preview pages
+ _printPreviewPages.Clear();
+
+ // Clear the print canvas of preview pages
+ PrintCanvas.Children.Clear();
+
+ // This variable keeps track of the last RichTextBlockOverflow element that was added to a page which will be printed
+ RichTextBlockOverflow lastRTBOOnPage;
+
+ // Get the PrintTaskOptions
+ PrintTaskOptions printingOptions = e.PrintTaskOptions;
+
+ // Get the page description to determine how big the page is
+ PrintPageDescription pageDescription = printingOptions.GetPageDescription(0);
+
+ var count = 0;
+ do
+ {
+ // We know there is at least one page to be printed. passing null as the first parameter to
+ // AddOnePrintPreviewPage tells the function to add the first page.
+ lastRTBOOnPage = AddOnePrintPreviewPage(null, pageDescription, count);
+
+ // We know there are more pages to be added as long as the last RichTextBoxOverflow added to a print preview
+ // page has extra content
+ while (lastRTBOOnPage.HasOverflowContent && lastRTBOOnPage.Visibility == Visibility.Visible)
+ {
+ lastRTBOOnPage = AddOnePrintPreviewPage(lastRTBOOnPage, pageDescription, count);
+ }
+
+ count += 1;
+ } while (count < _firstPage.Count);
+
+ PrintDocument printDoc = (PrintDocument)sender;
+
+ // Report the number of preview pages created
+ printDoc.SetPreviewPageCount(_printPreviewPages.Count, PreviewPageCountType.Intermediate);
+ }
+ }
+
+ ///
+ /// This function creates and adds one print preview page to the internal cache of print preview
+ /// pages stored in _printPreviewPages.
+ ///
+ /// Last RichTextBlockOverflow element added in the current content
+ /// Printer's page description
+ private static RichTextBlockOverflow AddOnePrintPreviewPage(RichTextBlockOverflow lastRTBOAdded, PrintPageDescription printPageDescription, int count)
+ {
+ // XAML element that is used to represent to "printing page"
+ FrameworkElement page;
+
+ // The link container for text overflowing in this page
+ RichTextBlockOverflow textLink;
+
+ // Check if this is the first page ( no previous RichTextBlockOverflow)
+ if (lastRTBOAdded == null)
+ {
+ // If this is the first page add the specific scenario content
+ page = _firstPage[count];
+
+ // Hide header and footer if not provided
+ StackPanel header = (StackPanel)page.FindName("Header");
+ if (!string.IsNullOrEmpty(_headerText))
+ {
+ header.Visibility = Visibility.Visible;
+ TextBlock headerTextBlock = (TextBlock)page.FindName("HeaderTextBlock");
+ headerTextBlock.Text = _headerText;
+ }
+ else
+ {
+ header.Visibility = Visibility.Collapsed;
+ }
+
+ StackPanel footer = (StackPanel)page.FindName("Footer");
+ if (!string.IsNullOrEmpty(_footerText))
+ {
+ footer.Visibility = Visibility.Visible;
+ TextBlock footerTextBlock = (TextBlock)page.FindName("FooterTextBlock");
+ footerTextBlock.Text = _footerText;
+ }
+ else
+ {
+ footer.Visibility = Visibility.Collapsed;
+ }
+ }
+ else
+ {
+ // Flow content (text) from previous pages
+ page = new ContinuationPageFormat(lastRTBOAdded,
+ new FontFamily(AppSettingsService.EditorFontFamily),
+ AppSettingsService.EditorFontSize,
+ _headerText,
+ _footerText);
+ }
+
+ // Set "paper" width
+ page.Width = printPageDescription.PageSize.Width;
+ page.Height = printPageDescription.PageSize.Height;
+
+ Grid printableArea = (Grid)page.FindName("PrintableArea");
+
+ // Get the margins size
+ // If the ImageableRect is smaller than the app provided margins use the ImageableRect
+ double marginWidth = Math.Max(printPageDescription.PageSize.Width - printPageDescription.ImageableRect.Width, printPageDescription.PageSize.Width * _applicationContentMarginLeft * 2);
+ double marginHeight = Math.Max(printPageDescription.PageSize.Height - printPageDescription.ImageableRect.Height, printPageDescription.PageSize.Height * _applicationContentMarginTop * 2);
+
+ // Set-up "printable area" on the "paper"
+ printableArea.Width = _firstPage[count].Width - marginWidth;
+ printableArea.Height = _firstPage[count].Height - marginHeight;
+
+ // Add the (newley created) page to the print canvas which is part of the visual tree and force it to go
+ // through layout so that the linked containers correctly distribute the content inside them.
+ PrintCanvas.Children.Add(page);
+ PrintCanvas.InvalidateMeasure();
+ PrintCanvas.UpdateLayout();
+
+ // Find the last text container and see if the content is overflowing
+ textLink = (RichTextBlockOverflow)page.FindName("ContinuationPageLinkedContainer");
+
+ // Check if this is the last page
+ if (!textLink.HasOverflowContent && textLink.Visibility == Visibility.Visible)
+ {
+ PrintCanvas.UpdateLayout();
+ }
+
+ // Add the page to the page preview collection
+ _printPreviewPages.Add(page);
+
+ return textLink;
+ }
+
+ ///
+ /// This is the event handler for PrintDocument.GetPrintPreviewPage. It provides a specific print preview page,
+ /// in the form of an UIElement, to an instance of PrintDocument. PrintDocument subsequently converts the UIElement
+ /// into a page that the Windows print system can deal with.
+ ///
+ /// PrintDocument
+ /// Arguments containing the preview requested page
+ private static void GetPrintPreviewPage(object sender, GetPreviewPageEventArgs e)
+ {
+ PrintDocument printDoc = (PrintDocument)sender;
+ printDoc.SetPreviewPage(e.PageNumber, _printPreviewPages[e.PageNumber - 1]);
+ }
+
+ ///
+ /// This is the event handler for PrintDocument.AddPages. It provides all pages to be printed, in the form of
+ /// UIElements, to an instance of PrintDocument. PrintDocument subsequently converts the UIElements
+ /// into a pages that the Windows print system can deal with.
+ ///
+ /// PrintDocument
+ /// Add page event arguments containing a print task options reference
+ private static void AddPrintPages(object sender, AddPagesEventArgs e)
+ {
+ // Loop over all of the preview pages and add each one to add each page to be printied
+ for (int i = 0; i < _printPreviewPages.Count; i++)
+ {
+ // We should have all pages ready at this point...
+ _printDocument.AddPage(_printPreviewPages[i]);
+ }
+
+ PrintDocument printDoc = (PrintDocument)sender;
+
+ // Indicate that all of the print pages have been provided
+ printDoc.AddPagesComplete();
+ }
+
+ ///
+ /// This is the event handler for PrintManager.PrintTaskRequested.
+ ///
+ /// PrintManager
+ /// PrintTaskRequestedEventArgs
+ private static void PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs e)
+ {
+ PrintTask printTask = null;
+ printTask = e.Request.CreatePrintTask("Notepads", sourceRequestedArgs =>
+ {
+ var deferral = sourceRequestedArgs.GetDeferral();
+ PrintTaskOptionDetails printDetailedOptions = PrintTaskOptionDetails.GetFromPrintTaskOptions(printTask.Options);
+ IList displayedOptions = printTask.Options.DisplayedOptions;
+
+ // Choose the printer options to be shown.
+ // The order in which the options are appended determines the order in which they appear in the UI
+ displayedOptions.Clear();
+ displayedOptions.Add(StandardPrintTaskOptions.Orientation);
+ displayedOptions.Add(StandardPrintTaskOptions.Copies);
+ displayedOptions.Add(StandardPrintTaskOptions.MediaSize);
+ displayedOptions.Add(StandardPrintTaskOptions.InputBin);
+
+ // Add Margin setting in % options
+ PrintCustomTextOptionDetails leftMargin = printDetailedOptions.CreateTextOption("LeftMargin", _resourceLoader.GetString("Print_LeftMarginEntry_Title"));
+ PrintCustomTextOptionDetails topMargin = printDetailedOptions.CreateTextOption("TopMargin", _resourceLoader.GetString("Print_TopMarginEntry_Title"));
+ leftMargin.Description = topMargin.Description = _resourceLoader.GetString("Print_MarginEntry_Description");
+ leftMargin.TrySetValue(Math.Round(100 * _applicationContentMarginLeft, 1).ToString());
+ topMargin.TrySetValue(Math.Round(100 * _applicationContentMarginTop, 1).ToString());
+ displayedOptions.Add("LeftMargin");
+ displayedOptions.Add("TopMargin");
+
+ // Add Header and Footer text options
+ PrintCustomTextOptionDetails headerText = printDetailedOptions.CreateTextOption("HeaderText", _resourceLoader.GetString("Print_HeaderEntry_Title"));
+ PrintCustomTextOptionDetails footerText = printDetailedOptions.CreateTextOption("FooterText", _resourceLoader.GetString("Print_FooterEntry_Title"));
+ headerText.TrySetValue(_headerText);
+ footerText.TrySetValue(_footerText);
+ displayedOptions.Add("HeaderText");
+ displayedOptions.Add("FooterText");
+
+ displayedOptions.Add(StandardPrintTaskOptions.CustomPageRanges);
+ displayedOptions.Add(StandardPrintTaskOptions.Duplex);
+ displayedOptions.Add(StandardPrintTaskOptions.Collation);
+ //displayedOptions.Add(StandardPrintTaskOptions.NUp);
+ displayedOptions.Add(StandardPrintTaskOptions.MediaType);
+ displayedOptions.Add(StandardPrintTaskOptions.Bordering);
+ //displayedOptions.Add(StandardPrintTaskOptions.ColorMode);
+ displayedOptions.Add(StandardPrintTaskOptions.PrintQuality);
+ displayedOptions.Add(StandardPrintTaskOptions.HolePunch);
+ displayedOptions.Add(StandardPrintTaskOptions.Staple);
+
+ // Preset the default value of the printer option
+ printTask.Options.MediaSize = PrintMediaSize.Default;
+
+ printDetailedOptions.OptionChanged += PrintDetailedOptions_OptionChanged;
+
+ // Print Task event handler is invoked when the print job is completed.
+ printTask.Completed += async (s, args) =>
+ {
+ // Notify the user when the print operation fails.
+ if (args.Completion == PrintTaskCompletion.Failed)
+ {
+ await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
+ {
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("Print_NotificationMsg_PrintFailed"), 1500);
+ });
+ }
+ };
+
+ sourceRequestedArgs.SetSource(_printDocumentSource);
+
+ deferral.Complete();
+ });
+ }
+
+ ///
+ /// This is the event handler for PrintManager option changed.
+ ///
+ /// PrintTaskOptionDetails
+ /// PrintTaskOptionChangedEventArgs
+ private static async void PrintDetailedOptions_OptionChanged(PrintTaskOptionDetails sender, PrintTaskOptionChangedEventArgs args)
+ {
+ bool invalidatePreview = false;
+
+ string optionId = args.OptionId as string;
+ if (string.IsNullOrEmpty(optionId))
+ {
+ return;
+ }
+
+ if (optionId == "HeaderText")
+ {
+ PrintCustomTextOptionDetails headerText = (PrintCustomTextOptionDetails)sender.Options["HeaderText"];
+ _headerText = headerText.Value.ToString();
+ invalidatePreview = true;
+ }
+
+ if (optionId == "FooterText")
+ {
+ PrintCustomTextOptionDetails footerText = (PrintCustomTextOptionDetails)sender.Options["FooterText"];
+ _footerText = footerText.Value.ToString();
+ invalidatePreview = true;
+ }
+
+ if (optionId == "LeftMargin")
+ {
+ PrintCustomTextOptionDetails leftMargin = (PrintCustomTextOptionDetails)sender.Options["LeftMargin"];
+ var leftMarginValueConverterArg = double.TryParse(leftMargin.Value.ToString(), out var leftMarginValue);
+ if (leftMarginValue > 50 || leftMarginValue < 0 || !leftMarginValueConverterArg)
+ {
+ leftMargin.ErrorText = _resourceLoader.GetString("Print_ErrorMsg_ValueOutOfRange");
+ return;
+ }
+ else if (Math.Round(leftMarginValue, 1) != leftMarginValue)
+ {
+ leftMargin.ErrorText = _resourceLoader.GetString("Print_ErrorMsg_DecimalOutOfRange");
+ return;
+ }
+ leftMargin.ErrorText = string.Empty;
+ _applicationContentMarginLeft = (Math.Round(leftMarginValue / 100, 3));
+ invalidatePreview = true;
+ }
+
+ if (optionId == "TopMargin")
+ {
+ PrintCustomTextOptionDetails topMargin = (PrintCustomTextOptionDetails)sender.Options["TopMargin"];
+ var topMarginValueConverterArg = double.TryParse(topMargin.Value.ToString(), out var topMarginValue);
+ if (topMarginValue > 50 || topMarginValue < 0 || !topMarginValueConverterArg)
+ {
+ topMargin.ErrorText = _resourceLoader.GetString("Print_ErrorMsg_ValueOutOfRange");
+ return;
+ }
+ else if (Math.Round(topMarginValue, 1) != topMarginValue)
+ {
+ topMargin.ErrorText = _resourceLoader.GetString("Print_ErrorMsg_DecimalOutOfRange");
+ return;
+ }
+ topMargin.ErrorText = string.Empty;
+ _applicationContentMarginTop = (Math.Round(topMarginValue / 100, 3));
+ invalidatePreview = true;
+ }
+
+ if (invalidatePreview)
+ {
+ await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
+ {
+ _printDocument.InvalidatePreview();
+ });
+ }
+ }
+
+ ///
+ /// This method will show PrintManager UI and its options
+ /// Any printing error will be handled by this method
+ ///
+ public static async Task ShowPrintUIAsync()
+ {
+ // Catch and print out any errors reported
+ try
+ {
+ await PrintManager.ShowPrintUIAsync();
+ }
+ catch (Exception e)
+ {
+ NotificationCenter.Instance.PostNotification(
+ _resourceLoader.GetString("Print_NotificationMsg_PrintError") + " " + e.Message + ", hr=" + e.HResult, 1500);
+ }
+ }
+ }
+}
diff --git a/src/src/Notepads/Controls/Print/PrintPageFormat.xaml b/src/src/Notepads/Controls/Print/PrintPageFormat.xaml
new file mode 100644
index 0000000..b5e81a4
--- /dev/null
+++ b/src/src/Notepads/Controls/Print/PrintPageFormat.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/Print/PrintPageFormat.xaml.cs b/src/src/Notepads/Controls/Print/PrintPageFormat.xaml.cs
new file mode 100644
index 0000000..b17fe80
--- /dev/null
+++ b/src/src/Notepads/Controls/Print/PrintPageFormat.xaml.cs
@@ -0,0 +1,57 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.Print
+{
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Documents;
+ using Windows.UI.Xaml.Media;
+
+ public sealed partial class PrintPageFormat : Page
+ {
+ public RichTextBlock TextContentBlock { get; set; }
+
+ public PrintPageFormat(string textEditorText, FontFamily textEditorFontFamily, double textEditorFontSize, string headerText, string footerText)
+ {
+ InitializeComponent();
+
+ TextContent.FontFamily = textEditorFontFamily;
+ TextContent.FontSize = textEditorFontSize;
+
+ if (!string.IsNullOrEmpty(headerText))
+ {
+ Header.Visibility = Visibility.Visible;
+ Header.Margin = new Thickness(0, 0, 0, textEditorFontSize + 6);
+ HeaderTextBlock.Text = headerText;
+ HeaderTextBlock.FontFamily = textEditorFontFamily;
+ HeaderTextBlock.FontSize = textEditorFontSize + 4;
+ }
+ else
+ {
+ Header.Visibility = Visibility.Collapsed;
+ HeaderTextBlock.FontFamily = textEditorFontFamily;
+ HeaderTextBlock.FontSize = textEditorFontSize + 4;
+ }
+
+ if (!string.IsNullOrEmpty(footerText))
+ {
+ Footer.Visibility = Visibility.Visible;
+ FooterTextBlock.Text = footerText;
+ FooterTextBlock.FontFamily = textEditorFontFamily;
+ FooterTextBlock.FontSize = textEditorFontSize;
+ }
+ else
+ {
+ Footer.Visibility = Visibility.Collapsed;
+ FooterTextBlock.FontFamily = textEditorFontFamily;
+ FooterTextBlock.FontSize = textEditorFontSize;
+ }
+
+ var run = new Run { Text = textEditorText };
+ TextEditorContent.Inlines.Add(run);
+ }
+ }
+}
diff --git a/src/src/Notepads/Controls/TextEditor/ITextEditor.cs b/src/src/Notepads/Controls/TextEditor/ITextEditor.cs
new file mode 100644
index 0000000..b8dc4cc
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/ITextEditor.cs
@@ -0,0 +1,144 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Text;
+ using System.Threading.Tasks;
+ using Notepads.Models;
+ using Notepads.Utilities;
+ using Windows.Storage;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Input;
+
+ public interface ITextEditor
+ {
+ event RoutedEventHandler Loaded;
+ event RoutedEventHandler Unloaded;
+
+ event KeyEventHandler KeyDown;
+ event EventHandler ModeChanged;
+ event EventHandler ModificationStateChanged;
+ event EventHandler FileModificationStateChanged;
+ event EventHandler LineEndingChanged;
+ event EventHandler EncodingChanged;
+ event EventHandler SelectionChanged;
+ event EventHandler FontZoomFactorChanged;
+ event EventHandler TextChanging;
+ event EventHandler ChangeReverted;
+ event EventHandler FileSaved;
+ event EventHandler FileReloaded;
+ event EventHandler FileRenamed;
+
+ Guid Id { get; set; }
+
+ FileType FileType { get; }
+
+ TextFile LastSavedSnapshot { get; }
+
+ LineEnding? RequestedLineEnding { get; }
+
+ Encoding RequestedEncoding { get; }
+
+ string FileNamePlaceholder { get; set; }
+
+ string EditingFileName { get; }
+
+ string EditingFilePath { get; }
+
+ StorageFile EditingFile { get; }
+
+ bool IsModified { get; }
+
+ FileModificationState FileModificationState { get; }
+
+ TextEditorMode Mode { get; }
+
+ bool DisplayLineNumbers { get; set; }
+
+ bool DisplayLineHighlighter { get; set; }
+
+ void Init(TextFile textFile,
+ StorageFile file,
+ bool resetLastSavedSnapshot = true,
+ bool clearUndoQueue = true,
+ bool isModified = false,
+ bool resetText = true);
+
+ Task RenameAsync(string newFileName);
+
+ string GetText();
+
+ void StartCheckingFileStatusPeriodically();
+
+ void StopCheckingFileStatus();
+
+ TextEditorStateMetaData GetTextEditorStateMetaData();
+
+ void ResetEditorState(TextEditorStateMetaData metadata, string newText = null);
+
+ Task ReloadFromEditingFileAsync(Encoding encoding = null);
+
+ LineEnding GetLineEnding();
+
+ Encoding GetEncoding();
+
+ void CopyTextToWindowsClipboard(TextControlCopyingToClipboardEventArgs args);
+
+ void RevertAllChanges();
+
+ bool TryChangeEncoding(Encoding encoding);
+
+ bool TryChangeLineEnding(LineEnding lineEnding);
+
+ void ShowHideContentPreview();
+
+ void OpenSideBySideDiffViewer();
+
+ void CloseSideBySideDiffViewer();
+
+ ///
+ /// Returns 1-based indexing values
+ ///
+ void GetLineColumnSelection(
+ out int startLineIndex,
+ out int endLineIndex,
+ out int startColumnIndex,
+ out int endColumnIndex,
+ out int selectedCount,
+ out int lineCount);
+
+ double GetFontZoomFactor();
+
+ void SetFontZoomFactor(double fontZoomFactor);
+
+ bool IsEditorEnabled();
+
+ Task SaveContentToFileAndUpdateEditorStateAsync(StorageFile file);
+
+ string GetContentForSharing();
+
+ void TypeText(string text);
+
+ void Focus();
+
+ bool NoChangesSinceLastSaved(bool compareTextOnly = false);
+
+ void ShowFindAndReplaceControl(bool showReplaceBar);
+
+ void HideFindAndReplaceControl();
+
+ void ShowGoToControl();
+
+ void HideGoToControl();
+
+ void Dispose();
+
+ FlyoutBase GetContextFlyout();
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditor.xaml b/src/src/Notepads/Controls/TextEditor/TextEditor.xaml
new file mode 100644
index 0000000..d4cdad7
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditor.xaml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs b/src/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs
new file mode 100644
index 0000000..dd30c6a
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs
@@ -0,0 +1,1130 @@
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Notepads.Commands;
+ using Notepads.Controls.FindAndReplace;
+ using Notepads.Controls.GoTo;
+ using Notepads.Extensions;
+ using Notepads.Models;
+ using Notepads.Services;
+ using Notepads.Utilities;
+ using Windows.ApplicationModel.DataTransfer;
+ using Windows.ApplicationModel.Resources;
+ using Windows.Storage;
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Input;
+
+ public enum TextEditorMode
+ {
+ Editing = 0,
+ DiffPreview
+ }
+
+ public enum FileModificationState
+ {
+ Untouched,
+ Modified,
+ RenamedMovedOrDeleted
+ }
+
+ public sealed partial class TextEditor : ITextEditor, IDisposable
+ {
+ public new event RoutedEventHandler Loaded;
+ public new event RoutedEventHandler Unloaded;
+ public new event KeyEventHandler KeyDown;
+ public event EventHandler ModeChanged;
+ public event EventHandler ModificationStateChanged;
+ public event EventHandler FileModificationStateChanged;
+ public event EventHandler LineEndingChanged;
+ public event EventHandler EncodingChanged;
+ public event EventHandler TextChanging;
+ public event EventHandler ChangeReverted;
+ public event EventHandler SelectionChanged;
+ public event EventHandler FontZoomFactorChanged;
+ public event EventHandler FileSaved;
+ public event EventHandler FileReloaded;
+ public event EventHandler FileRenamed;
+
+ public Guid Id { get; set; }
+
+ public INotepadsExtensionProvider ExtensionProvider;
+
+ public string FileNamePlaceholder { get; set; } = string.Empty;
+
+ public FileType FileType { get; private set; }
+
+ public TextFile LastSavedSnapshot { get; private set; }
+
+ public LineEnding? RequestedLineEnding { get; private set; }
+
+ public Encoding RequestedEncoding { get; private set; }
+
+ public string EditingFileName { get; private set; }
+
+ public string EditingFilePath { get; private set; }
+
+ private StorageFile _editingFile;
+
+ public StorageFile EditingFile
+ {
+ get => _editingFile;
+ private set
+ {
+ _editingFile = value;
+ UpdateDocumentInfo();
+ }
+ }
+
+ private void UpdateDocumentInfo()
+ {
+ if (EditingFile == null)
+ {
+ EditingFileName = null;
+ EditingFilePath = null;
+ FileType = FileTypeUtility.GetFileTypeByFileName(FileNamePlaceholder);
+ }
+ else
+ {
+ EditingFileName = EditingFile.Name;
+ EditingFilePath = EditingFile.Path;
+ FileType = FileTypeUtility.GetFileTypeByFileName(EditingFile.Name);
+ }
+
+ // Hide content preview if current file type is not supported for previewing
+ if (!FileTypeUtility.IsPreviewSupported(FileType))
+ {
+ if (SplitPanel != null && SplitPanel.Visibility == Visibility.Visible)
+ {
+ ShowHideContentPreview();
+ }
+ }
+ }
+
+ private bool _isModified;
+
+ public bool IsModified
+ {
+ get => _isModified;
+ private set
+ {
+ if (_isModified != value)
+ {
+ _isModified = value;
+ ModificationStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public FileModificationState FileModificationState
+ {
+ get => _fileModificationState;
+ private set
+ {
+ if (_fileModificationState != value)
+ {
+ _fileModificationState = value;
+ FileModificationStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private bool _loaded;
+
+ private FileModificationState _fileModificationState;
+
+ private bool _isContentPreviewPanelOpened;
+
+ private readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView();
+
+ private CancellationTokenSource _fileStatusCheckerCancellationTokenSource;
+
+ private readonly int _fileStatusCheckerPollingRateInSec = 6;
+
+ private readonly double _fileStatusCheckerDelayInSec = 0.3;
+
+ private readonly SemaphoreSlim _fileStatusSemaphoreSlim = new SemaphoreSlim(1, 1);
+
+ private TextEditorMode _mode = TextEditorMode.Editing;
+
+ private readonly ICommandHandler _keyboardCommandHandler;
+
+ private IContentPreviewExtension _contentPreviewExtension;
+
+ private SearchContext _lastSearchContext = new SearchContext(string.Empty);
+
+ public TextEditorMode Mode
+ {
+ get => _mode;
+ private set
+ {
+ if (_mode != value)
+ {
+ _mode = value;
+ ModeChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool DisplayLineNumbers
+ {
+ get => TextEditorCore.DisplayLineNumbers;
+ set => TextEditorCore.DisplayLineNumbers = value;
+ }
+
+ public bool DisplayLineHighlighter
+ {
+ get => TextEditorCore.DisplayLineHighlighter;
+ set => TextEditorCore.DisplayLineHighlighter = value;
+ }
+
+ public TextEditor()
+ {
+ InitializeComponent();
+
+ TextEditorCore.TextChanging += TextEditorCore_OnTextChanging;
+ TextEditorCore.SelectionChanged += TextEditorCore_OnSelectionChanged;
+ TextEditorCore.KeyDown += TextEditorCore_OnKeyDown;
+ TextEditorCore.CopyTextToWindowsClipboardRequested += TextEditorCore_CopyTextToWindowsClipboardRequested;
+ TextEditorCore.CutSelectedTextToWindowsClipboardRequested += TextEditorCore_CutSelectedTextToWindowsClipboardRequested;
+ TextEditorCore.ContextFlyout = new TextEditorContextFlyout(this, TextEditorCore);
+
+ // Init shortcuts
+ _keyboardCommandHandler = GetKeyboardCommandHandler();
+
+ ThemeSettingsService.OnThemeChanged += ThemeSettingsService_OnThemeChanged;
+
+ base.Loaded += TextEditor_Loaded;
+ base.Unloaded += TextEditor_Unloaded;
+ base.KeyDown += TextEditor_KeyDown;
+
+ TextEditorCore.FontZoomFactorChanged += TextEditorCore_OnFontZoomFactorChanged;
+ }
+
+ private void TextEditor_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ KeyDown?.Invoke(this, e);
+ }
+
+ // Unhook events and clear state
+ public void Dispose()
+ {
+ StopCheckingFileStatus();
+
+ TextEditorCore.TextChanging -= TextEditorCore_OnTextChanging;
+ TextEditorCore.SelectionChanged -= TextEditorCore_OnSelectionChanged;
+ TextEditorCore.KeyDown -= TextEditorCore_OnKeyDown;
+ TextEditorCore.CopyTextToWindowsClipboardRequested -= TextEditorCore_CopyTextToWindowsClipboardRequested;
+ TextEditorCore.CutSelectedTextToWindowsClipboardRequested -= TextEditorCore_CutSelectedTextToWindowsClipboardRequested;
+
+ if (TextEditorCore.ContextFlyout is TextEditorContextFlyout contextFlyout)
+ {
+ contextFlyout.Dispose();
+ }
+
+ ThemeSettingsService.OnThemeChanged -= ThemeSettingsService_OnThemeChanged;
+
+ Unloaded?.Invoke(this, new RoutedEventArgs());
+
+ base.Loaded -= TextEditor_Loaded;
+ base.Unloaded -= TextEditor_Unloaded;
+ base.KeyDown -= TextEditor_KeyDown;
+
+ TextEditorCore.FontZoomFactorChanged -= TextEditorCore_OnFontZoomFactorChanged;
+
+ _contentPreviewExtension?.Dispose();
+
+ if (SplitPanel != null)
+ {
+ SplitPanel.KeyDown -= SplitPanel_OnKeyDown;
+ UnloadObject(SplitPanel);
+ }
+
+ if (SideBySideDiffViewer != null)
+ {
+ SideBySideDiffViewer.OnCloseEvent -= SideBySideDiffViewer_OnCloseEvent;
+ SideBySideDiffViewer.Dispose();
+ UnloadObject(SideBySideDiffViewer);
+ }
+
+ if (FindAndReplacePlaceholder != null && FindAndReplacePlaceholder.Content is FindAndReplaceControl findAndReplaceControl)
+ {
+ findAndReplaceControl.Dispose();
+ UnloadObject(FindAndReplacePlaceholder);
+ }
+
+ if (GoToPlaceholder != null && GoToPlaceholder.Content is GoToControl goToControl)
+ {
+ goToControl.Dispose();
+ UnloadObject(GoToPlaceholder);
+ }
+
+ if (GridSplitter != null)
+ {
+ UnloadObject(GridSplitter);
+ }
+
+ _fileStatusSemaphoreSlim.Dispose();
+ TextEditorCore.Dispose();
+ }
+
+ private async void ThemeSettingsService_OnThemeChanged(object sender, ElementTheme theme)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ if (Mode == TextEditorMode.DiffPreview)
+ {
+ SideBySideDiffViewer.RenderDiff(LastSavedSnapshot.Content, TextEditorCore.GetText(), theme);
+ Task.Factory.StartNew(async () =>
+ {
+ await Dispatcher.CallOnUIThreadAsync(() => { SideBySideDiffViewer.Focus(); });
+ });
+ }
+ });
+ }
+
+ public async Task RenameAsync(string newFileName)
+ {
+ if (EditingFile == null)
+ {
+ FileNamePlaceholder = newFileName;
+ }
+ else
+ {
+ await EditingFile.RenameAsync(newFileName);
+ }
+
+ UpdateDocumentInfo();
+
+ FileRenamed?.Invoke(this, EventArgs.Empty);
+ }
+
+ public string GetText()
+ {
+ return TextEditorCore.GetText();
+ }
+
+ // Make sure this method is thread safe
+ public TextEditorStateMetaData GetTextEditorStateMetaData()
+ {
+ TextEditorCore.GetScrollViewerPosition(out var horizontalOffset, out var verticalOffset);
+ TextEditorCore.GetTextSelectionPosition(out var textSelectionStartPosition, out var textSelectionEndPosition);
+
+ var metaData = new TextEditorStateMetaData
+ {
+ FileNamePlaceholder = FileNamePlaceholder,
+ LastSavedEncoding = EncodingUtility.GetEncodingName(LastSavedSnapshot.Encoding),
+ LastSavedLineEnding = LineEndingUtility.GetLineEndingName(LastSavedSnapshot.LineEnding),
+ DateModifiedFileTime = LastSavedSnapshot.DateModifiedFileTime,
+ HasEditingFile = EditingFile != null,
+ IsModified = IsModified,
+ SelectionStartPosition = textSelectionStartPosition,
+ SelectionEndPosition = textSelectionEndPosition,
+ WrapWord = TextEditorCore.TextWrapping == TextWrapping.Wrap ||
+ TextEditorCore.TextWrapping == TextWrapping.WrapWholeWords,
+ ScrollViewerHorizontalOffset = horizontalOffset,
+ ScrollViewerVerticalOffset = verticalOffset,
+ FontZoomFactor = TextEditorCore.GetFontZoomFactor() / 100,
+ IsContentPreviewPanelOpened = _isContentPreviewPanelOpened,
+ IsInDiffPreviewMode = (Mode == TextEditorMode.DiffPreview)
+ };
+
+ if (RequestedEncoding != null)
+ {
+ metaData.RequestedEncoding = EncodingUtility.GetEncodingName(RequestedEncoding);
+ }
+
+ if (RequestedLineEnding != null)
+ {
+ metaData.RequestedLineEnding = LineEndingUtility.GetLineEndingName(RequestedLineEnding.Value);
+ }
+
+ return metaData;
+ }
+
+ private void TextEditor_Loaded(object sender, RoutedEventArgs e)
+ {
+ Loaded?.Invoke(this, e);
+
+ StartCheckingFileStatusPeriodically();
+
+ // Insert "Legacy Windows Notepad" style date and time if document starts with ".LOG"
+ TextEditorCore.TryInsertNewLogEntry();
+ }
+
+ private void TextEditor_Unloaded(object sender, RoutedEventArgs e)
+ {
+ Unloaded?.Invoke(this, e);
+ StopCheckingFileStatus();
+ }
+
+ public async void StartCheckingFileStatusPeriodically()
+ {
+ if (EditingFile == null) return;
+ StopCheckingFileStatus();
+ var cancellationTokenSource = new CancellationTokenSource();
+ var cancellationToken = cancellationTokenSource.Token;
+ _fileStatusCheckerCancellationTokenSource = cancellationTokenSource;
+
+ try
+ {
+ await Task.Run(async () =>
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(_fileStatusCheckerDelayInSec), cancellationToken);
+ LoggingService.LogInfo($"[{nameof(TextEditor)}] Checking file status for \"{EditingFile.Path}\".", consoleOnly: true);
+ await CheckAndUpdateFileStatusAsync(cancellationToken);
+ await Task.Delay(TimeSpan.FromSeconds(_fileStatusCheckerPollingRateInSec), cancellationToken);
+ }
+ }, cancellationToken);
+ }
+ catch (TaskCanceledException)
+ {
+ // ignore
+ }
+ catch (ObjectDisposedException)
+ {
+ // ignore
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(TextEditor)}] Failed to check status for file [{EditingFile?.Path}]: {ex.Message}");
+ }
+ }
+
+ public void StopCheckingFileStatus()
+ {
+ if (_fileStatusCheckerCancellationTokenSource?.IsCancellationRequested == false)
+ {
+ _fileStatusCheckerCancellationTokenSource.Cancel();
+ }
+ }
+
+ private async Task CheckAndUpdateFileStatusAsync(CancellationToken cancellationToken)
+ {
+ if (EditingFile == null) return;
+
+ await _fileStatusSemaphoreSlim.WaitAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ _fileStatusSemaphoreSlim.Release();
+ return;
+ }
+
+ FileModificationState? newState = null;
+
+ if (!await FileSystemUtility.FileExistsAsync(EditingFile))
+ {
+ newState = FileModificationState.RenamedMovedOrDeleted;
+ }
+ else
+ {
+ long fileModifiedTime = await FileSystemUtility.GetDateModifiedAsync(EditingFile);
+ newState = fileModifiedTime != LastSavedSnapshot.DateModifiedFileTime ?
+ FileModificationState.Modified :
+ FileModificationState.Untouched;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ _fileStatusSemaphoreSlim.Release();
+ return;
+ }
+
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ FileModificationState = newState.Value;
+ });
+
+ _fileStatusSemaphoreSlim.Release();
+ }
+
+ private KeyboardCommandHandler GetKeyboardCommandHandler()
+ {
+ return new KeyboardCommandHandler(new List>
+ {
+ new KeyboardCommand(true, false, false, VirtualKey.F, (args) => ShowFindAndReplaceControl(showReplaceBar: false)),
+ new KeyboardCommand(true, false, true, VirtualKey.F, (args) => ShowFindAndReplaceControl(showReplaceBar: true)),
+ new KeyboardCommand(true, false, false, VirtualKey.H, (args) => ShowFindAndReplaceControl(showReplaceBar: true)),
+ new KeyboardCommand(true, false, false, VirtualKey.G, (args) => ShowGoToControl()),
+ new KeyboardCommand(false, true, false, VirtualKey.P, (args) => { if (FileTypeUtility.IsPreviewSupported(FileType)) ShowHideContentPreview(); }),
+ new KeyboardCommand(false, true, false, VirtualKey.D, (args) => ShowHideSideBySideDiffViewer()),
+ new KeyboardCommand(VirtualKey.F3, (args) =>
+ InitiateFindAndReplace(new FindAndReplaceEventArgs (_lastSearchContext, string.Empty, FindAndReplaceMode.FindOnly, SearchDirection.Next), out _)),
+ new KeyboardCommand(false, false, true, VirtualKey.F3, (args) =>
+ InitiateFindAndReplace(new FindAndReplaceEventArgs (_lastSearchContext, string.Empty, FindAndReplaceMode.FindOnly, SearchDirection.Previous), out _)),
+ new KeyboardCommand(VirtualKey.Escape, (args) => { OnEscapeKeyDown(); }, shouldHandle: false, shouldSwallow: true)
+ });
+ }
+
+ public void Init(TextFile textFile, StorageFile file, bool resetLastSavedSnapshot = true, bool clearUndoQueue = true, bool isModified = false, bool resetText = true)
+ {
+ _loaded = false;
+ EditingFile = file;
+ RequestedEncoding = null;
+ RequestedLineEnding = null;
+ if (resetText)
+ {
+ TextEditorCore.SetText(textFile.Content);
+ }
+ if (resetLastSavedSnapshot)
+ {
+ textFile.Content = TextEditorCore.GetText();
+ LastSavedSnapshot = textFile;
+ }
+ if (clearUndoQueue)
+ {
+ TextEditorCore.ClearUndoQueue();
+ }
+ IsModified = isModified;
+ _loaded = true;
+ }
+
+ public async Task ReloadFromEditingFileAsync(Encoding encoding = null)
+ {
+ if (EditingFile != null)
+ {
+ var textFile = await FileSystemUtility.ReadFileAsync(EditingFile, ignoreFileSizeLimit: false, encoding: encoding);
+ Init(textFile, EditingFile, clearUndoQueue: false);
+ LineEndingChanged?.Invoke(this, EventArgs.Empty);
+ EncodingChanged?.Invoke(this, EventArgs.Empty);
+ StartCheckingFileStatusPeriodically();
+ CloseSideBySideDiffViewer();
+ HideGoToControl();
+ FileReloaded?.Invoke(this, EventArgs.Empty);
+ AnalyticsService.TrackEvent(encoding == null ? "OnFileReloaded" : "OnFileReopenedWithEncoding");
+ }
+ }
+
+ public void ResetEditorState(TextEditorStateMetaData metadata, string newText = null)
+ {
+ if (!string.IsNullOrEmpty(metadata.RequestedEncoding))
+ {
+ TryChangeEncoding(EncodingUtility.GetEncodingByName(metadata.RequestedEncoding));
+ }
+
+ if (!string.IsNullOrEmpty(metadata.RequestedLineEnding))
+ {
+ TryChangeLineEnding(LineEndingUtility.GetLineEndingByName(metadata.RequestedLineEnding));
+ }
+
+ if (newText != null)
+ {
+ TextEditorCore.SetText(newText);
+ }
+
+ TextEditorCore.TextWrapping = metadata.WrapWord ? TextWrapping.Wrap : TextWrapping.NoWrap;
+ TextEditorCore.FontSize = metadata.FontZoomFactor * AppSettingsService.EditorFontSize;
+ TextEditorCore.SetTextSelectionPosition(metadata.SelectionStartPosition, metadata.SelectionEndPosition);
+ TextEditorCore.SetScrollViewerInitPosition(metadata.ScrollViewerHorizontalOffset, metadata.ScrollViewerVerticalOffset);
+ TextEditorCore.ClearUndoQueue();
+ }
+
+ public void RevertAllChanges()
+ {
+ Init(LastSavedSnapshot, EditingFile, clearUndoQueue: false);
+ ChangeReverted?.Invoke(this, EventArgs.Empty);
+ }
+
+ public bool TryChangeEncoding(Encoding encoding)
+ {
+ if (encoding == null) return false;
+
+ if (!EncodingUtility.Equals(LastSavedSnapshot.Encoding, encoding))
+ {
+ RequestedEncoding = encoding;
+ IsModified = true;
+ EncodingChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ if (RequestedEncoding != null && EncodingUtility.Equals(LastSavedSnapshot.Encoding, encoding))
+ {
+ RequestedEncoding = null;
+ IsModified = !NoChangesSinceLastSaved();
+ EncodingChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+ return false;
+ }
+
+ public bool TryChangeLineEnding(LineEnding lineEnding)
+ {
+ if (LastSavedSnapshot.LineEnding != lineEnding)
+ {
+ RequestedLineEnding = lineEnding;
+ IsModified = true;
+ LineEndingChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ if (RequestedLineEnding != null && LastSavedSnapshot.LineEnding == lineEnding)
+ {
+ RequestedLineEnding = null;
+ IsModified = !NoChangesSinceLastSaved();
+ LineEndingChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+ return false;
+ }
+
+ public LineEnding GetLineEnding()
+ {
+ return RequestedLineEnding ?? LastSavedSnapshot.LineEnding;
+ }
+
+ public Encoding GetEncoding()
+ {
+ return RequestedEncoding ?? LastSavedSnapshot.Encoding;
+ }
+
+ private void OpenSplitView(IContentPreviewExtension extension)
+ {
+ SplitPanel.Content = extension;
+ SplitPanelColumnDefinition.Width = new GridLength(1, GridUnitType.Star);
+ SplitPanelColumnDefinition.MinWidth = 100.0f;
+ SplitPanel.Visibility = Visibility.Visible;
+ GridSplitter.Visibility = Visibility.Visible;
+ AnalyticsService.TrackEvent("MarkdownContentPreview_Opened");
+ _isContentPreviewPanelOpened = true;
+ }
+
+ private void CloseSplitView()
+ {
+ SplitPanelColumnDefinition.Width = new GridLength(0);
+ EditorColumnDefinition.Width = new GridLength(1, GridUnitType.Star);
+ SplitPanelColumnDefinition.MinWidth = 0.0f;
+ SplitPanel.Visibility = Visibility.Collapsed;
+ GridSplitter.Visibility = Visibility.Collapsed;
+ TextEditorCore.ResetFocusAndScrollToPreviousPosition();
+ _isContentPreviewPanelOpened = false;
+ }
+
+ public void ShowHideContentPreview()
+ {
+ if (_contentPreviewExtension == null)
+ {
+ _contentPreviewExtension = ExtensionProvider?.GetContentPreviewExtension(FileType);
+ if (_contentPreviewExtension == null) return;
+ _contentPreviewExtension.Bind(TextEditorCore);
+ }
+
+ if (SplitPanel == null) LoadSplitView();
+
+ if (SplitPanel.Visibility == Visibility.Collapsed)
+ {
+ _contentPreviewExtension.IsExtensionEnabled = true;
+ OpenSplitView(_contentPreviewExtension);
+ }
+ else
+ {
+ _contentPreviewExtension.IsExtensionEnabled = false;
+ CloseSplitView();
+ }
+ }
+
+ public void OpenSideBySideDiffViewer()
+ {
+ if (string.Equals(LastSavedSnapshot.Content, TextEditorCore.GetText())) return;
+ if (Mode == TextEditorMode.DiffPreview) return;
+ if (SideBySideDiffViewer == null) LoadSideBySideDiffViewer();
+ Mode = TextEditorMode.DiffPreview;
+ TextEditorCore.IsEnabled = false;
+ EditorRowDefinition.Height = new GridLength(0);
+ SideBySideDiffViewRowDefinition.Height = new GridLength(1, GridUnitType.Star);
+ SideBySideDiffViewer.Visibility = Visibility.Visible;
+ SideBySideDiffViewer.RenderDiff(LastSavedSnapshot.Content, TextEditorCore.GetText(), ThemeSettingsService.ThemeMode);
+ SideBySideDiffViewer.Focus();
+ AnalyticsService.TrackEvent("SideBySideDiffViewer_Opened");
+ }
+
+ public void CloseSideBySideDiffViewer()
+ {
+ if (Mode != TextEditorMode.DiffPreview) return;
+ Mode = TextEditorMode.Editing;
+ TextEditorCore.IsEnabled = true;
+ EditorRowDefinition.Height = new GridLength(1, GridUnitType.Star);
+ SideBySideDiffViewRowDefinition.Height = new GridLength(0);
+ SideBySideDiffViewer.Visibility = Visibility.Collapsed;
+ SideBySideDiffViewer.StopRenderingAndClearCache();
+ TextEditorCore.ResetFocusAndScrollToPreviousPosition();
+ }
+
+ private void ShowHideSideBySideDiffViewer()
+ {
+ if (Mode != TextEditorMode.DiffPreview)
+ {
+ OpenSideBySideDiffViewer();
+ }
+ else
+ {
+ CloseSideBySideDiffViewer();
+ }
+ }
+
+ ///
+ /// Returns 1-based indexing values
+ ///
+ public void GetLineColumnSelection(
+ out int startLine,
+ out int endLine,
+ out int startColumn,
+ out int endColumn,
+ out int selected,
+ out int lineCount)
+ {
+ TextEditorCore.GetLineColumnSelection(
+ out startLine,
+ out endLine,
+ out startColumn,
+ out endColumn,
+ out selected,
+ out lineCount,
+ GetLineEnding());
+ }
+
+ public double GetFontZoomFactor()
+ {
+ return TextEditorCore.GetFontZoomFactor();
+ }
+
+ public void SetFontZoomFactor(double fontZoomFactor)
+ {
+ TextEditorCore.SetFontZoomFactor(fontZoomFactor);
+ }
+
+ public bool IsEditorEnabled()
+ {
+ return TextEditorCore.IsEnabled;
+ }
+
+ public async Task SaveContentToFileAndUpdateEditorStateAsync(StorageFile file)
+ {
+ if (Mode == TextEditorMode.DiffPreview) CloseSideBySideDiffViewer();
+ TextFile textFile = await SaveContentToFileAsync(file); // Will throw if not succeeded
+ FileModificationState = FileModificationState.Untouched;
+ Init(textFile, file, clearUndoQueue: false, resetText: false);
+ FileSaved?.Invoke(this, EventArgs.Empty);
+ StartCheckingFileStatusPeriodically();
+ }
+
+ private async Task SaveContentToFileAsync(StorageFile file)
+ {
+ var text = TextEditorCore.GetText();
+ var encoding = RequestedEncoding ?? LastSavedSnapshot.Encoding;
+ var lineEnding = RequestedLineEnding ?? LastSavedSnapshot.LineEnding;
+ await FileSystemUtility.WriteTextToFileAsync(file, LineEndingUtility.ApplyLineEnding(text, lineEnding), encoding); // Will throw if not succeeded
+ var newFileModifiedTime = await FileSystemUtility.GetDateModifiedAsync(file);
+ return new TextFile(text, encoding, lineEnding, newFileModifiedTime);
+ }
+
+ public string GetContentForSharing()
+ {
+ return TextEditorCore.Document.Selection.StartPosition == TextEditorCore.Document.Selection.EndPosition ?
+ TextEditorCore.GetText() :
+ TextEditorCore.Document.Selection.Text;
+ }
+
+ public void TypeText(string text)
+ {
+ if (TextEditorCore.IsEnabled)
+ {
+ TextEditorCore.Document.Selection.TypeText(text);
+ }
+ }
+
+ public void Focus()
+ {
+ if (Mode == TextEditorMode.DiffPreview)
+ {
+ SideBySideDiffViewer.Focus();
+ }
+ else if (Mode == TextEditorMode.Editing)
+ {
+ TextEditorCore.ResetFocusAndScrollToPreviousPosition();
+ }
+ }
+
+ public FlyoutBase GetContextFlyout()
+ {
+ return TextEditorCore.ContextFlyout;
+ }
+
+ public void CopyTextToWindowsClipboard(TextControlCopyingToClipboardEventArgs args)
+ {
+ if (args != null)
+ {
+ args.Handled = true;
+ }
+
+ if (AppSettingsService.IsSmartCopyEnabled)
+ {
+ TextEditorCore.SmartlyTrimTextSelection();
+ }
+
+ CopyTextToWindowsClipboardInternal(true);
+ }
+
+ public void CutSelectedTextToWindowsClipboard(TextControlCuttingToClipboardEventArgs args)
+ {
+ if (args != null)
+ {
+ args.Handled = true;
+ }
+
+ CopyTextToWindowsClipboardInternal(false);
+ TextEditorCore.Document.Selection.SetText(TextSetOptions.None, string.Empty);
+ }
+
+ private void CopyTextToWindowsClipboardInternal(bool clearLineSelection)
+ {
+ try
+ {
+ DataPackage dataPackage = new DataPackage { RequestedOperation = DataPackageOperation.Copy };
+
+ // Selection.Length can be negative when user selecting text from right to left
+ var isTextSelected = TextEditorCore.Document.Selection.Length != 0;
+ var cursorPosition = TextEditorCore.Document.Selection.StartPosition;
+
+ if (!isTextSelected)
+ {
+ TextEditorCore.Document.Selection.Expand(TextRangeUnit.Paragraph);
+ }
+
+ var text = LineEndingUtility.ApplyLineEnding(TextEditorCore.Document.Selection.Text, GetLineEnding());
+ dataPackage.SetText(text);
+
+ if (clearLineSelection && !isTextSelected)
+ {
+ TextEditorCore.Document.Selection.SetRange(cursorPosition, cursorPosition);
+ }
+
+ Clipboard.SetContentWithOptions(dataPackage, new ClipboardContentOptions() { IsAllowedInHistory = true, IsRoamable = true });
+ Clipboard.Flush(); // This method allows the content to remain available after the application shuts down.
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(TextEditor)}] Failed to copy plain text to Windows clipboard: {ex.Message}");
+ }
+ }
+
+ public bool NoChangesSinceLastSaved(bool compareTextOnly = false)
+ {
+ if (!_loaded) return true;
+
+ if (!compareTextOnly)
+ {
+ if (RequestedLineEnding != null)
+ {
+ return false;
+ }
+
+ if (RequestedEncoding != null)
+ {
+ return false;
+ }
+ }
+
+ return string.Equals(LastSavedSnapshot.Content, TextEditorCore.GetText());
+ }
+
+ private void OnEscapeKeyDown()
+ {
+ if (_isContentPreviewPanelOpened)
+ {
+ _contentPreviewExtension.IsExtensionEnabled = false;
+ CloseSplitView();
+ }
+ else if (FindAndReplacePlaceholder != null && FindAndReplacePlaceholder.Visibility == Visibility.Visible)
+ {
+ HideFindAndReplaceControl();
+ TextEditorCore.Focus(FocusState.Programmatic);
+ }
+ else if (GoToPlaceholder != null && GoToPlaceholder.Visibility == Visibility.Visible)
+ {
+ HideGoToControl();
+ TextEditorCore.Focus(FocusState.Programmatic);
+ }
+ }
+
+ private void LoadSplitView()
+ {
+ FindName("SplitPanel");
+ FindName("GridSplitter");
+ SplitPanel.Visibility = Visibility.Collapsed;
+ GridSplitter.Visibility = Visibility.Collapsed;
+ SplitPanel.KeyDown += SplitPanel_OnKeyDown;
+ }
+
+ private void LoadSideBySideDiffViewer()
+ {
+ FindName("SideBySideDiffViewer");
+ SideBySideDiffViewer.Visibility = Visibility.Collapsed;
+ SideBySideDiffViewer.OnCloseEvent += SideBySideDiffViewer_OnCloseEvent;
+ }
+
+ private void SideBySideDiffViewer_OnCloseEvent(object sender, EventArgs e)
+ {
+ CloseSideBySideDiffViewer();
+ }
+
+ private void SplitPanel_OnKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var result = _keyboardCommandHandler.Handle(e);
+ if (result.ShouldHandle)
+ {
+ e.Handled = true;
+ }
+ }
+
+ private void TextEditorCore_OnSelectionChanged(object sender, RoutedEventArgs e)
+ {
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void TextEditorCore_OnFontZoomFactorChanged(object sender, double e)
+ {
+ FontZoomFactorChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void TextEditorCore_OnKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
+ var alt = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu);
+
+ if (FindAndReplacePlaceholder?.Visibility == Visibility.Visible && !ctrl.HasFlag(CoreVirtualKeyStates.Down) && !alt.HasFlag(CoreVirtualKeyStates.Down))
+ {
+ if (e.Key == VirtualKey.F3)
+ {
+ return;
+ }
+ }
+
+ var result = _keyboardCommandHandler.Handle(e);
+ if (result.ShouldHandle)
+ {
+ e.Handled = true;
+ }
+ }
+
+ private void TextEditorCore_OnTextChanging(RichEditBox textEditor, RichEditBoxTextChangingEventArgs args)
+ {
+ if (!args.IsContentChanging || !_loaded) return;
+ if (IsModified)
+ {
+ IsModified = !NoChangesSinceLastSaved();
+ }
+ else
+ {
+ IsModified = !NoChangesSinceLastSaved(compareTextOnly: true);
+ }
+ TextChanging?.Invoke(this, EventArgs.Empty);
+
+ GoToPlaceholder?.Dismiss();
+ }
+
+ private void TextEditorCore_CopyTextToWindowsClipboardRequested(object sender, TextControlCopyingToClipboardEventArgs e)
+ {
+ CopyTextToWindowsClipboard(e);
+ }
+
+ private void TextEditorCore_CutSelectedTextToWindowsClipboardRequested(object sender, TextControlCuttingToClipboardEventArgs e)
+ {
+ CutSelectedTextToWindowsClipboard(e);
+ }
+
+ private void FindAndReplaceControl_OnToggleReplaceModeButtonClicked(object sender, bool showReplaceBar)
+ {
+ ShowFindAndReplaceControl(showReplaceBar);
+ }
+
+ public void ShowFindAndReplaceControl(bool showReplaceBar)
+ {
+ if (!TextEditorCore.IsEnabled || Mode != TextEditorMode.Editing)
+ {
+ return;
+ }
+
+ GoToPlaceholder?.Dismiss();
+
+ if (FindAndReplacePlaceholder == null)
+ {
+ FindName("FindAndReplacePlaceholder"); // Lazy loading
+ }
+
+ var findAndReplace = (FindAndReplaceControl)FindAndReplacePlaceholder.Content;
+
+ if (findAndReplace == null) return;
+
+ FindAndReplacePlaceholder.Height = findAndReplace.GetHeight(showReplaceBar);
+ findAndReplace.ShowReplaceBar(showReplaceBar);
+
+ if (FindAndReplacePlaceholder.Visibility == Visibility.Collapsed)
+ {
+ FindAndReplacePlaceholder.Show();
+ }
+
+ findAndReplace.Focus(TextEditorCore.GetSearchString(), FindAndReplaceMode.FindOnly);
+ }
+
+ public void HideFindAndReplaceControl()
+ {
+ FindAndReplacePlaceholder?.Dismiss();
+ }
+
+ private async void FindAndReplaceControl_OnFindAndReplaceButtonClicked(object sender, FindAndReplaceEventArgs e)
+ {
+ TextEditorCore.Focus(FocusState.Programmatic);
+ InitiateFindAndReplace(e, out bool found);
+
+ // In case user hit "enter" key in search box instead of clicking on search button or hit F3
+ // We should re-focus on FindAndReplaceControl to make the next search "flows"
+ if (!(sender is Button))
+ {
+ if (found)
+ {
+ // Wait for layout to refresh (ScrollViewer scroll to the found text) before focusing
+ await Task.Delay(10);
+ }
+ FindAndReplaceControl.Focus(string.Empty, e.FindAndReplaceMode);
+ }
+ }
+
+ private void InitiateFindAndReplace(FindAndReplaceEventArgs findAndReplaceEventArgs, out bool found)
+ {
+ found = false;
+
+ if (string.IsNullOrEmpty(findAndReplaceEventArgs.SearchContext.SearchText)) return;
+
+ bool regexError = false;
+
+ if (FindAndReplacePlaceholder?.Visibility == Visibility.Visible)
+ _lastSearchContext = findAndReplaceEventArgs.SearchContext;
+
+ switch (findAndReplaceEventArgs.FindAndReplaceMode)
+ {
+ case FindAndReplaceMode.FindOnly:
+ found = findAndReplaceEventArgs.SearchDirection == SearchDirection.Next
+ ? TextEditorCore.TryFindNextAndSelect(
+ findAndReplaceEventArgs.SearchContext,
+ stopAtEof: false,
+ out regexError)
+ : TextEditorCore.TryFindPreviousAndSelect(
+ findAndReplaceEventArgs.SearchContext,
+ stopAtBof: false,
+ out regexError);
+ break;
+ case FindAndReplaceMode.Replace:
+ found = findAndReplaceEventArgs.SearchDirection == SearchDirection.Next
+ ? TextEditorCore.TryFindNextAndReplace(
+ findAndReplaceEventArgs.SearchContext,
+ findAndReplaceEventArgs.ReplaceText,
+ out regexError)
+ : TextEditorCore.TryFindPreviousAndReplace(
+ findAndReplaceEventArgs.SearchContext,
+ findAndReplaceEventArgs.ReplaceText,
+ out regexError);
+ break;
+ case FindAndReplaceMode.ReplaceAll:
+ found = TextEditorCore.TryFindAndReplaceAll(
+ findAndReplaceEventArgs.SearchContext,
+ findAndReplaceEventArgs.ReplaceText,
+ out regexError);
+ break;
+ }
+
+ if (!found)
+ {
+ if (findAndReplaceEventArgs.SearchContext.UseRegex && regexError)
+ {
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("FindAndReplace_NotificationMsg_InvalidRegex"), 1500);
+ }
+ else
+ {
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("FindAndReplace_NotificationMsg_NotFound"), 1500);
+ }
+ }
+ }
+
+ private void FindAndReplacePlaceholder_Closed(object sender, InAppNotificationClosedEventArgs e)
+ {
+ FindAndReplacePlaceholder.Visibility = Visibility.Collapsed;
+ }
+
+ private void FindAndReplaceControl_OnDismissKeyDown(object sender, RoutedEventArgs e)
+ {
+ FindAndReplacePlaceholder?.Dismiss();
+ TextEditorCore.Focus(FocusState.Programmatic);
+ }
+
+ public void ShowGoToControl()
+ {
+ if (!TextEditorCore.IsEnabled || Mode != TextEditorMode.Editing) return;
+
+ FindAndReplacePlaceholder?.Dismiss();
+
+ if (GoToPlaceholder == null)
+ FindName("GoToPlaceholder"); // Lazy loading
+
+ var goToControl = (GoToControl)GoToPlaceholder.Content;
+
+ if (goToControl == null) return;
+
+ GoToPlaceholder.Height = goToControl.GetHeight();
+
+ if (GoToPlaceholder.Visibility == Visibility.Collapsed)
+ GoToPlaceholder.Show();
+
+ GetLineColumnSelection(out var startLine, out _, out _, out _, out _, out var lineCount);
+ goToControl.SetLineData(startLine, lineCount);
+ goToControl.Focus();
+ }
+
+ public void HideGoToControl()
+ {
+ GoToPlaceholder?.Dismiss();
+ }
+
+ private void GoToControl_OnGoToButtonClicked(object sender, GoToEventArgs e)
+ {
+ var found = false;
+
+ if (int.TryParse(e.SearchLine, out var line))
+ {
+ found = TextEditorCore.GoTo(line);
+ }
+
+ if (!found)
+ {
+ GoToControl.Focus();
+ NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("FindAndReplace_NotificationMsg_NotFound"), 1500);
+ }
+ else
+ {
+ HideGoToControl();
+ TextEditorCore.Focus(FocusState.Programmatic);
+ }
+ }
+
+ private void GoToPlaceholder_Closed(object sender, InAppNotificationClosedEventArgs e)
+ {
+ GoToPlaceholder.Visibility = Visibility.Collapsed;
+ }
+
+ private void GoToControl_OnDismissKeyDown(object sender, RoutedEventArgs e)
+ {
+ GoToPlaceholder.Dismiss();
+ TextEditorCore.Focus(FocusState.Programmatic);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs b/src/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs
new file mode 100644
index 0000000..2d6a582
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs
@@ -0,0 +1,388 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using Notepads.Utilities;
+ using Windows.ApplicationModel.Resources;
+ using Windows.System;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ public sealed class TextEditorContextFlyout : MenuFlyout
+ {
+ private MenuFlyoutItem _cut;
+ private MenuFlyoutItem _copy;
+ private MenuFlyoutItem _paste;
+ private MenuFlyoutItem _undo;
+ private MenuFlyoutItem _redo;
+ private MenuFlyoutItem _selectAll;
+ private MenuFlyoutItem _rightToLeftReadingOrder;
+ private MenuFlyoutItem _webSearch;
+ private MenuFlyoutItem _wordWrap;
+ private MenuFlyoutItem _previewToggle;
+ private MenuFlyoutItem _share;
+
+ private MenuFlyout _proofingFlyout;
+ private readonly MenuFlyoutSeparator _proofingSeparator = new MenuFlyoutSeparator();
+
+ private readonly ITextEditor _textEditor;
+ private readonly TextEditorCore _textEditorCore;
+
+ private readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView();
+
+ public TextEditorContextFlyout(ITextEditor editor, TextEditorCore editorCore)
+ {
+ _textEditor = editor;
+ _textEditorCore = editorCore;
+
+ Items.Add(Cut);
+ Items.Add(Copy);
+ Items.Add(Paste);
+ Items.Add(Undo);
+ Items.Add(Redo);
+ Items.Add(SelectAll);
+ Items.Add(new MenuFlyoutSeparator());
+ Items.Add(RightToLeftReadingOrder);
+ Items.Add(WordWrap);
+ Items.Add(WebSearch);
+ Items.Add(PreviewToggle);
+ Items.Add(Share);
+
+ Opening += TextEditorContextFlyout_Opening;
+ Closed += TextEditorContextFlyout_Closed;
+ }
+
+ public void Dispose()
+ {
+ Opening -= TextEditorContextFlyout_Opening;
+ Closed -= TextEditorContextFlyout_Closed;
+ }
+
+ private void TextEditorContextFlyout_Opening(object sender, object e)
+ {
+ if (_textEditorCore.IsSpellCheckEnabled &&
+ _textEditorCore.ProofingMenuFlyout is MenuFlyout proofingFlyout &&
+ proofingFlyout.Items?.Count > 0)
+ {
+ BuildProofingSubItems(proofingFlyout);
+ }
+ else
+ {
+ _proofingSeparator.Visibility = Visibility.Collapsed;
+ }
+
+ if (_textEditorCore.Document.Selection.Type == SelectionType.InsertionPoint ||
+ _textEditorCore.Document.Selection.Type == SelectionType.None)
+ {
+ PrepareForInsertionMode();
+ }
+ else
+ {
+ PrepareForSelectionMode();
+ }
+
+ Undo.IsEnabled = _textEditorCore.Document.CanUndo();
+ Redo.IsEnabled = _textEditorCore.Document.CanRedo();
+
+ PreviewToggle.Visibility = FileTypeUtility.IsPreviewSupported(_textEditor.FileType) ? Visibility.Visible : Visibility.Collapsed;
+ WordWrap.Icon.Visibility = (_textEditorCore.TextWrapping == TextWrapping.Wrap) ? Visibility.Visible : Visibility.Collapsed;
+ RightToLeftReadingOrder.Icon.Visibility = (_textEditorCore.FlowDirection == FlowDirection.RightToLeft) ? Visibility.Visible : Visibility.Collapsed;
+
+ if (App.IsGameBarWidget)
+ {
+ Share.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void BuildProofingSubItems(MenuFlyout proofingFlyout)
+ {
+ _proofingFlyout = proofingFlyout;
+
+ foreach (var item in _proofingFlyout.Items)
+ {
+ Items.Insert(_proofingFlyout.Items.IndexOf(item), item);
+ }
+
+ if (!Items.Contains(_proofingSeparator))
+ {
+ Items.Insert(_proofingFlyout.Items.Count, _proofingSeparator);
+ }
+
+ if (_proofingSeparator.Visibility == Visibility.Collapsed)
+ {
+ _proofingSeparator.Visibility = Visibility.Visible;
+ }
+ }
+
+ private void TextEditorContextFlyout_Closed(object sender, object e)
+ {
+ if (_proofingFlyout?.Items?.Count > 0)
+ {
+ foreach (var item in _proofingFlyout.Items)
+ {
+ Items.Remove(item);
+ }
+ }
+ }
+
+ public void PrepareForInsertionMode()
+ {
+ RightToLeftReadingOrder.Visibility = !string.IsNullOrEmpty(_textEditor.GetText()) ? Visibility.Visible : Visibility.Collapsed;
+ WebSearch.Visibility = Visibility.Collapsed;
+ Share.Text = _resourceLoader.GetString("TextEditor_ContextFlyout_ShareButtonDisplayText");
+ }
+
+ public void PrepareForSelectionMode()
+ {
+ RightToLeftReadingOrder.Visibility = !string.IsNullOrEmpty(_textEditor.GetText()) ? Visibility.Visible : Visibility.Collapsed;
+ WebSearch.Visibility = Visibility.Visible;
+ Share.Text = _resourceLoader.GetString("TextEditor_ContextFlyout_ShareSelectedButtonDisplayText");
+ }
+
+ public MenuFlyoutItem Cut
+ {
+ get
+ {
+ if (_cut != null) return _cut;
+
+ _cut = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Cut), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_CutButtonDisplayText") };
+ _cut.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.X,
+ IsEnabled = false,
+ });
+ _cut.Click += (sender, args) => _textEditorCore.Document.Selection.Cut();
+ return _cut;
+ }
+ }
+
+ public MenuFlyoutItem Copy
+ {
+ get
+ {
+ if (_copy != null) return _copy;
+
+ _copy = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Copy), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_CopyButtonDisplayText") };
+ _copy.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.C,
+ IsEnabled = false,
+ });
+ _copy.Click += (sender, args) => _textEditor.CopyTextToWindowsClipboard(null);
+ return _copy;
+ }
+ }
+
+ public MenuFlyoutItem Paste
+ {
+ get
+ {
+ if (_paste != null) return _paste;
+
+ _paste = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Paste), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_PasteButtonDisplayText") };
+ _paste.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.V,
+ IsEnabled = false,
+ });
+ _paste.Click += async (sender, args) => await _textEditorCore.PastePlainTextFromWindowsClipboardAsync(null);
+ return _paste;
+ }
+ }
+
+ public MenuFlyoutItem Undo
+ {
+ get
+ {
+ if (_undo != null) return _undo;
+
+ _undo = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Undo), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_UndoButtonDisplayText") };
+ _undo.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.Z,
+ IsEnabled = false,
+ });
+ _undo.Click += (sender, args) => _textEditorCore.Undo();
+ return _undo;
+ }
+ }
+
+ public MenuFlyoutItem Redo
+ {
+ get
+ {
+ if (_redo != null) return _redo;
+
+ _redo = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Redo), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_RedoButtonDisplayText") };
+ _redo.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = (VirtualKeyModifiers.Control & VirtualKeyModifiers.Shift),
+ Key = VirtualKey.Z,
+ IsEnabled = false,
+ });
+ _redo.KeyboardAcceleratorTextOverride = "Ctrl+Shift+Z";
+ _redo.Click += (sender, args) => _textEditorCore.Redo();
+ return _redo;
+ }
+ }
+
+ public MenuFlyoutItem SelectAll
+ {
+ get
+ {
+ if (_selectAll != null) return _selectAll;
+
+ _selectAll = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.SelectAll), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_SelectAllButtonDisplayText") };
+ _selectAll.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.A,
+ IsEnabled = false,
+ });
+ _selectAll.Click += (sender, args) => _textEditorCore.Document.Selection.SetRange(0, Int32.MaxValue);
+ return _selectAll;
+ }
+ }
+
+ public MenuFlyoutItem RightToLeftReadingOrder
+ {
+ get
+ {
+ if (_rightToLeftReadingOrder != null) return _rightToLeftReadingOrder;
+
+ _rightToLeftReadingOrder = new MenuFlyoutItem
+ {
+ Text = _resourceLoader.GetString("TextEditor_ContextFlyout_RightToLeftReadingOrderButtonDisplayText"),
+ Icon = new FontIcon()
+ {
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ Glyph = "\uE73E"
+ }
+ };
+ _rightToLeftReadingOrder.Icon.Visibility = (_textEditorCore.FlowDirection == FlowDirection.RightToLeft) ? Visibility.Visible : Visibility.Collapsed;
+ _rightToLeftReadingOrder.Click += (sender, args) =>
+ {
+ var flowDirection = (_textEditorCore.FlowDirection == FlowDirection.LeftToRight)
+ ? FlowDirection.RightToLeft
+ : FlowDirection.LeftToRight;
+ _textEditorCore.SwitchTextFlowDirection(flowDirection);
+ _rightToLeftReadingOrder.Icon.Visibility = (_textEditorCore.FlowDirection == FlowDirection.RightToLeft) ? Visibility.Visible : Visibility.Collapsed;
+ };
+ return _rightToLeftReadingOrder;
+ }
+ }
+
+ public MenuFlyoutItem WebSearch
+ {
+ get
+ {
+ if (_webSearch != null) return _webSearch;
+
+ _webSearch = new MenuFlyoutItem
+ {
+ Text = _resourceLoader.GetString("TextEditor_ContextFlyout_WebSearchButtonDisplayText"),
+ Icon = new FontIcon()
+ {
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ Glyph = "\uE721"
+ }
+ };
+ _webSearch.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Control,
+ Key = VirtualKey.E,
+ IsEnabled = false,
+ });
+ _webSearch.Click += async (sender, args) => await _textEditorCore.SearchInWebAsync();
+
+ return _webSearch;
+ }
+ }
+
+ public MenuFlyoutItem Share
+ {
+ get
+ {
+ if (_share != null) return _share;
+
+ _share = new MenuFlyoutItem { Icon = new SymbolIcon(Symbol.Share), Text = _resourceLoader.GetString("TextEditor_ContextFlyout_ShareButtonDisplayText") };
+ _share.Click += (sender, args) => Windows.ApplicationModel.DataTransfer.DataTransferManager.ShowShareUI();
+ return _share;
+ }
+ }
+
+ public MenuFlyoutItem WordWrap
+ {
+ get
+ {
+ if (_wordWrap != null) return _wordWrap;
+
+ _wordWrap = new MenuFlyoutItem
+ {
+ Text = _resourceLoader.GetString("TextEditor_ContextFlyout_WordWrapButtonDisplayText"),
+ Icon = new FontIcon()
+ {
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ Glyph = "\uE73E"
+ }
+ };
+ _wordWrap.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Menu,
+ Key = VirtualKey.Z,
+ IsEnabled = false,
+ });
+ _wordWrap.Icon.Visibility = _textEditorCore.TextWrapping == TextWrapping.Wrap ? Visibility.Visible : Visibility.Collapsed;
+ _wordWrap.Click += (sender, args) =>
+ {
+ _wordWrap.Icon.Visibility = _textEditorCore.TextWrapping == TextWrapping.Wrap ? Visibility.Visible : Visibility.Collapsed;
+ _textEditorCore.TextWrapping = _textEditorCore.TextWrapping == TextWrapping.Wrap ? TextWrapping.NoWrap : TextWrapping.Wrap;
+ };
+ return _wordWrap;
+ }
+ }
+
+ public MenuFlyoutItem PreviewToggle
+ {
+ get
+ {
+ if (_previewToggle != null) return _previewToggle;
+
+ _previewToggle = new MenuFlyoutItem
+ {
+ Text = _resourceLoader.GetString("TextEditor_ContextFlyout_PreviewToggleDisplay_Text"),
+ Icon = new FontIcon()
+ {
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ Glyph = "\uE89F"
+ }
+ };
+ _previewToggle.KeyboardAccelerators.Add(new KeyboardAccelerator()
+ {
+ Modifiers = VirtualKeyModifiers.Menu,
+ Key = VirtualKey.P,
+ IsEnabled = false,
+ });
+ _previewToggle.Click += (sender, args) =>
+ {
+ if (FileTypeUtility.IsPreviewSupported(_textEditor.FileType))
+ {
+ _textEditor.ShowHideContentPreview();
+ }
+ };
+ return _previewToggle;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.DateTime.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.DateTime.cs
new file mode 100644
index 0000000..a52106c
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.DateTime.cs
@@ -0,0 +1,50 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Globalization;
+ using Windows.UI.Text;
+
+ public partial class TextEditorCore
+ {
+ private bool _hasAddedLogEntry = false;
+
+ private void InsertDateTimeString()
+ {
+ var dateStr = DateTime.Now.ToString(CultureInfo.CurrentCulture);
+ Document.Selection.SetText(TextSetOptions.None, dateStr);
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ }
+
+ ///
+ ///
+ /// Adds "Windows Notepad" style header with current date and time,
+ /// if a text document contains ".LOG" at the very beginning of file.
+ /// User can then add log entry from the very next line.
+ ///
+ ///
+ ///
+ public void TryInsertNewLogEntry()
+ {
+ if (_hasAddedLogEntry) return;
+
+ var docText = GetText();
+
+ if (!docText.StartsWith(".LOG")) return;
+
+ _hasAddedLogEntry = true;
+
+ Document.Selection.StartPosition = docText.Length;
+
+ Document.Selection.SetText(TextSetOptions.None, RichEditBoxDefaultLineEnding
+ + DateTime.Now.ToString("h:mm tt M/dd/yyyy")
+ + RichEditBoxDefaultLineEnding);
+
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.DuplicateText.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.DuplicateText.cs
new file mode 100644
index 0000000..c12e45e
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.DuplicateText.cs
@@ -0,0 +1,76 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.UI.Text;
+ using Notepads.Services;
+
+ public partial class TextEditorCore
+ {
+ private void DuplicateText()
+ {
+ try
+ {
+ GetLineColumnSelection(
+ out int startLineIndex,
+ out int endLineIndex,
+ out int startColumnIndex,
+ out int endColumnIndex,
+ out int selectedCount,
+ out int lineCount);
+
+ GetTextSelectionPosition(out var start, out var end);
+
+ if (end == start)
+ {
+ // Duplicate Line
+ var lines = GetDocumentLinesCache();
+ var line = lines[startLineIndex - 1];
+ var column = Document.Selection.EndPosition + line.Length + 1;
+
+ if (startColumnIndex == 1)
+ Document.Selection.EndPosition += 1;
+
+ Document.Selection.EndOf(TextRangeUnit.Paragraph, false);
+
+ if (startLineIndex < lineCount)
+ Document.Selection.EndPosition -= 1;
+
+ Document.Selection.SetText(TextSetOptions.None, RichEditBoxDefaultLineEnding + line);
+ Document.Selection.StartPosition = Document.Selection.EndPosition = column;
+ }
+ else
+ {
+ // Duplicate selection
+ var textRange = Document.GetRange(start, end);
+ textRange.GetText(TextGetOptions.None, out string text);
+
+ if (text.EndsWith(RichEditBoxDefaultLineEnding))
+ {
+ Document.Selection.EndOf(TextRangeUnit.Line, false);
+
+ if (startLineIndex < lineCount && end < GetText().Length)
+ Document.Selection.StartPosition = Document.Selection.EndPosition - 1;
+ }
+ else
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ }
+
+ Document.Selection.SetText(TextSetOptions.None, text);
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(TextEditorCore)}] Failed to duplicate text: {ex}");
+ AnalyticsService.TrackEvent("TextEditorCore_FailedToDuplicateText",
+ new Dictionary { { "Exception", ex.ToString() } });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.ExternalEventListener.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.ExternalEventListener.cs
new file mode 100644
index 0000000..1de43f5
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.ExternalEventListener.cs
@@ -0,0 +1,119 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using Windows.UI;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Media;
+ using Notepads.Extensions;
+ using Notepads.Services;
+
+ public partial class TextEditorCore
+ {
+ internal void HookExternalEvents()
+ {
+ AppSettingsService.OnFontFamilyChanged += EditorSettingsService_OnFontFamilyChanged;
+ AppSettingsService.OnFontSizeChanged += EditorSettingsService_OnFontSizeChanged;
+ AppSettingsService.OnFontStyleChanged += EditorSettingsService_OnFontStyleChanged;
+ AppSettingsService.OnFontWeightChanged += EditorSettingsService_OnFontWeightChanged;
+ AppSettingsService.OnDefaultTextWrappingChanged += EditorSettingsService_OnDefaultTextWrappingChanged;
+ AppSettingsService.OnHighlightMisspelledWordsChanged += EditorSettingsService_OnHighlightMisspelledWordsChanged;
+ AppSettingsService.OnDefaultDisplayLineNumbersViewStateChanged += EditorSettingsService_OnDefaultDisplayLineNumbersViewStateChanged;
+ AppSettingsService.OnDefaultLineHighlighterViewStateChanged += EditorSettingsService_OnDefaultLineHighlighterViewStateChanged;
+
+ ThemeSettingsService.OnAccentColorChanged += ThemeSettingsService_OnAccentColorChanged;
+ }
+
+ internal void UnhookExternalEvents()
+ {
+ AppSettingsService.OnFontFamilyChanged -= EditorSettingsService_OnFontFamilyChanged;
+ AppSettingsService.OnFontSizeChanged -= EditorSettingsService_OnFontSizeChanged;
+ AppSettingsService.OnFontStyleChanged -= EditorSettingsService_OnFontStyleChanged;
+ AppSettingsService.OnFontWeightChanged -= EditorSettingsService_OnFontWeightChanged;
+ AppSettingsService.OnDefaultTextWrappingChanged -= EditorSettingsService_OnDefaultTextWrappingChanged;
+ AppSettingsService.OnHighlightMisspelledWordsChanged -= EditorSettingsService_OnHighlightMisspelledWordsChanged;
+ AppSettingsService.OnDefaultDisplayLineNumbersViewStateChanged -= EditorSettingsService_OnDefaultDisplayLineNumbersViewStateChanged;
+ AppSettingsService.OnDefaultLineHighlighterViewStateChanged -= EditorSettingsService_OnDefaultLineHighlighterViewStateChanged;
+
+ ThemeSettingsService.OnAccentColorChanged -= ThemeSettingsService_OnAccentColorChanged;
+ }
+
+ private async void EditorSettingsService_OnFontFamilyChanged(object sender, string fontFamily)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ FontFamily = new FontFamily(fontFamily);
+ SetDefaultTabStopAndLineSpacing(FontFamily, FontSize);
+ });
+ }
+
+ private async void EditorSettingsService_OnFontSizeChanged(object sender, int fontSize)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ FontSize = fontSize;
+ });
+ }
+
+ private async void EditorSettingsService_OnFontStyleChanged(object sender, FontStyle fontStyle)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ FontStyle = fontStyle;
+ });
+ }
+
+ private async void EditorSettingsService_OnFontWeightChanged(object sender, FontWeight fontWeight)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ FontWeight = fontWeight;
+ });
+ }
+
+ private async void EditorSettingsService_OnDefaultTextWrappingChanged(object sender, TextWrapping textWrapping)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ TextWrapping = textWrapping;
+ });
+ }
+
+ private async void EditorSettingsService_OnHighlightMisspelledWordsChanged(object sender, bool isSpellCheckEnabled)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ IsSpellCheckEnabled = isSpellCheckEnabled;
+ });
+ }
+
+ private async void EditorSettingsService_OnDefaultDisplayLineNumbersViewStateChanged(object sender, bool displayLineNumbers)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ DisplayLineNumbers = displayLineNumbers;
+ });
+ }
+
+ private async void EditorSettingsService_OnDefaultLineHighlighterViewStateChanged(object sender, bool displayLineHighlighter)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ DisplayLineHighlighter = displayLineHighlighter;
+ });
+ }
+
+ private async void ThemeSettingsService_OnAccentColorChanged(object sender, Color color)
+ {
+ await Dispatcher.CallOnUIThreadAsync(() =>
+ {
+ SelectionHighlightColor = new SolidColorBrush(color);
+ SelectionHighlightColorWhenNotFocused = new SolidColorBrush(color);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.FindAndReplace.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.FindAndReplace.cs
new file mode 100644
index 0000000..2f596fc
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.FindAndReplace.cs
@@ -0,0 +1,363 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Text.RegularExpressions;
+ using Windows.UI.Text;
+ using Notepads.Controls.FindAndReplace;
+ using Notepads.Extensions;
+
+ public partial class TextEditorCore
+ {
+ public string GetSearchString()
+ {
+ var searchString = Document.Selection.Text.Trim();
+
+ if (searchString.Contains(RichEditBoxDefaultLineEnding.ToString())) return string.Empty;
+
+ var document = GetText();
+
+ if (string.IsNullOrEmpty(searchString) && Document.Selection.StartPosition < document.Length)
+ {
+ var startIndex = Document.Selection.StartPosition;
+ var endIndex = startIndex;
+
+ for (; startIndex >= 0; startIndex--)
+ {
+ if (!char.IsLetterOrDigit(document[startIndex]))
+ break;
+ }
+
+ for (; endIndex < document.Length; endIndex++)
+ {
+ if (!char.IsLetterOrDigit(document[endIndex]))
+ break;
+ }
+
+ if (startIndex != endIndex) return document.Substring(startIndex + 1, endIndex - startIndex - 1);
+ }
+
+ return searchString;
+ }
+
+ public bool TryFindNextAndSelect(SearchContext searchContext, bool stopAtEof, out bool regexError)
+ {
+ regexError = false;
+
+ if (string.IsNullOrEmpty(searchContext.SearchText))
+ {
+ return false;
+ }
+
+ var document = GetText();
+
+ if (Document.Selection.EndPosition > document.Length) Document.Selection.EndPosition = document.Length;
+
+ if (searchContext.UseRegex)
+ {
+ return TryFindNextAndSelectUsingRegex(document, searchContext, stopAtEof, out regexError);
+ }
+ else
+ {
+ StringComparison comparison = searchContext.MatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+
+ var index = searchContext.MatchWholeWord
+ ? document.IndexOfWholeWord(searchContext.SearchText, Document.Selection.EndPosition, comparison)
+ : document.IndexOf(searchContext.SearchText, Document.Selection.EndPosition, comparison);
+
+ if (index != -1)
+ {
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + searchContext.SearchText.Length;
+ }
+ else
+ {
+ if (!stopAtEof)
+ {
+ index = searchContext.MatchWholeWord
+ ? document.IndexOfWholeWord(searchContext.SearchText, 0, comparison)
+ : document.IndexOf(searchContext.SearchText, 0, comparison);
+
+ if (index != -1)
+ {
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + searchContext.SearchText.Length;
+ }
+ }
+ }
+
+ if (index == -1)
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ public bool TryFindPreviousAndSelect(SearchContext searchContext, bool stopAtBof, out bool regexError)
+ {
+ regexError = false;
+
+ if (string.IsNullOrEmpty(searchContext.SearchText))
+ {
+ return false;
+ }
+
+ var document = GetText();
+
+ if (searchContext.UseRegex)
+ {
+ return TryFindPreviousAndSelectUsingRegex(document, searchContext, out regexError, stopAtBof);
+ }
+ else
+ {
+ var searchIndex = Document.Selection.StartPosition - 1;
+ if (searchIndex < 0)
+ {
+ if (stopAtBof)
+ {
+ return false;
+ }
+ else
+ {
+ searchIndex = document.Length - 1;
+ }
+ }
+
+ StringComparison comparison = searchContext.MatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+
+ var index = searchContext.MatchWholeWord
+ ? document.LastIndexOfWholeWord(searchContext.SearchText, searchIndex, comparison)
+ : document.LastIndexOf(searchContext.SearchText, searchIndex, comparison);
+
+ if (index != -1)
+ {
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + searchContext.SearchText.Length;
+ }
+ else
+ {
+ index = searchContext.MatchWholeWord
+ ? document.LastIndexOfWholeWord(searchContext.SearchText, document.Length - 1, comparison)
+ : document.LastIndexOf(searchContext.SearchText, document.Length - 1, comparison);
+
+ if (index != -1)
+ {
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + searchContext.SearchText.Length;
+ }
+ }
+
+ if (index == -1)
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ public bool TryFindNextAndReplace(SearchContext searchContext, string replaceText, out bool regexError)
+ {
+ Document.Selection.EndPosition = Document.Selection.StartPosition;
+
+ if (TryFindNextAndSelect(searchContext, stopAtEof: true, out regexError))
+ {
+ if (searchContext.UseRegex)
+ {
+ replaceText = ApplyTabAndLineEndingFix(replaceText);
+ }
+
+ Document.Selection.SetText(TextSetOptions.None, replaceText);
+ TryFindNextAndSelect(searchContext, stopAtEof: true, out _);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryFindPreviousAndReplace(SearchContext searchContext, string replaceText, out bool regexError)
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+
+ if (TryFindPreviousAndSelect(searchContext, stopAtBof: true, out regexError))
+ {
+ if (searchContext.UseRegex)
+ {
+ replaceText = ApplyTabAndLineEndingFix(replaceText);
+ }
+
+ Document.Selection.SetText(TextSetOptions.None, replaceText);
+ TryFindPreviousAndSelect(searchContext, stopAtBof: true, out _);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryFindAndReplaceAll(SearchContext searchContext, string replaceText, out bool regexError)
+ {
+ regexError = false;
+ var found = false;
+ var text = GetText();
+
+ if (string.IsNullOrEmpty(searchContext.SearchText))
+ {
+ return false;
+ }
+
+ if (searchContext.UseRegex)
+ {
+ found = TryFindAndReplaceAllUsingRegex(text, searchContext, replaceText, out regexError, out var output);
+ if (found) text = output;
+ }
+ else
+ {
+ var pos = 0;
+ var searchTextLength = searchContext.SearchText.Length;
+ var replaceTextLength = replaceText.Length;
+
+ StringComparison comparison = searchContext.MatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+
+ pos = searchContext.MatchWholeWord
+ ? text.IndexOfWholeWord(searchContext.SearchText, pos, comparison)
+ : text.IndexOf(searchContext.SearchText, pos, comparison);
+
+ while (pos != -1)
+ {
+ found = true;
+ text = text.Remove(pos, searchTextLength).Insert(pos, replaceText);
+ pos += replaceTextLength;
+ pos = searchContext.MatchWholeWord
+ ? text.IndexOfWholeWord(searchContext.SearchText, pos, comparison)
+ : text.IndexOf(searchContext.SearchText, pos, comparison);
+ }
+ }
+
+ if (found)
+ {
+ SetText(text);
+ Document.Selection.StartPosition = int.MaxValue;
+ Document.Selection.EndPosition = Document.Selection.StartPosition;
+ }
+
+ return found;
+ }
+
+ private bool TryFindNextAndSelectUsingRegex(string content, SearchContext searchContext, bool stopAtEof,
+ out bool regexError)
+ {
+ try
+ {
+ regexError = false;
+ content = content.Replace(RichEditBoxDefaultLineEnding, RegexDefaultLineEnding);
+ Regex regex = new Regex(searchContext.SearchText,
+ RegexOptions.Multiline | (searchContext.MatchCase ? RegexOptions.None : RegexOptions.IgnoreCase));
+
+ var match = regex.Match(content, Document.Selection.EndPosition);
+
+ if (!match.Success && !stopAtEof)
+ {
+ match = regex.Match(content, 0);
+ }
+
+ if (match.Success)
+ {
+ var index = match.Index;
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + match.Length;
+ return true;
+ }
+ else
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ return false;
+ }
+ }
+ catch (Exception)
+ {
+ regexError = true;
+ return false;
+ }
+ }
+
+ private bool TryFindPreviousAndSelectUsingRegex(string content, SearchContext searchContext, out bool regexError, bool stopAtBof)
+ {
+ try
+ {
+ regexError = false;
+ content = content.Replace(RichEditBoxDefaultLineEnding, RegexDefaultLineEnding);
+ Regex regex = new Regex(searchContext.SearchText,
+ RegexOptions.RightToLeft | RegexOptions.Multiline |
+ (searchContext.MatchCase ? RegexOptions.None : RegexOptions.IgnoreCase));
+
+ var match = regex.Match(content, Document.Selection.StartPosition);
+
+ if (!match.Success && !stopAtBof)
+ {
+ match = regex.Match(content, content.Length);
+ }
+
+ if (match.Success)
+ {
+ var index = match.Index;
+ Document.Selection.StartPosition = index;
+ Document.Selection.EndPosition = index + match.Length;
+ return true;
+ }
+ else
+ {
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ return false;
+ }
+ }
+ catch (Exception)
+ {
+ regexError = true;
+ return false;
+ }
+ }
+
+ private static bool TryFindAndReplaceAllUsingRegex(string content, SearchContext searchContext, string replaceText, out bool regexError, out string output)
+ {
+ regexError = false;
+ output = string.Empty;
+ content = content.Replace(RichEditBoxDefaultLineEnding, RegexDefaultLineEnding);
+
+ try
+ {
+ Regex regex = new Regex(searchContext.SearchText, RegexOptions.Multiline | (searchContext.MatchCase ? RegexOptions.None : RegexOptions.IgnoreCase));
+
+ if (regex.IsMatch(content))
+ {
+ if (searchContext.UseRegex)
+ {
+ replaceText = ApplyTabAndLineEndingFix(replaceText);
+ }
+
+ output = regex.Replace(content, replaceText).Replace(RegexDefaultLineEnding, RichEditBoxDefaultLineEnding);
+ return true;
+ }
+ }
+ catch (Exception)
+ {
+ regexError = true;
+ return false;
+ }
+
+ return false;
+ }
+
+ private static string ApplyTabAndLineEndingFix(string text)
+ {
+ return text.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.FontSize.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.FontSize.cs
new file mode 100644
index 0000000..a2146a2
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.FontSize.cs
@@ -0,0 +1,48 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using Notepads.Services;
+
+ public partial class TextEditorCore
+ {
+ private void IncreaseFontSize(double delta)
+ {
+ if (_fontZoomFactor < _maximumZoomFactor)
+ {
+ if (_fontZoomFactor % 10 > 0)
+ {
+ SetFontZoomFactor(Math.Ceiling(_fontZoomFactor / 10) * 10);
+ }
+ else
+ {
+ FontSize += delta * AppSettingsService.EditorFontSize;
+ }
+ }
+ }
+
+ private void DecreaseFontSize(double delta)
+ {
+ if (_fontZoomFactor > _minimumZoomFactor)
+ {
+ if (_fontZoomFactor % 10 > 0)
+ {
+ SetFontZoomFactor(Math.Floor(_fontZoomFactor / 10) * 10);
+ }
+ else
+ {
+ FontSize -= delta * AppSettingsService.EditorFontSize;
+ }
+ }
+ }
+
+ private void ResetFontSizeToDefault()
+ {
+ FontSize = AppSettingsService.EditorFontSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.Indentation.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.Indentation.cs
new file mode 100644
index 0000000..8877dc4
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.Indentation.cs
@@ -0,0 +1,215 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System.Text;
+ using Windows.UI.Text;
+
+ public partial class TextEditorCore
+ {
+ ///
+ /// Add indentation based on current selection
+ ///
+ ///
+ /// indent == -1 meaning '/t' should be used, otherwise it equals to number of spaces
+ ///
+ private void AddIndentation(int indent)
+ {
+ GetTextSelectionPosition(out var start, out var end);
+ GetLineColumnSelection(out var startLine,
+ out var endLine,
+ out var startColumn,
+ out var endColumn,
+ out _,
+ out _);
+
+ var document = GetText();
+ var lines = GetDocumentLinesCache();
+
+ var tabStr = indent == -1 ? "\t" : new string(' ', indent);
+
+ // Handle single line selection scenario where part of the line is selected
+ if (startLine == endLine)
+ {
+ Document.Selection.TypeText(tabStr);
+ Document.Selection.StartPosition = Document.Selection.EndPosition;
+ return;
+ }
+
+ var startLineInitialIndex = start - startColumn + 1;
+ var endLineFinalIndex = end - endColumn + lines[endLine - 1].Length + 1;
+ if (endLineFinalIndex > document.Length) endLineFinalIndex = document.Length;
+
+ var indentAmount = indent == -1 ? 1 : indent;
+ start += indentAmount;
+
+ var indentedStringBuilder = new StringBuilder();
+ for (var i = startLine - 1; i < endLine; i++)
+ {
+ indentedStringBuilder.Append(string.Concat(tabStr, lines[i],
+ (i < endLine - 1 || (endLine == lines.Length - 2 && string.IsNullOrEmpty(lines[endLine])))
+ ? RichEditBoxDefaultLineEnding.ToString()
+ : string.Empty));
+ end += indentAmount;
+ }
+
+ if (string.Equals(document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex),
+ indentedStringBuilder.ToString())) return;
+
+ if (Document.Selection.Text.EndsWith(RichEditBoxDefaultLineEnding) && endLineFinalIndex < document.Length)
+ {
+ indentedStringBuilder.Append(RichEditBoxDefaultLineEnding);
+ if (string.Equals(document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex),
+ indentedStringBuilder.ToString())) return;
+ }
+
+ Document.Selection.GetRect(Windows.UI.Text.PointOptions.Transform, out Windows.Foundation.Rect rect,
+ out var _);
+ GetScrollViewerPosition(out var horizontalOffset, out var verticalOffset);
+ var wasSelectionInView = IsSelectionRectInView(rect, horizontalOffset, verticalOffset);
+
+ var newContent = document.Remove(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex)
+ .Insert(startLineInitialIndex, indentedStringBuilder.ToString());
+
+ Document.SetText(TextSetOptions.None, newContent);
+ Document.Selection.SetRange(start, end);
+
+ // After SetText() and SetRange(), RichEdit will scroll selection into view and change scroll viewer's position even if it was already in the viewport
+ // It is better to keep its original scroll position after changing the indent in this case
+ if (wasSelectionInView)
+ {
+ _contentScrollViewer.ChangeView(
+ horizontalOffset,
+ verticalOffset,
+ zoomFactor: null,
+ disableAnimation: true);
+ }
+ }
+
+ ///
+ /// Remove indentation based on current selection
+ ///
+ ///
+ /// indent == -1 meaning '/t' should be used, otherwise it equals to number of spaces
+ ///
+ private void RemoveIndentation(int indent)
+ {
+ GetTextSelectionPosition(out var start, out var end);
+ GetLineColumnSelection(out var startLine,
+ out var endLine,
+ out var startColumn,
+ out var endColumn,
+ out _,
+ out _);
+
+ var document = GetText();
+ var lines = GetDocumentLinesCache();
+
+ var startLineInitialIndex = start - startColumn + 1;
+ var endLineFinalIndex = end - endColumn + lines[endLine - 1].Length + 1;
+ if (endLineFinalIndex > document.Length) endLineFinalIndex = document.Length;
+
+ if (startLineInitialIndex == endLineFinalIndex) return;
+
+ var indentedStringBuilder = new StringBuilder();
+ for (var i = startLine - 1; i < endLine; i++)
+ {
+ var lineTrailingString = (i < endLine - 1 || (endLine == lines.Length - 2 && string.IsNullOrEmpty(lines[endLine])))
+ ? RichEditBoxDefaultLineEnding.ToString()
+ : string.Empty;
+
+ if (lines[i].StartsWith('\t'))
+ {
+ indentedStringBuilder.Append(lines[i].Remove(0, 1) + lineTrailingString);
+ end--;
+ }
+ else
+ {
+ var spaceCount = 0;
+ var indentAmount = indent == -1 ? 4 : indent;
+
+ for (var charIndex = 0;
+ charIndex < lines[i].Length && lines[i][charIndex] == ' ';
+ charIndex++)
+ {
+ spaceCount++;
+ }
+
+ if (spaceCount == 0)
+ {
+ indentedStringBuilder.Append(lines[i] + lineTrailingString);
+ continue;
+ }
+
+ var insufficientSpace = spaceCount % indentAmount;
+
+ if (insufficientSpace > 0)
+ {
+ indentedStringBuilder.Append(lines[i].Remove(0, insufficientSpace) +
+ lineTrailingString);
+ end -= insufficientSpace;
+ }
+ else
+ {
+ indentedStringBuilder.Append(lines[i].Remove(0, indentAmount) +
+ lineTrailingString);
+ end -= indentAmount;
+ }
+ }
+
+ if (i == startLine - 1)
+ {
+ if (startLine == endLine)
+ {
+ start -= lines[i].Length - indentedStringBuilder.Length;
+ }
+ else
+ {
+ start -= lines[i].Length - indentedStringBuilder.Length + 1;
+ }
+
+ if (start < startLineInitialIndex)
+ {
+ if (end == start) end = startLineInitialIndex;
+ start = startLineInitialIndex;
+ }
+ }
+ }
+
+ if (string.Equals(document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex),
+ indentedStringBuilder.ToString())) return;
+
+ if (Document.Selection.Text.EndsWith(RichEditBoxDefaultLineEnding) && endLineFinalIndex < document.Length)
+ {
+ indentedStringBuilder.Append(RichEditBoxDefaultLineEnding);
+ if (string.Equals(document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex),
+ indentedStringBuilder.ToString())) return;
+ }
+
+ Document.Selection.GetRect(Windows.UI.Text.PointOptions.Transform, out Windows.Foundation.Rect rect,
+ out var _);
+ GetScrollViewerPosition(out var horizontalOffset, out var verticalOffset);
+ var wasSelectionInView = IsSelectionRectInView(rect, horizontalOffset, verticalOffset);
+
+ var newContent = document.Remove(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex)
+ .Insert(startLineInitialIndex, indentedStringBuilder.ToString());
+
+ Document.SetText(TextSetOptions.None, newContent);
+ Document.Selection.SetRange(start, end);
+
+ // After SetText() and SetRange(), RichEdit will scroll selection into view and change scroll viewer's position even if it was already in the viewport
+ // It is better to keep its original scroll position after changing the indent in this case
+ if (wasSelectionInView)
+ {
+ _contentScrollViewer.ChangeView(
+ horizontalOffset,
+ verticalOffset,
+ zoomFactor: null,
+ disableAnimation: true);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.JoinText.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.JoinText.cs
new file mode 100644
index 0000000..0f8617c
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.JoinText.cs
@@ -0,0 +1,68 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.UI.Text;
+ using Notepads.Services;
+
+ public partial class TextEditorCore
+ {
+ ///
+ /// Join selected lines into one line using space char as the separator
+ /// Example:
+ /// ...
+ /// aaa
+ /// bbb
+ /// ccc
+ /// ...
+ /// ->
+ /// aaa bbb ccc
+ ///
+ private void JoinText()
+ {
+ try
+ {
+ GetTextSelectionPosition(out var start, out var end);
+ GetLineColumnSelection(out var startLine,
+ out var endLine,
+ out var startColumn,
+ out var endColumn,
+ out _,
+ out _);
+
+ // Does not make any sense to join 1 line
+ if (startLine == endLine) return;
+
+ var document = GetText();
+ var lines = GetDocumentLinesCache();
+
+ var startLineInitialIndex = start - startColumn + 1;
+ var endLineFinalIndex = end - endColumn + lines[endLine - 1].Length + 1;
+ if (endLineFinalIndex > document.Length) endLineFinalIndex = document.Length;
+
+ if (document[endLineFinalIndex - 1] == RichEditBoxDefaultLineEnding) endLineFinalIndex--;
+ if (endLineFinalIndex - startLineInitialIndex <= 0) return;
+
+ var selectedLines = document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex);
+ var joinedLines = selectedLines.Replace(RichEditBoxDefaultLineEnding, ' ');
+
+ var newContent = document.Remove(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex)
+ .Insert(startLineInitialIndex, joinedLines);
+
+ Document.SetText(TextSetOptions.None, newContent);
+ Document.Selection.SetRange(start, end);
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(TextEditorCore)}] Failed to join text: {ex}");
+ AnalyticsService.TrackEvent("TextEditorCore_FailedToJoinText",
+ new Dictionary { { "Exception", ex.ToString() } });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineHighlighter.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineHighlighter.cs
new file mode 100644
index 0000000..69d7458
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineHighlighter.cs
@@ -0,0 +1,76 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using Windows.UI.Xaml;
+
+ public partial class TextEditorCore
+ {
+ private bool _displayLineHighlighter;
+
+ public bool DisplayLineHighlighter
+ {
+ get => _displayLineHighlighter;
+ set
+ {
+ if (_displayLineHighlighter != value)
+ {
+ _displayLineHighlighter = value;
+ UpdateLineHighlighterAndIndicator();
+ }
+ }
+ }
+
+ private void UpdateLineHighlighterAndIndicator()
+ {
+ if (!_loaded) return;
+
+ if (!DisplayLineHighlighter && !DisplayLineNumbers)
+ {
+ _lineHighlighter.Visibility = Visibility.Collapsed;
+ _lineIndicator.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ Document.Selection.GetRect(Windows.UI.Text.PointOptions.ClientCoordinates,
+ out Windows.Foundation.Rect selectionRect, out var _);
+
+ var singleLineHeight = GetSingleLineHeight();
+ var thickness = new Thickness(0.08 * singleLineHeight);
+ var height = selectionRect.Height;
+
+ // Just to make sure height is a positive number and not smaller than single line height
+ if (height < singleLineHeight) height = singleLineHeight;
+
+ // Show line highlighter rect when it is enabled when selection is single line only
+ if (DisplayLineHighlighter && height < singleLineHeight * 1.5f)
+ {
+ _lineHighlighter.Height = height;
+ _lineHighlighter.Margin = new Thickness(0, selectionRect.Y + Padding.Top, 0, 0);
+ _lineHighlighter.Width = Math.Clamp(_rootGrid.ActualWidth, 0, Double.PositiveInfinity);
+
+ _lineHighlighter.Visibility = Visibility.Visible;
+ _lineIndicator.Visibility = Visibility.Collapsed;
+ }
+ else if (DisplayLineNumbers) // Show line indicator when line highlighter is disabled but line numbers are enabled
+ {
+ _lineIndicator.Height = height;
+ _lineIndicator.Margin = new Thickness(0, selectionRect.Y + Padding.Top, 0, 0);
+ _lineIndicator.BorderThickness = thickness;
+ _lineIndicator.Width = 0.1 * singleLineHeight;
+
+ _lineIndicator.Visibility = Visibility.Visible;
+ _lineHighlighter.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _lineIndicator.Visibility = Visibility.Collapsed;
+ _lineHighlighter.Visibility = Visibility.Collapsed;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineNumbers.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineNumbers.cs
new file mode 100644
index 0000000..7f3d6e9
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.LineNumbers.cs
@@ -0,0 +1,246 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Collections.Generic;
+ using Windows.Foundation;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Media;
+ using Notepads.Utilities;
+ using Microsoft.Toolkit.Uwp.Helpers;
+
+ public partial class TextEditorCore
+ {
+ private bool _displayLineNumbers;
+
+ public bool DisplayLineNumbers
+ {
+ get => _displayLineNumbers;
+ set
+ {
+ if (_displayLineNumbers != value)
+ {
+ _displayLineNumbers = value;
+
+ if (value)
+ {
+ ShowLineNumbers();
+ }
+ else
+ {
+ HideLineNumbers();
+ }
+ }
+ }
+ }
+
+ private readonly IList _renderedLineNumberBlocks = new List();
+ private readonly Dictionary _minRequisiteIntegerTextRenderingWidthCache = new Dictionary();
+ private readonly SolidColorBrush _lineNumberDarkModeForegroundBrush = new SolidColorBrush("#99EEEEEE".ToColor());
+ private readonly SolidColorBrush _lineNumberLightModeForegroundBrush = new SolidColorBrush("#99000000".ToColor());
+
+ private void ShowLineNumbers()
+ {
+ if (!_loaded) return;
+
+ ResetLineNumberCanvasClipping();
+ UpdateLineNumbersRendering();
+
+ // Call UpdateLineHighlighterAndIndicator to adjust it's state
+ UpdateLineHighlighterAndIndicator();
+ }
+
+ private void HideLineNumbers()
+ {
+ if (!_loaded) return;
+
+ foreach (var lineNumberBlock in _renderedLineNumberBlocks)
+ {
+ lineNumberBlock.Visibility = Visibility.Collapsed;
+ }
+
+ _lineNumberGrid.BorderThickness = new Thickness(0, 0, 0, 0);
+ _lineNumberGrid.Margin = new Thickness(0, 0, 0, 0);
+ _lineNumberGrid.Width = .0f;
+
+ // Call UpdateLineHighlighterAndIndicator to adjust it's state
+ // Since when line highlighter is disabled, we still show the line indicator when line numbers are showing
+ UpdateLineHighlighterAndIndicator();
+ }
+
+ private void OnLineNumberGridSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ ResetLineNumberCanvasClipping();
+ }
+
+ private void ResetLineNumberCanvasClipping()
+ {
+ if (!_loaded || !DisplayLineNumbers) return;
+
+ _lineNumberGrid.Margin = new Thickness(0, 0, (-1 * Padding.Left) + 1, 0);
+ _lineNumberGrid.Clip = new RectangleGeometry
+ {
+ Rect = new Rect(
+ 0,
+ Padding.Top,
+ _lineNumberGrid.ActualWidth,
+ Math.Clamp(_lineNumberGrid.ActualHeight - (Padding.Top + Padding.Bottom), .0f, Double.PositiveInfinity))
+ };
+ }
+
+ private void UpdateLineNumbersRendering()
+ {
+ if (!_loaded || !DisplayLineNumbers) return;
+
+ var startRange = Document.GetRangeFromPoint(
+ new Point(_contentScrollViewer.HorizontalOffset, _contentScrollViewer.VerticalOffset),
+ PointOptions.ClientCoordinates);
+
+ var endRange = Document.GetRangeFromPoint(
+ new Point(_contentScrollViewer.HorizontalOffset + _contentScrollViewer.ViewportWidth,
+ _contentScrollViewer.VerticalOffset + _contentScrollViewer.ViewportHeight),
+ PointOptions.ClientCoordinates);
+
+ var document = GetDocumentLinesCache();
+
+ Dictionary lineNumberTextRenderingPositions = CalculateLineNumberTextRenderingPositions(document, startRange, endRange);
+
+ var minLineNumberTextRenderingWidth = CalculateMinimumRequisiteIntegerTextRenderingWidth(FontFamily,
+ FontSize, (document.Length - 1).ToString().Length);
+
+ RenderLineNumbersInternal(lineNumberTextRenderingPositions, minLineNumberTextRenderingWidth);
+ }
+
+ ///
+ /// Get minimum rendering width needed for displaying number text with certain length.
+ /// Take length of 3 as example, it is going to iterate thru all possible combinations like:
+ /// 111, 222, 333, 444 ... 999 to get minimum rendering length needed to display all of them (the largest width is the min here).
+ /// For mono font text, the width is always the same for same length but for non-mono font text, it depends.
+ /// Thus we need to calculate here to determine width needed for rendering integer number only text.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private double CalculateMinimumRequisiteIntegerTextRenderingWidth(FontFamily fontFamily, double fontSize, int numberTextLength)
+ {
+ var cacheKey = $"{fontFamily.Source}-{(int)fontSize}-{numberTextLength}";
+
+ if (_minRequisiteIntegerTextRenderingWidthCache.ContainsKey(cacheKey))
+ {
+ return _minRequisiteIntegerTextRenderingWidthCache[cacheKey];
+ }
+
+ double minRequisiteWidth = 0;
+
+ for (int i = 0; i < 10; i++)
+ {
+ var str = new string((char)('0' + i), numberTextLength);
+ var width = FontUtility.GetTextSize(fontFamily, fontSize, str).Width;
+ if (width > minRequisiteWidth)
+ {
+ minRequisiteWidth = width;
+ }
+ }
+
+ _minRequisiteIntegerTextRenderingWidthCache[cacheKey] = minRequisiteWidth;
+ return minRequisiteWidth;
+ }
+
+ private Dictionary CalculateLineNumberTextRenderingPositions(string[] lines, ITextRange startRange, ITextRange endRange)
+ {
+ var offset = 0;
+ var lineRects = new Dictionary(); // 1 - based
+
+ for (int i = 0; i < lines.Length - 1; i++)
+ {
+ var line = lines[i];
+
+ // Use "offset + line.Length + 1" instead of just "offset" here is to capture the line right above the viewport
+ if (offset + line.Length + 1 >= startRange.StartPosition && offset <= endRange.EndPosition)
+ {
+ Document.GetRange(offset, offset + line.Length)
+ .GetRect(PointOptions.ClientCoordinates, out var rect, out _);
+
+ lineRects[i + 1] = rect;
+ }
+ else if (offset > endRange.EndPosition)
+ {
+ break;
+ }
+
+ offset += line.Length + 1; // 1 for line ending: '\r'
+ }
+
+ return lineRects;
+ }
+
+ private void RenderLineNumbersInternal(Dictionary lineNumberTextRenderingPositions, double minLineNumberTextRenderingWidth)
+ {
+ var padding = FontSize / 2;
+ var lineNumberPadding = new Thickness(padding, 2, padding + 2, 2);
+ var lineHeight = GetSingleLineHeight();
+ var lineNumberTextBlockHeight = lineHeight + Padding.Top + lineNumberPadding.Top;
+ var lineNumberForeground = (ActualTheme == ElementTheme.Dark)
+ ? _lineNumberDarkModeForegroundBrush
+ : _lineNumberLightModeForegroundBrush;
+
+ var numOfReusableLineNumberBlocks = _renderedLineNumberBlocks.Count;
+
+ foreach (var (lineNumber, rect) in lineNumberTextRenderingPositions)
+ {
+ var margin = new Thickness(lineNumberPadding.Left,
+ rect.Top + lineNumberPadding.Top + Padding.Top,
+ lineNumberPadding.Right,
+ lineNumberPadding.Bottom);
+
+ // Re-use already rendered line number blocks
+ if (numOfReusableLineNumberBlocks > 0)
+ {
+ var index = numOfReusableLineNumberBlocks - 1;
+ _renderedLineNumberBlocks[index].Text = lineNumber.ToString();
+ _renderedLineNumberBlocks[index].Margin = margin;
+ _renderedLineNumberBlocks[index].Height = lineNumberTextBlockHeight;
+ _renderedLineNumberBlocks[index].Width = minLineNumberTextRenderingWidth;
+ _renderedLineNumberBlocks[index].Visibility = Visibility.Visible;
+ _renderedLineNumberBlocks[index].Foreground = lineNumberForeground;
+
+ numOfReusableLineNumberBlocks--;
+ }
+ else // Render new line number block when there is nothing to re-use
+ {
+ var lineNumberBlock = new TextBlock()
+ {
+ Text = lineNumber.ToString(),
+ Height = lineNumberTextBlockHeight,
+ Width = minLineNumberTextRenderingWidth,
+ Margin = margin,
+ TextAlignment = TextAlignment.Right,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ HorizontalTextAlignment = TextAlignment.Right,
+ Foreground = lineNumberForeground
+ };
+
+ _lineNumberCanvas.Children.Add(lineNumberBlock);
+ _renderedLineNumberBlocks.Add(lineNumberBlock);
+ }
+ }
+
+ // Hide all un-used rendered line number block to avoid rendering collision from happening
+ for (int i = 0; i < numOfReusableLineNumberBlocks; i++)
+ {
+ _renderedLineNumberBlocks[i].Visibility = Visibility.Collapsed;
+ }
+
+ _lineNumberGrid.BorderThickness = new Thickness(0, 0, 0.08 * lineHeight, 0);
+ _lineNumberGrid.Width = lineNumberPadding.Left + minLineNumberTextRenderingWidth + lineNumberPadding.Right;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.MoveText.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.MoveText.cs
new file mode 100644
index 0000000..a6ed172
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.MoveText.cs
@@ -0,0 +1,243 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using Windows.UI.Text;
+
+ public partial class TextEditorCore
+ {
+ private void MoveTextUp()
+ {
+ GetLineColumnSelection(out var startLine,
+ out var endLine,
+ out var startColumn,
+ out var endColumn,
+ out _,
+ out _);
+
+ if (startLine == 1) return;
+
+ GetTextSelectionPosition(out var start, out var end);
+
+ var document = GetText();
+ var lines = GetDocumentLinesCache();
+
+ var startLineInitialIndex = start - startColumn;
+ var endLineFinalIndex = end - endColumn + lines[endLine - 1].Length +
+ (end > start && (end > document.Length || document[end - 1].Equals(RichEditBoxDefaultLineEnding)) ? 0 : 1);
+
+ MoveLines(document, startLineInitialIndex, endLineFinalIndex, start, end, -lines[startLine - 2].Length - 1);
+ }
+
+ private void MoveTextDown()
+ {
+ GetLineColumnSelection(out _,
+ out var endLine,
+ out var startColumn,
+ out var endColumn,
+ out _,
+ out var lineCount);
+
+ if (endLine == lineCount) return;
+
+ GetTextSelectionPosition(out var start, out var end);
+
+ var document = GetText();
+ var lines = GetDocumentLinesCache();
+
+ var startLineInitialIndex = start - startColumn + 1;
+ var endLineFinalIndex = end - endColumn + lines[endLine - 1].Length +
+ (end > start && document[end - 1].Equals(RichEditBoxDefaultLineEnding) ? 1 : 2);
+
+ MoveLines(document, startLineInitialIndex, endLineFinalIndex, start, end, lines[endLine].Length + 1);
+ }
+
+ private void MoveLines(string document,
+ int startLineInitialIndex, int endLineFinalIndex,
+ int selectionStart, int selectionEnd, int selectionMoveAmount)
+ {
+ var insertIndex = startLineInitialIndex + selectionMoveAmount;
+ var movingLines = document.Substring(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex);
+ var remainingContent = document.Remove(startLineInitialIndex, endLineFinalIndex - startLineInitialIndex);
+
+ if (insertIndex < 0)
+ {
+ insertIndex = 0;
+ remainingContent = RichEditBoxDefaultLineEnding + remainingContent;
+ movingLines = movingLines.Remove(0, 1);
+ }
+ else if (insertIndex >= remainingContent.Length)
+ {
+ remainingContent += RichEditBoxDefaultLineEnding;
+ movingLines = movingLines.Remove(movingLines.Length - 1);
+ }
+
+ Document.Selection.GetRect(PointOptions.Transform, out Windows.Foundation.Rect rect, out var _);
+ GetScrollViewerPosition(out var horizontalOffset, out var verticalOffset);
+ var wasSelectionInView = IsSelectionRectInView(rect, horizontalOffset, verticalOffset);
+
+ var newContent = remainingContent.Insert(insertIndex, movingLines);
+ selectionStart += selectionMoveAmount;
+ selectionEnd += selectionMoveAmount;
+ if (selectionStart < 0) selectionStart = 0;
+
+ Document.SetText(TextSetOptions.None, newContent);
+ Document.Selection.SetRange(selectionStart, selectionEnd);
+
+ // After SetText() and SetRange(), RichEdit will scroll selection into view
+ // and change scroll viewer's position even if it was already in the viewport
+ // It is better to keep its original scroll position after changing the texts' position in this case
+ if (wasSelectionInView)
+ {
+ _contentScrollViewer.ChangeView(
+ horizontalOffset,
+ verticalOffset,
+ zoomFactor: null,
+ disableAnimation: true);
+ }
+ }
+
+ private void MoveTextLeft()
+ {
+ GetTextSelectionPosition(out var start, out var end);
+
+ var document = GetText();
+
+ if (start == 0) return;
+
+ var movingWordIndexData = GetMovingWordsIndexData(document, start, end);
+ var startIndex = movingWordIndexData.Item1;
+ var endIndex = movingWordIndexData.Item2;
+ if (startIndex <= 0 || startIndex >= endIndex) return;
+ end = movingWordIndexData.Item3;
+
+ var replacedWordEndIndex = startIndex;
+ while (replacedWordEndIndex > 0)
+ {
+ replacedWordEndIndex--;
+ if (char.IsLetterOrDigit(document[replacedWordEndIndex]))
+ {
+ replacedWordEndIndex++;
+ break;
+ }
+ }
+
+ var replacedWordStartIndex = replacedWordEndIndex;
+ while (replacedWordStartIndex > 0)
+ {
+ replacedWordStartIndex--;
+ if (!char.IsLetterOrDigit(document[replacedWordStartIndex]))
+ {
+ replacedWordStartIndex++;
+ break;
+ }
+ }
+
+ MoveWords(document, replacedWordStartIndex, replacedWordEndIndex, startIndex, endIndex, start, end, replacedWordStartIndex - startIndex);
+ }
+
+ private void MoveTextRight()
+ {
+ GetTextSelectionPosition(out var start, out var end);
+
+ var document = GetText();
+
+ if (end >= document.Length) return;
+
+ var movingWordIndexData = GetMovingWordsIndexData(document, start, end);
+ var startIndex = movingWordIndexData.Item1;
+ var endIndex = movingWordIndexData.Item2;
+ if (endIndex <= startIndex || endIndex >= document.Length) return;
+
+ var replacedWordStartIndex = endIndex;
+ for (; replacedWordStartIndex < document.Length; replacedWordStartIndex++)
+ {
+ if (char.IsLetterOrDigit(document[replacedWordStartIndex]))
+ {
+ break;
+ }
+ }
+
+ var replacedWordEndIndex = replacedWordStartIndex;
+ for (; replacedWordEndIndex < document.Length; replacedWordEndIndex++)
+ {
+ if (!char.IsLetterOrDigit(document[replacedWordEndIndex]))
+ {
+ break;
+ }
+ }
+
+ MoveWords(document, startIndex, endIndex, replacedWordStartIndex, replacedWordEndIndex, start, end, replacedWordEndIndex - endIndex);
+ }
+
+ private Tuple GetMovingWordsIndexData(string document, int selectionStart, int selectionEnd)
+ {
+ var startIndex = selectionStart;
+ if (selectionEnd == selectionStart || char.IsLetterOrDigit(document[selectionStart]))
+ {
+ while (startIndex > 0)
+ {
+ startIndex--;
+ if (!char.IsLetterOrDigit(document[startIndex]))
+ {
+ startIndex++;
+ break;
+ }
+ }
+ }
+
+ if (selectionEnd > document.Length) selectionEnd = document.Length;
+ var endIndex = selectionEnd;
+ if (selectionEnd == selectionStart || char.IsLetterOrDigit(document[selectionEnd - 1]))
+ {
+ while (endIndex < document.Length)
+ {
+ endIndex++;
+ if (!char.IsLetterOrDigit(document[endIndex - 1]))
+ {
+ endIndex--;
+ break;
+ }
+ }
+ }
+
+ return Tuple.Create(startIndex, endIndex, selectionEnd);
+ }
+
+ private void MoveWords(string document,
+ int leftWordsStartIndex, int leftWordsEndIndex,
+ int rightWordsStartIndex, int rightWordsEndIndex,
+ int selectionStart, int selectionEnd, int selectionMoveAmount)
+ {
+ Document.Selection.GetRect(PointOptions.Transform, out Windows.Foundation.Rect rect, out var _);
+ GetScrollViewerPosition(out var horizontalOffset, out var verticalOffset);
+ var wasSelectionInView = IsSelectionRectInView(rect, horizontalOffset, verticalOffset);
+
+ var leftWords = document.Substring(leftWordsStartIndex, leftWordsEndIndex - leftWordsStartIndex);
+ var rightWords = document.Substring(rightWordsStartIndex, rightWordsEndIndex - rightWordsStartIndex);
+ document = document.Remove(rightWordsStartIndex, rightWordsEndIndex - rightWordsStartIndex).Insert(rightWordsStartIndex, leftWords)
+ .Remove(leftWordsStartIndex, leftWordsEndIndex - leftWordsStartIndex).Insert(leftWordsStartIndex, rightWords);
+ selectionStart += selectionMoveAmount;
+ selectionEnd += selectionMoveAmount;
+
+ Document.SetText(TextSetOptions.None, document);
+ Document.Selection.SetRange(selectionStart, selectionEnd);
+
+ // After SetText() and SetRange(), RichEdit will scroll selection into view
+ // and change scroll viewer's position even if it was already in the viewport
+ // It is better to keep its original vertical scroll position after changing the texts' position in this case
+ if (wasSelectionInView)
+ {
+ _contentScrollViewer.ChangeView(
+ null,
+ verticalOffset,
+ zoomFactor: null,
+ disableAnimation: true);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.WebSearch.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.WebSearch.cs
new file mode 100644
index 0000000..a1bd121
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.WebSearch.cs
@@ -0,0 +1,49 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using Windows.System;
+ using Notepads.Services;
+ using Notepads.Utilities;
+ using System.Threading.Tasks;
+
+ public partial class TextEditorCore
+ {
+ public async Task SearchInWebAsync()
+ {
+ try
+ {
+ if (Document.Selection.Length == 0)
+ {
+ return;
+ }
+
+ var selectedText = Document.Selection.Text.Trim();
+
+ // The maximum length of a URL in the address bar is 2048 characters
+ // Let's take 2000 here to make sure we are not exceeding the limit
+ // Otherwise we will see "Invalid URI: The uri string is too long" exception
+ var searchString = selectedText.Length <= 2000 ? selectedText : selectedText.Substring(0, 2000);
+
+ if (Uri.TryCreate(searchString, UriKind.Absolute, out var webUrl) &&
+ (webUrl.Scheme == Uri.UriSchemeHttp || webUrl.Scheme == Uri.UriSchemeHttps))
+ {
+ await Launcher.LaunchUriAsync(webUrl);
+ return;
+ }
+
+ var searchUri = new Uri(string.Format(SearchEngineUtility.GetSearchUrlBySearchEngine(AppSettingsService.EditorDefaultSearchEngine)
+ , string.Join("+", searchString.Split(null))));
+ await Launcher.LaunchUriAsync(searchUri);
+ }
+ catch (Exception ex)
+ {
+ LoggingService.LogError($"[{nameof(TextEditorCore)}] Failed to open search link: {ex.Message}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/Notepads/Controls/TextEditor/TextEditorCore.cs b/src/src/Notepads/Controls/TextEditor/TextEditorCore.cs
new file mode 100644
index 0000000..460fe4c
--- /dev/null
+++ b/src/src/Notepads/Controls/TextEditor/TextEditorCore.cs
@@ -0,0 +1,831 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved.
+// See LICENSE file in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
+namespace Notepads.Controls.TextEditor
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using Notepads.Commands;
+ using Notepads.Extensions;
+ using Notepads.Services;
+ using Notepads.Utilities;
+ using Windows.ApplicationModel.DataTransfer;
+ using Windows.Foundation;
+ using Windows.System;
+ using Windows.UI.Core;
+ using Windows.UI.Text;
+ using Windows.UI.Xaml;
+ using Windows.UI.Xaml.Controls;
+ using Windows.UI.Xaml.Controls.Primitives;
+ using Windows.UI.Xaml.Input;
+ using Windows.UI.Xaml.Media;
+
+ [TemplatePart(Name = ContentElementName, Type = typeof(ScrollViewer))]
+ [TemplatePart(Name = RootGridName, Type = typeof(Grid))]
+ [TemplatePart(Name = LineNumberCanvasName, Type = typeof(Canvas))]
+ [TemplatePart(Name = LineNumberGridName, Type = typeof(Grid))]
+ [TemplatePart(Name = LineHighlighterAndIndicatorCanvasName, Type = typeof(Canvas))]
+ [TemplatePart(Name = LineHighlighterName, Type = typeof(Grid))]
+ [TemplatePart(Name = LineIndicatorName, Type = typeof(Border))]
+ public partial class TextEditorCore : RichEditBox
+ {
+ public event EventHandler TextWrappingChanged;
+ public event EventHandler FontSizeChanged;
+ public event EventHandler FontZoomFactorChanged;
+ public event EventHandler CopyTextToWindowsClipboardRequested;
+ public event EventHandler CutSelectedTextToWindowsClipboardRequested;
+ public event EventHandler ScrollViewerViewChanging;
+
+ private const char RichEditBoxDefaultLineEnding = '\r';
+ private const char RegexDefaultLineEnding = '\n';
+
+ private bool _isDocumentLinesCachePendingUpdate = true;
+ private string[] _documentLinesCache; // internal copy of the active document text in array format
+ private string _document = string.Empty; // internal copy of the active document text
+
+ private readonly ICommandHandler _keyboardCommandHandler;
+ private readonly ICommandHandler _mouseCommandHandler;
+
+ private int _textSelectionStartPosition = 0;
+ private int _textSelectionEndPosition = 0;
+
+ private double _contentScrollViewerHorizontalOffset = 0;
+ private double _contentScrollViewerVerticalOffset = 0;
+ private double _contentScrollViewerHorizontalOffsetLastKnownPosition = 0;
+ private double _contentScrollViewerVerticalOffsetLastKnownPosition = 0;
+
+ private bool _shouldResetScrollViewerToLastKnownPositionAfterRegainingFocus = false;
+
+ private bool _loaded = false;
+
+ private readonly double _minimumZoomFactor = 10;
+ private readonly double _maximumZoomFactor = 500;
+
+ private const string ContentElementName = "ContentElement";
+ private ScrollViewer _contentScrollViewer;
+ private const string RootGridName = "RootGrid";
+ private Grid _rootGrid;
+ private const string LineNumberCanvasName = "LineNumberCanvas";
+ private Canvas _lineNumberCanvas;
+ private const string LineNumberGridName = "LineNumberGrid";
+ private Grid _lineNumberGrid;
+ private const string ContentScrollViewerVerticalScrollBarName = "VerticalScrollBar";
+ private ScrollBar _contentScrollViewerVerticalScrollBar;
+ private const string LineHighlighterAndIndicatorCanvasName = "LineHighlighterAndIndicatorCanvas";
+ private Canvas _lineHighlighterAndIndicatorCanvas;
+ private const string LineHighlighterName = "LineHighlighter";
+ private Grid _lineHighlighter;
+ private const string LineIndicatorName = "LineIndicator";
+ private Border _lineIndicator;
+
+ private TextWrapping _textWrapping = AppSettingsService.EditorDefaultTextWrapping;
+
+ public new TextWrapping TextWrapping
+ {
+ get => _textWrapping;
+ set
+ {
+ base.TextWrapping = value;
+ _textWrapping = value;
+ TextWrappingChanged?.Invoke(this, value);
+ }
+ }
+
+ private double _fontZoomFactor = 100;
+ private double _fontSize = AppSettingsService.EditorFontSize;
+
+ public new double FontSize
+ {
+ get => _fontSize;
+ set
+ {
+ base.FontSize = value;
+ _fontSize = value;
+ SetDefaultTabStopAndLineSpacing(FontFamily, value);
+ FontSizeChanged?.Invoke(this, value);
+
+ var newZoomFactor = Math.Round((value * 100) / AppSettingsService.EditorFontSize);
+ if (Math.Abs(newZoomFactor - _fontZoomFactor) >= 1)
+ {
+ _fontZoomFactor = newZoomFactor;
+ FontZoomFactorChanged?.Invoke(this, newZoomFactor);
+ }
+ }
+ }
+
+ public TextEditorCore()
+ {
+ IsSpellCheckEnabled = AppSettingsService.IsHighlightMisspelledWordsEnabled;
+ TextWrapping = AppSettingsService.EditorDefaultTextWrapping;
+ FontFamily = new FontFamily(AppSettingsService.EditorFontFamily);
+ FontSize = AppSettingsService.EditorFontSize;
+ FontStyle = AppSettingsService.EditorFontStyle;
+ FontWeight = AppSettingsService.EditorFontWeight;
+ SelectionHighlightColor = new SolidColorBrush(ThemeSettingsService.AppAccentColor);
+ SelectionHighlightColorWhenNotFocused = new SolidColorBrush(ThemeSettingsService.AppAccentColor);
+ SelectionFlyout = null;
+ HorizontalAlignment = HorizontalAlignment.Stretch;
+ VerticalAlignment = VerticalAlignment.Stretch;
+ DisplayLineNumbers = AppSettingsService.EditorDisplayLineNumbers;
+ DisplayLineHighlighter = AppSettingsService.EditorDisplayLineHighlighter;
+ HandwritingView.BorderThickness = new Thickness(0);
+
+ CopyingToClipboard += OnCopyingToClipboard;
+ CuttingToClipboard += OnCuttingToClipboard;
+ Paste += OnPaste;
+ TextChanging += OnTextChanging;
+ TextChanged += OnTextChanged;
+ SelectionChanging += OnSelectionChanging;
+
+ SetDefaultTabStopAndLineSpacing(FontFamily, FontSize);
+ PointerWheelChanged += OnPointerWheelChanged;
+ LostFocus += OnLostFocus;
+ Loaded += OnLoaded;
+
+ SelectionChanged += OnSelectionChanged;
+ TextWrappingChanged += OnTextWrappingChanged;
+ SizeChanged += OnSizeChanged;
+ FontSizeChanged += OnFontSizeChanged;
+
+ // Init shortcuts
+ _keyboardCommandHandler = GetKeyboardCommandHandler();
+ _mouseCommandHandler = GetMouseCommandHandler();
+
+ HookExternalEvents();
+
+ Window.Current.CoreWindow.Activated += OnCoreWindowActivated;
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ _rootGrid = GetTemplateChild(RootGridName) as Grid;
+
+ _lineNumberGrid = GetTemplateChild(LineNumberGridName) as Grid;
+ _lineNumberCanvas = GetTemplateChild(LineNumberCanvasName) as Canvas;
+
+ _lineHighlighterAndIndicatorCanvas = GetTemplateChild(LineHighlighterAndIndicatorCanvasName) as Canvas;
+ _lineHighlighter = GetTemplateChild(LineHighlighterName) as Grid;
+ _lineIndicator = GetTemplateChild(LineIndicatorName) as Border;
+
+ _contentScrollViewer = GetTemplateChild(ContentElementName) as ScrollViewer;
+ _shouldResetScrollViewerToLastKnownPositionAfterRegainingFocus = true;
+ _contentScrollViewer.ViewChanging += OnContentScrollViewerViewChanging;
+ _contentScrollViewer.ViewChanged += OnContentScrollViewerViewChanged;
+ _contentScrollViewer.SizeChanged += OnContentScrollViewerSizeChanged;
+
+ _contentScrollViewer.ApplyTemplate();
+ var scrollViewerRoot = (FrameworkElement)VisualTreeHelper.GetChild(_contentScrollViewer, 0);
+ _contentScrollViewerVerticalScrollBar = (ScrollBar)scrollViewerRoot.FindName(ContentScrollViewerVerticalScrollBarName);
+ _contentScrollViewerVerticalScrollBar.ValueChanged += OnVerticalScrollBarValueChanged;
+
+ _lineNumberGrid.SizeChanged += OnLineNumberGridSizeChanged;
+ _rootGrid.SizeChanged += OnRootGridSizeChanged;
+
+ Microsoft.Toolkit.Uwp.UI.ScrollViewerExtensions.SetEnableMiddleClickScrolling(_contentScrollViewer, true);
+ }
+
+ // Unhook events and clear state
+ public void Dispose()
+ {
+ CopyingToClipboard -= OnCopyingToClipboard;
+ CuttingToClipboard -= OnCuttingToClipboard;
+ Paste -= OnPaste;
+ TextChanging -= OnTextChanging;
+ TextChanged -= OnTextChanged;
+ SelectionChanging -= OnSelectionChanging;
+ PointerWheelChanged -= OnPointerWheelChanged;
+ LostFocus -= OnLostFocus;
+ Loaded -= OnLoaded;
+
+ if (_contentScrollViewer != null)
+ {
+ _contentScrollViewer.ViewChanging -= OnContentScrollViewerViewChanging;
+ _contentScrollViewer.ViewChanged -= OnContentScrollViewerViewChanged;
+ _contentScrollViewer.SizeChanged -= OnContentScrollViewerSizeChanged;
+ }
+
+ if (_contentScrollViewerVerticalScrollBar != null)
+ {
+ _contentScrollViewerVerticalScrollBar.ValueChanged -= OnVerticalScrollBarValueChanged;
+ }
+
+ if (_lineNumberGrid != null)
+ {
+ _lineNumberGrid.SizeChanged -= OnLineNumberGridSizeChanged;
+ }
+
+ if (_rootGrid != null)
+ {
+ _rootGrid.SizeChanged -= OnRootGridSizeChanged;
+ }
+
+ _lineNumberCanvas?.Children.Clear();
+ _renderedLineNumberBlocks.Clear();
+ _minRequisiteIntegerTextRenderingWidthCache.Clear();
+
+ SelectionChanged -= OnSelectionChanged;
+ TextWrappingChanged -= OnTextWrappingChanged;
+ SizeChanged -= OnSizeChanged;
+ FontSizeChanged -= OnFontSizeChanged;
+
+ UnhookExternalEvents();
+
+ Window.Current.CoreWindow.Activated -= OnCoreWindowActivated;
+ }
+
+ private KeyboardCommandHandler GetKeyboardCommandHandler()
+ {
+ var swallowedKeys = new List()
+ {
+ VirtualKey.B, VirtualKey.I, VirtualKey.U, VirtualKey.Tab,
+ VirtualKey.Number1, VirtualKey.Number2, VirtualKey.Number3,
+ VirtualKey.Number4, VirtualKey.Number5, VirtualKey.Number6,
+ VirtualKey.Number7, VirtualKey.Number8, VirtualKey.Number9,
+ VirtualKey.F3,
+ };
+
+ return new KeyboardCommandHandler(new List>
+ {
+ new KeyboardCommand(true, false, false, VirtualKey.Z, (args) => Undo()),
+ new KeyboardCommand(true, false, true, VirtualKey.Z, (args) => Redo()),
+ new KeyboardCommand(false, true, false, VirtualKey.Z, (args) => TextWrapping = TextWrapping == TextWrapping.Wrap ? TextWrapping.NoWrap : TextWrapping.Wrap),
+ new KeyboardCommand