From 8468561d3e5fb337e34e409100f5991e12d5c30f Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 24 Mar 2026 07:15:43 -0400 Subject: [PATCH 1/3] Add local LCM source mode workflow --- .github/workflows/patch-installer-cd.yml | 10 +- .serena/project.yml | 11 +- .vscode/extensions.json | 4 +- .vscode/launch.json | 28 +- .vscode/tasks.json | 95 +- Build/Agent/Invoke-VsCodeDebugBuild.ps1 | 160 ++ Build/Agent/Setup-InstallerBuild.ps1 | 12 +- Build/Agent/validate-test-exclusions.ps1 | 2 +- Build/Installer.legacy.targets | 4 + Build/Src/FwBuildTasks/CollectTargets.cs | 8 + Directory.Build.props | 9 +- Directory.Build.targets | 37 + Directory.Packages.props | 22 +- Docs/64bit-regfree-migration.md | 6 +- Docs/architecture/dependencies.md | 34 +- Docs/architecture/liblcm-debugging.md | 200 ++ Docs/installer-build-guide.md | 20 +- Docs/traversal-sdk-migration.md | 6 +- FieldWorks.LocalLcm.sln | 1028 ++++++++ FieldWorks.sln | 16 + Lib/src/ScrChecks/BuildInclude.targets | 29 + Lib/src/ScrChecks/CapitalizationCheck.cs | 595 +++++ Lib/src/ScrChecks/ChapterVerseCheck.cs | 995 ++++++++ Lib/src/ScrChecks/CharactersCheck.cs | 409 ++++ Lib/src/ScrChecks/MatchedPairsCheck.cs | 471 ++++ Lib/src/ScrChecks/MixedCapitalizationCheck.cs | 425 ++++ Lib/src/ScrChecks/Properties/AssemblyInfo.cs | 11 + .../Properties/Resources.Designer.cs | 63 + Lib/src/ScrChecks/Properties/Resources.resx | 117 + .../ScrChecks/Properties/Settings.Designer.cs | 26 + .../ScrChecks/Properties/Settings.settings | 7 + Lib/src/ScrChecks/PunctuationCheck.cs | 818 +++++++ Lib/src/ScrChecks/QuotationCheck.cs | 1190 +++++++++ Lib/src/ScrChecks/RepeatedWordsCheck.cs | 300 +++ Lib/src/ScrChecks/ScrChecks.csproj | 41 + .../CapitalizationCheckSilUnitTest.cs | 988 ++++++++ .../CapitalizationCheckUnitTest.cs | 250 ++ .../ScrChecksTests/ChapterVerseTests.cs | 2138 +++++++++++++++++ .../ScrChecksTests/CharactersCheckUnitTest.cs | 299 +++ .../ScrChecksTests/DummyTextToken.cs | 220 ++ .../MatchedPairsCheckUnitTest.cs | 409 ++++ .../MixedCapitalizationCheckUnitTest.cs | 317 +++ .../PunctuationCheckUnitTest.cs | 838 +++++++ .../QuotationCheckSilUnitTest.cs | 1013 ++++++++ .../ScrChecksTests/QuotationCheckUnitTest.cs | 1301 ++++++++++ .../ScrChecksTests/RepeatedWordsCheckTests.cs | 221 ++ .../RepeatedWordsCheckUnitTest.cs | 121 + .../ScrChecksTests/ScrChecksTestBase.cs | 114 + .../ScrChecksTests/ScrChecksTests.csproj | 41 + ...ceFinalPunctCapitalizationCheckUnitTest.cs | 146 ++ .../ScrChecksTests/TestChecksDataSource.cs | 144 ++ .../UnitTestChecksDataSource.cs | 103 + .../ScrChecksTests/UnitTestTokenizer.cs | 451 ++++ Lib/src/ScrChecks/TextInventory.cs | 109 + Lib/src/ScrChecks/VerseTextToken.cs | 155 ++ SDK_MIGRATION.md | 2 +- Setup-Developer-Machine.ps1 | 74 +- .../Controls/Widgets/FontHeightAdjuster.cs | 24 +- Src/Common/FieldWorks/FieldWorks.csproj | 1 + Src/Common/FwUtils/FwDirectoryFinder.cs | 37 + .../ParseCharacterSequencesTests.cs | 303 --- .../FwUtilsTests/TestFwStylesheetTests.cs | 70 +- Src/Common/FwUtils/IChecksDataSource.cs | 90 + Src/Common/FwUtils/IScriptureCheck.cs | 116 + Src/Common/FwUtils/ITextToken.cs | 67 + Src/Common/FwUtils/RecordErrorEventArgs.cs | 62 + Src/Common/FwUtils/TextFileDataSource.cs | 297 ++- Src/Common/FwUtils/TextTokenSubstring.cs | 31 + Src/Common/SimpleRootSite/EditingHelper.cs | 34 +- Src/FwCoreDlgs/CharContextCtrl.cs | 56 +- .../FwCoreDlgsTests/CharContextCtrlTests.cs | 90 + Src/FwCoreDlgs/ValidCharactersDlg.cs | 2 + build.ps1 | 237 +- scripts/Agent/Copy-LocalLcm.ps1 | 184 -- scripts/Agent/Invoke-Installer.ps1 | 14 +- scripts/Agent/Invoke-InstallerCheck.ps1 | 8 +- test.ps1 | 35 + 77 files changed, 17664 insertions(+), 757 deletions(-) create mode 100644 Build/Agent/Invoke-VsCodeDebugBuild.ps1 create mode 100644 Docs/architecture/liblcm-debugging.md create mode 100644 FieldWorks.LocalLcm.sln create mode 100644 Lib/src/ScrChecks/BuildInclude.targets create mode 100644 Lib/src/ScrChecks/CapitalizationCheck.cs create mode 100644 Lib/src/ScrChecks/ChapterVerseCheck.cs create mode 100644 Lib/src/ScrChecks/CharactersCheck.cs create mode 100644 Lib/src/ScrChecks/MatchedPairsCheck.cs create mode 100644 Lib/src/ScrChecks/MixedCapitalizationCheck.cs create mode 100644 Lib/src/ScrChecks/Properties/AssemblyInfo.cs create mode 100644 Lib/src/ScrChecks/Properties/Resources.Designer.cs create mode 100644 Lib/src/ScrChecks/Properties/Resources.resx create mode 100644 Lib/src/ScrChecks/Properties/Settings.Designer.cs create mode 100644 Lib/src/ScrChecks/Properties/Settings.settings create mode 100644 Lib/src/ScrChecks/PunctuationCheck.cs create mode 100644 Lib/src/ScrChecks/QuotationCheck.cs create mode 100644 Lib/src/ScrChecks/RepeatedWordsCheck.cs create mode 100644 Lib/src/ScrChecks/ScrChecks.csproj create mode 100644 Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckSilUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/ChapterVerseTests.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/CharactersCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/DummyTextToken.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/MatchedPairsCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/MixedCapitalizationCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/PunctuationCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/QuotationCheckSilUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/QuotationCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckTests.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/ScrChecksTestBase.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/ScrChecksTests.csproj create mode 100644 Lib/src/ScrChecks/ScrChecksTests/SentenceFinalPunctCapitalizationCheckUnitTest.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/TestChecksDataSource.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/UnitTestChecksDataSource.cs create mode 100644 Lib/src/ScrChecks/ScrChecksTests/UnitTestTokenizer.cs create mode 100644 Lib/src/ScrChecks/TextInventory.cs create mode 100644 Lib/src/ScrChecks/VerseTextToken.cs delete mode 100644 Src/Common/FwUtils/FwUtilsTests/ParseCharacterSequencesTests.cs create mode 100644 Src/Common/FwUtils/IChecksDataSource.cs create mode 100644 Src/Common/FwUtils/IScriptureCheck.cs create mode 100644 Src/Common/FwUtils/RecordErrorEventArgs.cs delete mode 100644 scripts/Agent/Copy-LocalLcm.ps1 diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 50f6af2400..b0df932740 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -151,7 +151,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: | + dotnet-version: | 3.1.x 5.0.x @@ -166,7 +166,7 @@ jobs: choco install wixtoolset --version 3.11.2 --allow-downgrade --force echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append if: github.event_name != 'pull_request' - + - name: Import Base Build Artifacts shell: pwsh run: | @@ -211,7 +211,7 @@ jobs: ) $valueName = "Switch.System.DisableTempFileCollectionDirectoryFeature" $expectedValue = "true" - + foreach ($path in $regPaths) { Write-Host "Adding or updating registry value in $path..." if (-not (Test-Path $path)) { @@ -233,11 +233,11 @@ jobs: $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" if ($results) { foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red + Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red } exit 1 } else { - Write-Host "No errors found" -ForegroundColor green + Write-Host "No errors found" -ForegroundColor green exit 0 } diff --git a/.serena/project.yml b/.serena/project.yml index 63d7787207..cc6570bb14 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -93,7 +93,7 @@ initial_prompt: | Key build facts: - Uses MSBuild Traversal SDK via FieldWorks.proj (21 ordered phases, 110+ projects) - Native C++ (Phase 2) must build before managed code (Phases 3+) - - Build command: .\build.ps1 or msbuild FieldWorks.proj /p:Configuration=Debug /p:Platform=x64 /m + - Build command: .\build.ps1 or msbuild FieldWorks.proj /p:Configuration=Debug /m - Check .github/instructions/*.instructions.md for coding guidelines (managed, native, testing, etc.) - Per-folder AGENTS.md files describe component contracts and dependencies # project_name: Intentionally left out so that the folder name will be used and worktrees will not conflict @@ -136,3 +136,12 @@ symbol_info_budget: # Note: the backend is fixed at startup. If a project with a different backend # is activated post-init, an error will be returned. language_backend: + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8edbe7c5ee..203ffa3652 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,9 @@ // C# language + test explorer in VS Code (ReSharper-first) "jetbrains.resharper-code", + // Use the official C# extension for debugging language support, but disable its test runner to avoid conflicts with ReSharper's test runner. + "ms-dotnettools.csharp", + // PowerShell scripts are part of the standard workflow (build.ps1/test.ps1/etc.) "ms-vscode.powershell", @@ -17,6 +20,5 @@ "unwantedRecommendations": [ // This workspace is ReSharper-first; dotnet test providers are discouraged. "ms-dotnettools.csdevkit", - "ms-dotnettools.csharp" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d9a29496f..9f835bd181 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,19 +2,43 @@ "version": "0.2.0", "configurations": [ { - "name": "FieldWorks", + "name": "FieldWorks (.NET Framework, Package)", "type": "clr", "request": "launch", + "preLaunchTask": "Prepare Debug (Package)", "program": "${workspaceFolder}\\Output\\Debug\\FieldWorks.exe", "cwd": "${workspaceFolder}\\Output\\Debug", "console": "externalTerminal", "justMyCode": false, + "requireExactSource": false, + "symbolOptions": { + "searchPaths": [ + "${workspaceFolder}\\Output\\Debug" + ] + } + }, + { + "name": "FieldWorks (.NET Framework, Local LCM)", + "type": "clr", + "request": "launch", + "preLaunchTask": "Prepare Debug (Local LCM)", + "program": "${workspaceFolder}\\Output\\Debug\\FieldWorks.exe", + "cwd": "${workspaceFolder}\\Output\\Debug", + "console": "externalTerminal", + "justMyCode": false, + "requireExactSource": false, + "symbolOptions": { + "searchPaths": [ + "${workspaceFolder}\\Output\\Debug", + "${workspaceFolder}\\Localizations\\LCM\\artifacts\\Debug\\net462" + ] + } }, { "name": "Debug NUnit Tests", "type": "clr", "request": "launch", - "preLaunchTask": "Build Debug", + "preLaunchTask": "Build", "program": "dotnet", "args": [ "test", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e83806466c..15a8dcd2ce 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -211,12 +211,96 @@ { "label": "Build", "type": "shell", - "command": "./build.ps1", + "command": "./build.ps1 -LcmMode Auto", "group": { "kind": "build", "isDefault": true }, - "detail": "Build FieldWorks (auto-detects worktree)", + "detail": "Build FieldWorks (package-backed by default; use explicit local LCM tasks for source mode)", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Package)", + "type": "shell", + "command": "./build.ps1 -LcmMode Package", + "group": "build", + "detail": "Build FieldWorks against the pinned liblcm packages", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Package, VS Code Debug)", + "type": "shell", + "command": "./build.ps1 -LcmMode Package -ManagedDebugType portable", + "group": "build", + "detail": "Build FieldWorks against pinned liblcm packages with portable managed PDBs for the VS Code debugger", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Prepare Debug (Package)", + "type": "shell", + "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -LcmMode Package -ManagedDebugType portable", + "group": "build", + "detail": "Build for VS Code debugging only when relevant files changed since the last successful package-mode portable-PDB debug build", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Local LCM)", + "type": "shell", + "command": "./build.ps1 -LcmMode Local", + "group": "build", + "detail": "Build FieldWorks against the nested Localizations/LCM checkout", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Build (Local LCM, VS Code Debug)", + "type": "shell", + "command": "./build.ps1 -LcmMode Local -ManagedDebugType portable", + "group": "build", + "detail": "Build FieldWorks against the nested local LCM checkout with portable managed PDBs for the VS Code debugger", + "options": { + "shell": { + "executable": "powershell.exe", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + } + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Prepare Debug (Local LCM)", + "type": "shell", + "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -LcmMode Local -ManagedDebugType portable", + "group": "build", + "detail": "Build for VS Code debugging only when relevant files changed since the last successful local-LCM portable-PDB debug build", "options": { "shell": { "executable": "powershell.exe", @@ -239,7 +323,6 @@ }, "problemMatcher": "$msCompile" }, - // ==================== Test Tasks ==================== { "label": "Test", @@ -249,7 +332,7 @@ "kind": "test", "isDefault": true }, - "detail": "Run all tests (auto-detects worktree). See specs/007-test-modernization-vstest/IGNORED_TESTS.md for the tracked skip/ignore inventory.", + "detail": "Run all tests. See specs/007-test-modernization-vstest/IGNORED_TESTS.md for the tracked skip/ignore inventory.", "options": { "shell": { "executable": "powershell.exe", @@ -484,9 +567,9 @@ "problemMatcher": [] }, { - "label": "Installer Check: Bundle (Debug, x64)", + "label": "Installer Check: Bundle (Debug)", "type": "shell", - "command": "./scripts/Agent/Invoke-InstallerCheck.ps1 -InstallerType Bundle -Configuration Debug -Platform x64", + "command": "./scripts/Agent/Invoke-InstallerCheck.ps1 -InstallerType Bundle -Configuration Debug", "detail": "Run snapshot -> install bundle -> snapshot -> diff (writes evidence under Output/InstallerEvidence/)", "options": { "shell": { diff --git a/Build/Agent/Invoke-VsCodeDebugBuild.ps1 b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 new file mode 100644 index 0000000000..367195a14b --- /dev/null +++ b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 @@ -0,0 +1,160 @@ +[CmdletBinding()] +param( + [ValidateSet('Package', 'Local')] + [string]$LcmMode, + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug', + [ValidateSet('full', 'portable', 'pdbonly', 'embedded')] + [string]$ManagedDebugType = 'portable' +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$outputDir = Join-Path $repoRoot ("Output\{0}" -f $Configuration) +$stampPath = Join-Path $outputDir 'BuildStamp.json' +$runtimeExePath = Join-Path $outputDir 'FieldWorks.exe' + +function Get-DebugRebuildCheckPathspecs { + param( + [Parameter(Mandatory = $true)][ValidateSet('Package', 'Local')][string]$ResolvedLcmMode + ) + + $pathspecs = @( + 'build.ps1', + 'Directory.Build.props', + 'Directory.Build.targets', + 'Directory.Packages.props', + 'FieldWorks.proj', + 'Build', + 'Src', + 'Lib' + ) + + if ($ResolvedLcmMode -eq 'Local') { + $pathspecs += @('FieldWorks.LocalLcm.sln', 'Localizations/LCM') + } + else { + $pathspecs += 'FieldWorks.sln' + } + + return $pathspecs | ForEach-Object { $_ -replace '\\', '/' } +} + +function Invoke-Git { + param( + [Parameter(Mandatory = $true)][string[]]$Arguments + ) + + $output = & git @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Git command failed: git $($Arguments -join ' ')" + } + + return @($output | ForEach-Object { $_.TrimEnd() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) +} + +function Get-GitStatusForDebugRebuildCheck { + param( + [Parameter(Mandatory = $true)][string[]]$Pathspecs + ) + + return Invoke-Git -Arguments (@('status', '--porcelain=v1', '--untracked-files=all', '--') + $Pathspecs) +} + +function Test-GitStateRequiresDebugRebuild { + param( + [Parameter(Mandatory = $true)][psobject]$Stamp, + [Parameter(Mandatory = $true)][string[]]$Pathspecs + ) + + # We only skip the prelaunch build when we can prove the last successful debug build + # still matches the source inputs for this debug session. The proof has three parts: + # 1. the stamp recorded the same debug mode inputs we are asking for now, + # 2. no relevant commits have landed since the stamped HEAD, and + # 3. the current relevant worktree status still matches the stamped worktree status. + # If any of those checks fail, we rebuild before launching so debugging does not start + # against stale binaries or symbols. + + if (-not ($Stamp.PSObject.Properties.Name -contains 'GitHead') -or [string]::IsNullOrWhiteSpace($Stamp.GitHead)) { + Write-Host "Build stamp is missing Git head metadata. Rebuilding before launch..." -ForegroundColor Yellow + return $true + } + + if (-not ($Stamp.PSObject.Properties.Name -contains 'RelevantDebugPathspecs') -or -not ($Stamp.PSObject.Properties.Name -contains 'RelevantDebugStatus')) { + Write-Host "Build stamp is missing Git-based debug metadata. Rebuilding before launch..." -ForegroundColor Yellow + return $true + } + + $stampPathspecs = @($Stamp.RelevantDebugPathspecs) + if (($stampPathspecs.Count -ne $Pathspecs.Count) -or (@($stampPathspecs) -join "`n") -ne ($Pathspecs -join "`n")) { + Write-Host "Build stamp inputs do not match the requested VS Code debug mode. Rebuilding..." -ForegroundColor Yellow + return $true + } + + $currentHead = (Invoke-Git -Arguments @('rev-parse', 'HEAD'))[0] + if ($currentHead -ne $Stamp.GitHead) { + $committedChanges = Invoke-Git -Arguments (@('diff', '--name-only', "$($Stamp.GitHead)..$currentHead", '--') + $Pathspecs) + if ($committedChanges.Count -gt 0) { + Write-Host "Detected committed changes since the last successful $Configuration debug build. Rebuilding before launch..." -ForegroundColor Yellow + return $true + } + } + + $currentStatus = Get-GitStatusForDebugRebuildCheck -Pathspecs $Pathspecs + $stampStatus = @($Stamp.RelevantDebugStatus) + if (($stampStatus.Count -ne $currentStatus.Count) -or (($stampStatus -join "`n") -ne ($currentStatus -join "`n"))) { + Write-Host "Detected working tree changes since the last successful $Configuration debug build. Rebuilding before launch..." -ForegroundColor Yellow + return $true + } + + return $false +} + +function Invoke-DebugBuild { + $buildArgs = @( + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-File', + (Join-Path $repoRoot 'build.ps1'), + '-LcmMode', + $LcmMode, + '-Configuration', + $Configuration, + '-ManagedDebugType', + $ManagedDebugType + ) + + & powershell.exe @buildArgs + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +if (-not (Test-Path $stampPath) -or -not (Test-Path $runtimeExePath)) { + Write-Host "No successful $Configuration debug build stamp found. Building before launch..." -ForegroundColor Yellow + Invoke-DebugBuild + exit 0 +} + +$stamp = Get-Content -LiteralPath $stampPath -Raw | ConvertFrom-Json +$resolvedLcmMode = if ($LcmMode -eq 'Local') { 'Local' } else { 'Package' } + +$modeMatches = ($stamp.PSObject.Properties.Name -contains 'ResolvedLcmMode') -and ($stamp.ResolvedLcmMode -eq $resolvedLcmMode) +$debugTypeMatches = ($stamp.PSObject.Properties.Name -contains 'ManagedDebugType') -and ($stamp.ManagedDebugType -eq $ManagedDebugType) + +if (-not $modeMatches -or -not $debugTypeMatches) { + Write-Host "Build stamp mode does not match requested VS Code debug mode. Rebuilding..." -ForegroundColor Yellow + Invoke-DebugBuild + exit 0 +} + +$pathspecsToCheck = Get-DebugRebuildCheckPathspecs -ResolvedLcmMode $resolvedLcmMode +if (Test-GitStateRequiresDebugRebuild -Stamp $stamp -Pathspecs $pathspecsToCheck) { + Invoke-DebugBuild + exit 0 +} + +Write-Host "[OK] No relevant changes since the last successful $Configuration debug build. Skipping prelaunch build." -ForegroundColor Green +exit 0 \ No newline at end of file diff --git a/Build/Agent/Setup-InstallerBuild.ps1 b/Build/Agent/Setup-InstallerBuild.ps1 index bfec87848f..9026e6870e 100644 --- a/Build/Agent/Setup-InstallerBuild.ps1 +++ b/Build/Agent/Setup-InstallerBuild.ps1 @@ -326,15 +326,15 @@ if ($issues.Count -eq 0) { # VS Developer environment is active, show simple commands Write-Host "" Write-Host " # Restore packages" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64" -ForegroundColor Cyan + Write-Host " msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug" -ForegroundColor Cyan Write-Host "" Write-Host " # Build base installer" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n" -ForegroundColor Cyan + Write-Host " msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:config=release /m /v:n" -ForegroundColor Cyan Write-Host "" if ($SetupPatch) { Write-Host " # Build patch installer" -ForegroundColor Gray - Write-Host " msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n" -ForegroundColor Cyan + Write-Host " msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:config=release /m /v:n" -ForegroundColor Cyan Write-Host "" } } else { @@ -344,15 +344,15 @@ if ($issues.Count -eq 0) { Write-Host " # Option 2: Use these one-liner commands from any PowerShell:" -ForegroundColor Gray Write-Host "" Write-Host " # Restore packages" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64"' -ForegroundColor Cyan + Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug"' -ForegroundColor Cyan Write-Host "" Write-Host " # Build base installer" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -ForegroundColor Cyan + Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Debug /p:config=release /m /v:n"' -ForegroundColor Cyan Write-Host "" if ($SetupPatch) { Write-Host " # Build patch installer" -ForegroundColor Gray - Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /m /v:n"' -ForegroundColor Cyan + Write-Host ' cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"" -arch=amd64 >nul && msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:config=release /m /v:n"' -ForegroundColor Cyan Write-Host "" } } diff --git a/Build/Agent/validate-test-exclusions.ps1 b/Build/Agent/validate-test-exclusions.ps1 index 3513eeb557..cbf6da65be 100644 --- a/Build/Agent/validate-test-exclusions.ps1 +++ b/Build/Agent/validate-test-exclusions.ps1 @@ -26,7 +26,7 @@ try { # Run MSBuild and check for CS0436 $BuildLog = "$RepoRoot/Output/test-exclusions/build.log" Write-Host "Running MSBuild..." - $BuildArgs = @("FieldWorks.proj", "/m", "/p:Configuration=Debug", "/p:Platform=x64", "/v:minimal", "/nologo", "/fl", "/flp:LogFile=$BuildLog;Verbosity=Normal") + $BuildArgs = @("FieldWorks.proj", "/m", "/p:Configuration=Debug", "/v:minimal", "/nologo", "/fl", "/flp:LogFile=$BuildLog;Verbosity=Normal") & msbuild $BuildArgs if ($LASTEXITCODE -ne 0) { diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index 7532bdcce5..e068951d04 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -296,6 +296,10 @@ + + + + diff --git a/Build/Src/FwBuildTasks/CollectTargets.cs b/Build/Src/FwBuildTasks/CollectTargets.cs index 6c757b0659..33936bfccf 100644 --- a/Build/Src/FwBuildTasks/CollectTargets.cs +++ b/Build/Src/FwBuildTasks/CollectTargets.cs @@ -129,6 +129,8 @@ public void Generate() ); var infoEth = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/Ethnologue")); CollectInfo(infoEth); + var infoScr2 = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/ScrChecks")); + CollectInfo(infoScr2); var infoObj = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/ObjectBrowser")); CollectInfo(infoObj); @@ -550,6 +552,12 @@ private void WriteTargetFiles() writer.Write("\tdisable - $(MSBuildThisFileDirectory) + true + false + $(FwRoot)Localizations\LCM\ + $([System.IO.Path]::GetFullPath('$(LocalLcmRootDir)artifacts\$(Configuration)\net462')) $(FwRoot)DistFiles\ $(FwRoot)Output\ $(FwOutput)$(Configuration)\ @@ -111,6 +114,10 @@ $(FwRoot)Downloads $(DownloadsDir) + $(LocalLcmRootDir) + $(LocalLcmArtifactsDir) + $(LocalLcmArtifactsDir) + $(LocalLcmArtifactsDir) true true diff --git a/Directory.Build.targets b/Directory.Build.targets index d16b35b46d..fd7b62702a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -6,6 +6,43 @@ <_InstallerBuildProj>$([System.IO.Path]::Combine('$(_TransformRoot)','Build','InstallerBuild.proj')) + + <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel'))" /> + <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.Core'))" /> + <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.Utils'))" /> + <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.FixData'))" /> + + + + + + + + + + + + + + + + + + + - true + true - + - + @@ -47,7 +49,7 @@ SIL LCModel Ecosystem ============================================================= --> - + @@ -62,7 +64,7 @@ SIL LibPalaso Ecosystem ============================================================= --> - + @@ -86,7 +88,7 @@ SIL Localization (ExcludeAssets=all; consumed by Build/Localize.targets) ============================================================= --> - + @@ -97,7 +99,7 @@ SIL Tools ============================================================= --> - + @@ -115,7 +117,7 @@ Third-Party Runtime ============================================================= --> - + @@ -147,7 +149,7 @@ Test Infrastructure ============================================================= --> - + diff --git a/Docs/64bit-regfree-migration.md b/Docs/64bit-regfree-migration.md index 68e97b027e..4d4d890751 100644 --- a/Docs/64bit-regfree-migration.md +++ b/Docs/64bit-regfree-migration.md @@ -42,7 +42,7 @@ A) Move to64-bit only 3) Solution + CI - Update `FieldWorks.sln` to remove Win32 platforms and keep `x64` (Debug/Release). If other solutions exist, do the same. -- Update CI/build scripts to call: `msbuild FieldWorks.sln /m /p:Configuration=Debug /p:Platform=x64`. +- Update CI/build scripts to call: `build.ps1` or `msbuild FieldWorks.sln /m /p:Configuration=Debug`. - Remove any32-bit specific paths or tools (e.g., SysWOW64 regsvr32) from build/targets. B) Registration‑free COM (no regsvr32) @@ -133,7 +133,7 @@ Appendix: key references in repo - COM interop usage sites: `Src/Common/ViewsInterfaces/Views.cs`, `Src/Common/FwUtils/DebugProcs.cs`. Validation path (first pass) -- Build all (x64): `msbuild FieldWorks.sln /m /p:Configuration=Debug /p:Platform=x64`. +- Build all: `msbuild FieldWorks.sln /m /p:Configuration=Debug`. - Confirm `FieldWorks.exe.manifest` is generated and contains `` with `comClass` entries and interfaces. **✅ VERIFIED** - From a machine with no FieldWorks registrations, launch `FieldWorks.exe` → expect no class-not-registered exceptions. **✅ VERIFIED** @@ -143,7 +143,7 @@ Validation path (first pass) - `Directory.Build.props` enforces `x64` - `FieldWorks.sln` Win32 configurations removed - Native VCXPROJs x86 configurations removed -- CI enforces `/p:Platform=x64` by invoking `msbuild Build/FieldWorks.proj` +- CI enforces the x64-only default by invoking `build.ps1` ### Phase 2: Manifest wiring (✅ COMPLETE) - `Build/RegFree.targets` generates manifests with COM class/typelib entries diff --git a/Docs/architecture/dependencies.md b/Docs/architecture/dependencies.md index 7b5b5f80e9..a4f40c1685 100644 --- a/Docs/architecture/dependencies.md +++ b/Docs/architecture/dependencies.md @@ -4,7 +4,7 @@ FieldWorks depends on several external libraries and related repositories. This ## Overview -Most dependencies are automatically downloaded as NuGet packages during the build process. However, if you need to debug into or modify these libraries, you may need to build them locally. +Most dependencies are automatically downloaded as NuGet packages during the build process. If you need to debug into or modify these libraries, use either a local source workflow or a local package-validation workflow depending on the goal. ## Primary Dependencies @@ -32,9 +32,22 @@ By default, dependencies are downloaded as NuGet packages during the build. The ... ``` -## Building Dependencies Locally +## Local Source Workflow -If you need to debug into or modify a dependency library, you can build it locally. +Use local source mode when you are diagnosing or changing library code and need direct source-level debugging. + +For `liblcm`, the preferred local source workflow is: + +1. Clone `liblcm` under `Localizations/LCM`. +2. Use `FieldWorks.LocalLcm.sln` in Visual Studio or `./build.ps1 -LcmMode Local`. +3. Make and validate the `liblcm` fix locally. +4. Return to the package-backed FieldWorks workflow after a released `liblcm` package is available. + +This workflow is for development and local verification. It is not the CI truth. + +## Local Package Validation Workflow + +Use a local package workflow only when you explicitly need to validate FieldWorks as a package consumer rather than as a source consumer. ### Step 1: Clone the Repositories @@ -107,13 +120,14 @@ Update the NuGet versions in FieldWorks to use your local packages: ## Debugging Dependencies -To debug into dependency code: +For the detailed `liblcm` debugging workflow, see `Docs/architecture/liblcm-debugging.md`. + +Short version: -1. Build the dependency in Debug configuration -2. Open the dependency project in Visual Studio alongside FieldWorks -3. Start debugging FLEx -4. Choose **Debug → Attach to Process** from the dependency project -5. If breakpoints show "No symbols loaded", disable **Debug → Options → Enable Just My Code** +1. Use Visual Studio 2022 as the primary debugger for `.NET Framework` plus native FieldWorks work. +2. If you need exact source-level debugging into `liblcm`, clone it under `Localizations/LCM`, then use `FieldWorks.LocalLcm.sln` or `./build.ps1 -LcmMode Local`. +3. Use VS Code only for limited managed-only sessions in this repo, and only with the legacy C# extension path. +4. If breakpoints show "No symbols loaded", verify the loaded module path and PDB match before changing debugger settings. ## Dependency Configuration @@ -125,7 +139,7 @@ Build dependency information is also available in: FieldWorks uses GitHub Actions for CI/CD. The workflow files are in `.github/workflows/`. -Dependencies are restored automatically from NuGet during CI builds. +Dependencies are restored automatically from NuGet during CI builds. CI does not depend on a nested `Localizations/LCM` checkout. ## See Also diff --git a/Docs/architecture/liblcm-debugging.md b/Docs/architecture/liblcm-debugging.md new file mode 100644 index 0000000000..91e3ea5b8b --- /dev/null +++ b/Docs/architecture/liblcm-debugging.md @@ -0,0 +1,200 @@ +# Debugging liblcm from FieldWorks + +This guide documents the practical debugging workflow for `liblcm` when it is used by FieldWorks. + +FieldWorks is a Windows/x64, .NET Framework 4.8 desktop application with both managed and native code. That matters because the debugger choice is not interchangeable: + +- Visual Studio 2022 is the supported debugger for the full FieldWorks plus `liblcm` scenario. +- VS Code can help for editing, navigation, and some limited managed-only sessions, but it is not the primary debugger for mixed managed/native work. + +## Choose the right path + +Use this decision tree first: + +1. Need to step between C# and native C++ or debug a process that loads native DLLs: + Use Visual Studio 2022. +2. Need trustworthy source-level debugging into your local `liblcm` changes: + Use `FieldWorks.LocalLcm.sln` in Visual Studio or the local-LCM launcher/task pair in VS Code. +3. Need to inspect package behavior without rebuilding `liblcm` locally: + Use Visual Studio 2022 with symbols and Source Link when available. +4. Need a quick managed-only session from VS Code: + Use the legacy C# extension path with the existing `clr` launch configuration, but treat it as best effort. + +## How FieldWorks consumes liblcm + +By default, FieldWorks consumes `liblcm` through NuGet packages. The version is pinned in `Build/SilVersions.props` and flowed into `Directory.Packages.props`. + +For local debugging, the preferred workflow is now local source mode with a nested checkout at `Localizations/LCM`. In that mode, FieldWorks switches from the `SIL.LCModel*` packages to local project references and local `liblcm` build artifacts. + +## Recommended workflow: Visual Studio 2022 + +This is the default workflow for real debugging. + +### Package-based debugging + +Use this when you want to investigate the currently pinned package version without changing the dependency source. + +1. Build FieldWorks with `./build.ps1`. +2. Open FieldWorks in Visual Studio 2022. +3. Start the FieldWorks host process under the debugger, or attach to the running process. +4. In Visual Studio debugger options: + Enable Source Link support. +5. In Visual Studio debugger options: + Disable Just My Code for sessions where you need to step into package code. +6. In Visual Studio symbol settings: + Enable the NuGet.org symbol server if the package publishes symbols there. +7. Use the Modules window to verify: + the exact `SIL.LCModel*.dll` path loaded, + whether symbols were found, + and whether the PDB matches the loaded binary. + +Use this path when the issue reproduces against the pinned package and you do not need to modify `liblcm` itself. + +### Local-source debugging with nested checkout + +Use this when you are actively changing `liblcm` or you need exact source and symbol fidelity. + +Prerequisites: + +- `liblcm` is cloned at `Localizations/LCM`. +- Build output is Debug/x64. + +Steps: + +1. Open `FieldWorks.LocalLcm.sln` in Visual Studio 2022. +2. Build the solution. If the local liblcm build tasks have not been generated yet, the first build bootstraps them into `Localizations/LCM/artifacts//net462`. +3. Start debugging FieldWorks. +4. If breakpoints do not bind, check the Modules window before changing any debugger settings. + +Why this works: + +- FieldWorks compiles against local `liblcm` projects instead of the published packages. +- Shared build-time artifacts come from the local `liblcm` checkout. +- Visual Studio can resolve the matching local PDBs without a post-build copy step. + +Common reset step: + +- Switch back to `FieldWorks.sln` in Visual Studio or use the package-backed launcher/task in VS Code. + +## Limited workflow: VS Code + +VS Code is useful here, but with narrower expectations. + +Use VS Code only for these cases: + +- editing and navigation, +- quick managed-only debugging of the FieldWorks host, +- validating that breakpoints bind against local managed `liblcm` symbols, +- tracing and log-driven investigation. + +Do not treat VS Code as the primary workflow for: + +- mixed managed/native debugging, +- stepping across native boundaries, +- debugger-driven investigation of registration-free COM issues, +- anything that depends on Visual Studio's Modules and mixed-mode support. + +### VS Code prerequisites + +1. Keep `dotnet.preferCSharpExtension` enabled in workspace settings. +2. Use the Microsoft C# extension path, not C# Dev Kit, for this repo. +3. Use the `clr` launch type for the .NET Framework host executable. +4. Stay x64 only. +5. Ensure matching PDBs are present next to the loaded `SIL.LCModel*.dll` files. +6. Use the VS Code launchers in this repo, which prebuild managed projects with portable PDBs and keep package mode and local-source mode explicit. + +### VS Code launch workflow + +1. Build FieldWorks with `./build.ps1`. +2. Choose `FieldWorks (.NET Framework, Package)` when you want the pinned package path. +3. Choose `FieldWorks (.NET Framework, Local LCM)` when you want the nested `Localizations/LCM` path. +4. Keep `justMyCode` disabled when stepping into `liblcm`. +5. The VS Code launchers first run `Prepare Debug (*)`, which checks the last successful debug-build stamp and skips the build when no relevant saved files changed. +6. Do not switch the VS Code debug path to Windows PDBs. The debugger used here requires portable PDBs. +7. If symbols still do not bind, inspect the loaded binaries and symbol paths before changing code. + +Important boundary: + +- Local source mode is for diagnosis, development, and local verification. +- Package-backed builds remain the final merge and CI validation path. + +Practical limit: + +- This path is best effort only. If the session turns into mixed managed/native debugging, move to Visual Studio. + +## NuGet package versus local source + +### When to stay on the package + +Stay on the package when: + +- you are reproducing a bug in the version currently pinned by FieldWorks, +- Source Link and symbols are already good enough, +- you only need to understand behavior, not change `liblcm`. + +### When to switch to local source mode + +Switch to local source mode when: + +- you are modifying `liblcm`, +- you want a checked-out `liblcm` solution in the same Visual Studio session, +- you need build-time artifacts to come from the local checkout, +- you want the package-backed mode to remain untouched when local mode is off. + +## Build entrypoints + +- `./build.ps1 -LcmMode Package` forces the package-backed path. +- `./build.ps1 -LcmMode Local` forces the nested `Localizations/LCM` path and bootstraps the local build tasks when needed. +- `./build.ps1 -LcmMode Auto` stays package-backed by default, but prints whether local LCM inputs are available. +- `FieldWorks.sln` stays package-backed in Visual Studio. +- `FieldWorks.LocalLcm.sln` enables local source mode in Visual Studio. + +## Failure modes to check first + +If debugging does not behave as expected, check these before changing tool settings: + +1. Wrong binary loaded: + verify the loaded `SIL.LCModel*.dll` path. +2. PDB mismatch: + verify that the PDB came from the same build as the DLL. +3. Package cache confusion: + stale copies under `%USERPROFILE%\.nuget\packages` can mislead assumptions. +4. Architecture mismatch: + keep the workflow x64 end to end. + +## Minimal repo changes that improve this workflow + +The current repo is close, but a few small changes make the workflow more obvious and less fragile. + +### Recommended now + +1. Keep a dedicated `liblcm` debugging guide in the repo. +2. Keep a VS Code launch configuration named for `.NET Framework` instead of a generic `FieldWorks` label. +3. Keep workspace settings biased toward the legacy C# extension for this repo. + +### Recommended later if the team wants a smoother inner loop + +1. Add a script that reports which `SIL.LCModel*.dll` files are currently loaded and where they came from. +2. Add a short troubleshooting checklist for symbol binding and package-cache mismatches. +3. If package-debugging becomes a common workflow, standardize symbol publishing and Source Link expectations for the SIL packages. + +## References + +- Visual Studio mixed-mode debugging: + https://learn.microsoft.com/en-us/visualstudio/debugger/how-to-debug-in-mixed-mode?view=vs-2022 +- Visual Studio symbols and source files: + https://learn.microsoft.com/en-us/visualstudio/debugger/specify-symbol-dot-pdb-and-source-files-in-the-visual-studio-debugger?view=vs-2022 +- Visual Studio decompilation: + https://learn.microsoft.com/en-us/visualstudio/debugger/decompilation?view=vs-2022 +- Source Link guidance: + https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/sourcelink +- NuGet symbol packages: + https://learn.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg +- VS Code C# Dev Kit FAQ: + https://code.visualstudio.com/docs/csharp/cs-dev-kit-faq +- VS Code C# debugging: + https://code.visualstudio.com/docs/csharp/debugging +- VS Code C# extension desktop .NET Framework note: + https://github.com/dotnet/vscode-csharp/blob/main/docs/debugger/Desktop-.NET-Framework.md +- Damir Arh on debugging libraries from NuGet: + https://www.damirscorner.com/blog/posts/20250411-DebuggingLibrariesFromNuGet.html \ No newline at end of file diff --git a/Docs/installer-build-guide.md b/Docs/installer-build-guide.md index a761b45b19..65b9fb7843 100644 --- a/Docs/installer-build-guide.md +++ b/Docs/installer-build-guide.md @@ -58,22 +58,22 @@ git clone https://github.com/sillsdev/liblcm.git Localizations/LCM ### Full Build (Recommended) ```powershell -# Open VS Developer Command Prompt (x64) or run: +# Open VS Developer Command Prompt or run: # & "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 # Restore packages -msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64 +msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug -# Build base installer (x64 only) -msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n +# Build base installer +msbuild Build/InstallerBuild.proj /t:BuildInstaller /p:Configuration=Release /p:config=release /m /v:n ``` ### Output Location After successful build, artifacts are produced in one of these locations (bundle outputs are culture-specific under `en-US/`): -- WiX 3 default: `FLExInstaller/bin///` -- WiX 6 opt-in: `FLExInstaller/wix6/bin///` +- WiX 3 default: `FLExInstaller/bin/x64//` +- WiX 6 opt-in: `FLExInstaller/wix6/bin/x64//` ## Building a Patch Installer @@ -89,10 +89,10 @@ These can be downloaded from GitHub Releases (e.g., `build-1188`). ```powershell # Restore packages -msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64 +msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug -# Build patch installer (x64 only) -msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Release /p:Platform=x64 /p:config=release /m /v:n +# Build patch installer +msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Release /p:config=release /m /v:n ``` ### Output Location @@ -157,7 +157,7 @@ Workflows should use **WiX Toolset v6** via `WixToolset.Sdk` restored from NuGet **Cause**: NuGet restore/build tools not fully restored, or missing VS build prerequisites. **Fix**: -1. Ensure `msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug /p:Platform=x64` succeeds. +1. Ensure `msbuild Build/InstallerBuild.proj /t:RestorePackages /p:Configuration=Debug` succeeds. 2. Re-run `\Build\Agent\Setup-InstallerBuild.ps1 -ValidateOnly` and resolve any reported issues. 2. Add WiX bin directory to PATH: `C:\Program Files (x86)\WiX Toolset v3.14\bin` diff --git a/Docs/traversal-sdk-migration.md b/Docs/traversal-sdk-migration.md index 34960781d3..7ad970ab97 100644 --- a/Docs/traversal-sdk-migration.md +++ b/Docs/traversal-sdk-migration.md @@ -86,10 +86,10 @@ Installer builds use traversal internally but are invoked via MSBuild targets: ```powershell # Base installer (calls traversal build via Installer.targets) -msbuild Build/InstallerBuild.proj /t:BuildBaseInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release +msbuild Build/InstallerBuild.proj /t:BuildBaseInstaller /p:Configuration=Debug /p:config=release # Patch installer (calls traversal build via Installer.targets) -msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release +msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:config=release ``` Note: The installer targets in `Build/Installer.targets` have been modernized to call `FieldWorks.proj` instead of the old `remakefw` target. @@ -112,7 +112,7 @@ msbuild Src/xWorks/xWorks.csproj /p:Configuration=Debug **Solution**: Build native components first: ```powershell -msbuild Build\Src\NativeBuild\NativeBuild.csproj /p:Configuration=Debug /p:Platform=x64 +msbuild Build\Src\NativeBuild\NativeBuild.csproj /p:Configuration=Debug .\build.ps1 ``` diff --git a/FieldWorks.LocalLcm.sln b/FieldWorks.LocalLcm.sln new file mode 100644 index 0000000000..cf6d252c8a --- /dev/null +++ b/FieldWorks.LocalLcm.sln @@ -0,0 +1,1028 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36401.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheLightTests", "Src\CacheLight\CacheLightTests\CacheLightTests.csproj", "{6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertLib", "Lib\src\Converter\Convertlib\ConvertLib.csproj", "{7827DE67-1E76-5DFA-B3E7-122B2A5B2472}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertSFM", "Src\Utilities\SfmToXml\ConvertSFM\ConvertSFM.csproj", "{EB470157-7A33-5263-951E-2190FC2AD626}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Converter", "Lib\src\Converter\Converter\Converter.csproj", "{B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConverterConsole", "Lib\src\Converter\ConvertConsole\ConverterConsole.csproj", "{01C9D37F-BCFA-5353-A980-84EFD3821F8A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Design", "Src\Common\Controls\Design\Design.csproj", "{762BD8EC-F9B2-5927-BC21-9D31D5A14C10}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DetailControls", "Src\Common\Controls\DetailControls\DetailControls.csproj", "{43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DetailControlsTests", "Src\Common\Controls\DetailControls\DetailControlsTests\DetailControlsTests.csproj", "{36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discourse", "Src\LexText\Discourse\Discourse.csproj", "{A51BAFC3-1649-584D-8D25-101884EE9EAA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscourseTests", "Src\LexText\Discourse\DiscourseTests\DiscourseTests.csproj", "{1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FdoUi", "Src\FdoUi\FdoUi.csproj", "{D826C3DF-3501-5F31-BC84-24493A500F9D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FdoUiTests", "Src\FdoUi\FdoUiTests\FdoUiTests.csproj", "{33123A2A-FD82-5134-B385-ADAC0A433B85}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FieldWorks", "Src\Common\FieldWorks\FieldWorks.csproj", "{5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FieldWorksTests", "Src\Common\FieldWorks\FieldWorksTests\FieldWorksTests.csproj", "{DCA3866E-E101-5BBC-9E35-60E632A4EF24}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Filters", "Src\Common\Filters\Filters.csproj", "{9C375199-FB95-5FB0-A5F3-B1E68C447C49}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FiltersTests", "Src\Common\Filters\FiltersTests\FiltersTests.csproj", "{D7281406-A9A3-5B80-95CB-23D223A0FD2D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixFwData", "Src\Utilities\FixFwData\FixFwData.csproj", "{E6B2CDCC-E016-5328-AA87-BC095712FDE6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixFwDataDll", "Src\Utilities\FixFwDataDll\FixFwDataDll.csproj", "{AA147037-F6BB-5556-858E-FC03DE028A37}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexPathwayPlugin", "Src\LexText\FlexPathwayPlugin\FlexPathwayPlugin.csproj", "{BC6E6932-35C6-55F7-8638-89F6C7DCA43A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexPathwayPluginTests", "Src\LexText\FlexPathwayPlugin\FlexPathwayPluginTests\FlexPathwayPluginTests.csproj", "{221A2FA1-1710-5537-A125-5BE856B949CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexUIAdapter", "Src\XCore\FlexUIAdapter\FlexUIAdapter.csproj", "{B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FormLanguageSwitch", "Lib\src\FormLanguageSwitch\FormLanguageSwitch.csproj", "{016A743C-BD3C-523B-B5BC-E3791D3C49E3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Framework", "Src\Common\Framework\Framework.csproj", "{3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Src\Common\Framework\FrameworkTests\FrameworkTests.csproj", "{CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwBuildTasks", "Build\Src\FwBuildTasks\FwBuildTasks.csproj", "{D5BC4B46-5126-563F-9537-B8FA5F573E55}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwControls", "Src\Common\Controls\FwControls\FwControls.csproj", "{6E80DBC7-731A-5918-8767-9A402EC483E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwControlsTests", "Src\Common\Controls\FwControls\FwControlsTests\FwControlsTests.csproj", "{1EF0C15D-DF42-5457-841A-2F220B77304D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgControls", "Src\FwCoreDlgs\FwCoreDlgControls\FwCoreDlgControls.csproj", "{28A7428D-3BA0-576C-A7B6-BA998439A036}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgControlsTests", "Src\FwCoreDlgs\FwCoreDlgControls\FwCoreDlgControlsTests\FwCoreDlgControlsTests.csproj", "{74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgs", "Src\FwCoreDlgs\FwCoreDlgs.csproj", "{5E16031F-2584-55B4-86B8-B42D7EEE8F25}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgsTests", "Src\FwCoreDlgs\FwCoreDlgsTests\FwCoreDlgsTests.csproj", "{B46A3242-AAB2-5984-9F88-C65B7537D558}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwParatextLexiconPlugin", "Src\FwParatextLexiconPlugin\FwParatextLexiconPlugin.csproj", "{40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwParatextLexiconPluginTests", "Src\FwParatextLexiconPlugin\FwParatextLexiconPluginTests\FwParatextLexiconPluginTests.csproj", "{FE438201-74A1-5236-AE07-E502B853EA18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwResources", "Src\FwResources\FwResources.csproj", "{C7533C60-BF48-5844-8220-A488387AC016}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwUtils", "Src\Common\FwUtils\FwUtils.csproj", "{DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwUtilsTests", "Src\Common\FwUtils\FwUtilsTests\FwUtilsTests.csproj", "{A39B87BF-6846-559A-A01F-6251A0FE856E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxtDll", "Src\FXT\FxtDll\FxtDll.csproj", "{DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxtDllTests", "Src\FXT\FxtDll\FxtDllTests\FxtDllTests.csproj", "{3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateHCConfig", "Src\GenerateHCConfig\GenerateHCConfig.csproj", "{644A443A-1066-57D2-9DFA-35CD9E9A46BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITextDll", "Src\LexText\Interlinear\ITextDll.csproj", "{ABC70BB4-125D-54DD-B962-6131F490AB10}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITextDllTests", "Src\LexText\Interlinear\ITextDllTests\ITextDllTests.csproj", "{6DA137DD-449E-57F1-8489-686CC307A561}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstallValidator", "Src\InstallValidator\InstallValidator.csproj", "{A2FDE99A-204A-5C10-995F-FD56039385C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstallValidatorTests", "Src\InstallValidator\InstallValidatorTests\InstallValidatorTests.csproj", "{43D44B32-899D-511D-9CF6-18CF7D3844CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCMBrowser", "Src\LCMBrowser\LCMBrowser.csproj", "{1F87EA7A-211A-562D-95ED-00F935966948}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexEdDll", "Src\LexText\Lexicon\LexEdDll.csproj", "{6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexEdDllTests", "Src\LexText\Lexicon\LexEdDllTests\LexEdDllTests.csproj", "{0434B036-FB8A-58B1-A075-B3D2D94BF492}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextControls", "Src\LexText\LexTextControls\LexTextControls.csproj", "{FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextControlsTests", "Src\LexText\LexTextControls\LexTextControlsTests\LexTextControlsTests.csproj", "{3C904B25-FE98-55A8-A9AB-2CBA065AE297}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextDll", "Src\LexText\LexTextDll\LexTextDll.csproj", "{44E4C722-DCE1-5A8A-A586-81D329771F66}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextDllTests", "Src\LexText\LexTextDll\LexTextDllTests\LexTextDllTests.csproj", "{D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MGA", "Src\LexText\Morphology\MGA\MGA.csproj", "{1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MGATests", "Src\LexText\Morphology\MGA\MGATests\MGATests.csproj", "{78FB823E-35FE-5D1D-B44D-17C22FDF6003}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedLgIcuCollator", "Src\ManagedLgIcuCollator\ManagedLgIcuCollator.csproj", "{8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedLgIcuCollatorTests", "Src\ManagedLgIcuCollator\ManagedLgIcuCollatorTests\ManagedLgIcuCollatorTests.csproj", "{65C872FA-2DC7-5EC2-9A19-EDB4FA325934}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwDrawRootBuffered", "Src\ManagedVwDrawRootBuffered\ManagedVwDrawRootBuffered.csproj", "{BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwWindow", "Src\ManagedVwWindow\ManagedVwWindow.csproj", "{5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwWindowTests", "Src\ManagedVwWindow\ManagedVwWindowTests\ManagedVwWindowTests.csproj", "{FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessageBoxExLib", "Src\Utilities\MessageBoxExLib\MessageBoxExLib.csproj", "{C5AA04DD-F91B-5156-BD40-4A761058AC64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessageBoxExLibTests", "Src\Utilities\MessageBoxExLib\MessageBoxExLibTests\MessageBoxExLibTests.csproj", "{F2525F78-38CD-5E36-A854-E16BE8A1B8FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MigrateSqlDbs", "Src\MigrateSqlDbs\MigrateSqlDbs.csproj", "{170E9760-4036-5CC4-951D-DAFDBCEF7BEA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorphologyEditorDll", "Src\LexText\Morphology\MorphologyEditorDll.csproj", "{DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorphologyEditorDllTests", "Src\LexText\Morphology\MorphologyEditorDllTests\MorphologyEditorDllTests.csproj", "{83DC33D4-9323-56B1-865A-56CD516EE52A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnitReport", "Build\Src\NUnitReport\NUnitReport.csproj", "{DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObjectBrowser", "Lib\src\ObjectBrowser\ObjectBrowser.csproj", "{1B8FE336-2272-5424-A36A-7C786F9FE388}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paratext8Plugin", "Src\Paratext8Plugin\Paratext8Plugin.csproj", "{BF01268F-E755-5577-B8D7-9014D7591A2A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paratext8PluginTests", "Src\Paratext8Plugin\ParaText8PluginTests\Paratext8PluginTests.csproj", "{4B95DD96-AB0A-571E-81E8-3035ECCC8D47}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParatextImport", "Src\ParatextImport\ParatextImport.csproj", "{21F54BD0-152A-547C-A940-2BCFEA8D1730}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParatextImportTests", "Src\ParatextImport\ParatextImportTests\ParatextImportTests.csproj", "{66361165-1489-5B17-8969-4A6253C00931}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserCore", "Src\LexText\ParserCore\ParserCore.csproj", "{1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserCoreTests", "Src\LexText\ParserCore\ParserCoreTests\ParserCoreTests.csproj", "{E5F82767-7DC7-599F-BC29-AAFE4AC98060}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserUI", "Src\LexText\ParserUI\ParserUI.csproj", "{09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserUITests", "Src\LexText\ParserUI\ParserUITests\ParserUITests.csproj", "{2310A14E-5FFA-5939-885C-DA681EAFC168}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectUnpacker", "Src\ProjectUnpacker\ProjectUnpacker.csproj", "{3E1BAF09-02C0-55BF-8683-3FAACFE6F137}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reporting", "Src\Utilities\Reporting\Reporting.csproj", "{8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSite", "Src\Common\RootSite\RootSite.csproj", "{94AD32DE-8AA2-547E-90F9-99169687406F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSiteTests", "Src\Common\RootSite\RootSiteTests\RootSiteTests.csproj", "{EC934204-1D3A-5575-A500-CB7923C440E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecks", "Lib\src\ScrChecks\ScrChecks.csproj", "{0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecksTests", "Lib\src\ScrChecks\ScrChecksTests\ScrChecksTests.csproj", "{37555756-6D42-5E46-B455-E58E3D1E8E0C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtils", "Src\Common\ScriptureUtils\ScriptureUtils.csproj", "{8336DC7C-954B-5076-9315-D7DC5317282B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtilsTests", "Src\Common\ScriptureUtils\ScriptureUtilsTests\ScriptureUtilsTests.csproj", "{04546E35-9A3A-5629-8282-3683A5D848F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sfm2Xml", "Src\Utilities\SfmToXml\Sfm2Xml.csproj", "{7C859385-3602-59D1-9A7E-E81E7C6EBBE4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sfm2XmlTests", "Src\Utilities\SfmToXml\Sfm2XmlTests\Sfm2XmlTests.csproj", "{46A84616-92E0-567E-846E-DF0C203CF0D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SfmStats", "Src\Utilities\SfmStats\SfmStats.csproj", "{910ED78F-AE00-5547-ADEC-A0E54BF98B8D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SilSidePane", "Src\XCore\SilSidePane\SilSidePane.csproj", "{68C6DB83-7D0F-5F31-9307-6489E21F74E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SilSidePaneTests", "Src\XCore\SilSidePane\SilSidePaneTests\SilSidePaneTests.csproj", "{E63B6F76-5CD3-5757-93D7-E050CB412F23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleRootSite", "Src\Common\SimpleRootSite\SimpleRootSite.csproj", "{712CF492-5D74-5464-93CA-EAB5BE54D09B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleRootSiteTests", "Src\Common\SimpleRootSite\SimpleRootSiteTests\SimpleRootSiteTests.csproj", "{D2BAD63B-0914-5014-BCE8-8D767A871F06}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UIAdapterInterfaces", "Src\Common\UIAdapterInterfaces\UIAdapterInterfaces.csproj", "{98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicodeCharEditor", "Src\UnicodeCharEditor\UnicodeCharEditor.csproj", "{FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicodeCharEditorTests", "Src\UnicodeCharEditor\UnicodeCharEditorTests\UnicodeCharEditorTests.csproj", "{515DEC49-6C0F-5F02-AC05-69AC6AF51639}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewsInterfaces", "Src\Common\ViewsInterfaces\ViewsInterfaces.csproj", "{70163155-93C1-5816-A1D4-1EEA0215298C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewsInterfacesTests", "Src\Common\ViewsInterfaces\ViewsInterfacesTests\ViewsInterfacesTests.csproj", "{EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VwGraphicsReplayer", "Src\views\lib\VwGraphicsReplayer\VwGraphicsReplayer.csproj", "{AB011392-76C6-5D67-9623-CA9B2680B899}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Widgets", "Src\Common\Controls\Widgets\Widgets.csproj", "{3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WidgetsTests", "Src\Common\Controls\Widgets\WidgetsTests\WidgetsTests.csproj", "{17AE7011-A346-5BAE-A021-552E7A3A86DD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAmpleManagedWrapper", "Src\LexText\ParserCore\XAmpleManagedWrapper\XAmpleManagedWrapper.csproj", "{6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAmpleManagedWrapperTests", "Src\LexText\ParserCore\XAmpleManagedWrapper\XAmpleManagedWrapperTests\XAmpleManagedWrapperTests.csproj", "{5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComManifestTestHost", "Src\Utilities\ComManifestTestHost\ComManifestTestHost.csproj", "{9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLUtils", "Src\Utilities\XMLUtils\XMLUtils.csproj", "{D4F47DD8-A0E7-5081-808A-5286F873DC13}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLUtilsTests", "Src\Utilities\XMLUtils\XMLUtilsTests\XMLUtilsTests.csproj", "{2EB628C9-EC23-5394-8BEB-B7542360FEAE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLViews", "Src\Common\Controls\XMLViews\XMLViews.csproj", "{B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLViewsTests", "Src\Common\Controls\XMLViews\XMLViewsTests\XMLViewsTests.csproj", "{DA1CAEE2-340C-51E7-980B-916545074600}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCore", "Src\XCore\xCore.csproj", "{B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreInterfaces", "Src\XCore\xCoreInterfaces\xCoreInterfaces.csproj", "{1C758320-DE0A-50F3-8892-B0F7397CFA61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreInterfacesTests", "Src\XCore\xCoreInterfaces\xCoreInterfacesTests\xCoreInterfacesTests.csproj", "{9B1C17E4-3086-53B9-B1DC-8A39117E237F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreTests", "Src\XCore\xCoreTests\xCoreTests.csproj", "{2861A99F-3390-52B4-A2D8-0F80A62DB108}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xWorks", "Src\xWorks\xWorks.csproj", "{5B1DFFF7-6A59-5955-B77D-42DBF12721D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xWorksTests", "Src\xWorks\xWorksTests\xWorksTests.csproj", "{1308E147-8B51-55E0-B475-10A0053F9AAF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Generic", "Src\Generic\Generic.vcxproj", "{7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Kernel", "Src\Kernel\Kernel.vcxproj", "{6396B488-4D34-48B2-8639-EEB90707405B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "views", "Src\views\views.vcxproj", "{C86CA2EB-81B5-4411-B5B7-E983314E02DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheLight", "Src\CacheLight\CacheLight.csproj", "{34442A32-31DE-45A8-AD36-0ECFE4095523}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FXT", "FXT", "{6D69D131-C928-6A46-F508-A4A608CBE30A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FxtExe", "Src\FXT\FxtExe\FxtExe.csproj", "{8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallerArtifactsTests", "Src\InstallValidator\InstallerArtifactsTests\InstallerArtifactsTests.csproj", "{8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossLib", "Src\Utilities\HCSynthByGloss\HCSynthByGlossLib\HCSynthByGlossLib.csproj", "{AF250D69-786B-40FA-A125-FD3F448CC283}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateHCConfigForFLExTrans", "Src\Utilities\HCSynthByGloss\GenerateHCConfig4FLExTrans\GenerateHCConfigForFLExTrans.csproj", "{91D55536-1DE3-4279-9DD1-CA2CED068F42}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGloss", "Src\Utilities\HCSynthByGloss\HCSynthByGloss\HCSynthByGloss.csproj", "{BF5AD9CA-6FD6-49C7-B351-0630C11479C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossDll", "Src\Utilities\HCSynthByGloss\HCSynthByGlossDll\HCSynthByGlossDll.csproj", "{EEE765C8-6812-4F9F-A100-42AA71921926}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossDllTest", "Src\Utilities\HCSynthByGloss\HCSynthByGlossDll\HCSynthByGlossDllTest\HCSynthByGlossDllTest.csproj", "{8EF1E1AE-2226-4A9B-8942-CAB531956ED3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossTest", "Src\Utilities\HCSynthByGloss\HCSynthByGloss\HCSynthByGlossTest\HCSynthByGlossTest.csproj", "{5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Build.Tasks", "Localizations\LCM\src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj", "{5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel", "Localizations\LCM\src\SIL.LCModel\SIL.LCModel.csproj", "{E5E9DDC7-2855-4D92-AD46-960AC4C46457}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Core", "Localizations\LCM\src\SIL.LCModel.Core\SIL.LCModel.Core.csproj", "{4C7D6B65-A331-4ED7-9B53-3301E714F8E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.FixData", "Localizations\LCM\src\SIL.LCModel.FixData\SIL.LCModel.FixData.csproj", "{4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Utils", "Localizations\LCM\src\SIL.LCModel.Utils\SIL.LCModel.Utils.csproj", "{4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Bounds|x64 = Bounds|x64 + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Bounds|x64.ActiveCfg = Release|x64 + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Bounds|x64.Build.0 = Release|x64 + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Debug|x64.ActiveCfg = Debug|x64 + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Debug|x64.Build.0 = Debug|x64 + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Release|x64.ActiveCfg = Release|x64 + {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Release|x64.Build.0 = Release|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Bounds|x64.ActiveCfg = Release|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Bounds|x64.Build.0 = Release|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Debug|x64.ActiveCfg = Debug|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Debug|x64.Build.0 = Debug|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Release|x64.ActiveCfg = Release|x64 + {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Release|x64.Build.0 = Release|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Bounds|x64.ActiveCfg = Release|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Bounds|x64.Build.0 = Release|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Debug|x64.ActiveCfg = Debug|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Debug|x64.Build.0 = Debug|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Release|x64.ActiveCfg = Release|x64 + {EB470157-7A33-5263-951E-2190FC2AD626}.Release|x64.Build.0 = Release|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Bounds|x64.ActiveCfg = Release|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Bounds|x64.Build.0 = Release|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Debug|x64.ActiveCfg = Debug|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Debug|x64.Build.0 = Debug|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Release|x64.ActiveCfg = Release|x64 + {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Release|x64.Build.0 = Release|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Bounds|x64.ActiveCfg = Release|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Bounds|x64.Build.0 = Release|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Debug|x64.ActiveCfg = Debug|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Debug|x64.Build.0 = Debug|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Release|x64.ActiveCfg = Release|x64 + {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Release|x64.Build.0 = Release|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Bounds|x64.ActiveCfg = Release|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Bounds|x64.Build.0 = Release|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Debug|x64.ActiveCfg = Debug|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Debug|x64.Build.0 = Debug|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Release|x64.ActiveCfg = Release|x64 + {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Release|x64.Build.0 = Release|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Bounds|x64.ActiveCfg = Release|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Bounds|x64.Build.0 = Release|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Debug|x64.ActiveCfg = Debug|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Debug|x64.Build.0 = Debug|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Release|x64.ActiveCfg = Release|x64 + {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Release|x64.Build.0 = Release|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Bounds|x64.ActiveCfg = Release|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Bounds|x64.Build.0 = Release|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Debug|x64.ActiveCfg = Debug|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Debug|x64.Build.0 = Debug|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Release|x64.ActiveCfg = Release|x64 + {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Release|x64.Build.0 = Release|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Bounds|x64.ActiveCfg = Release|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Bounds|x64.Build.0 = Release|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Debug|x64.ActiveCfg = Debug|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Debug|x64.Build.0 = Debug|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Release|x64.ActiveCfg = Release|x64 + {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Release|x64.Build.0 = Release|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Bounds|x64.ActiveCfg = Release|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Bounds|x64.Build.0 = Release|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Debug|x64.ActiveCfg = Debug|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Debug|x64.Build.0 = Debug|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Release|x64.ActiveCfg = Release|x64 + {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Release|x64.Build.0 = Release|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Bounds|x64.ActiveCfg = Release|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Bounds|x64.Build.0 = Release|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Debug|x64.ActiveCfg = Debug|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Debug|x64.Build.0 = Debug|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Release|x64.ActiveCfg = Release|x64 + {D826C3DF-3501-5F31-BC84-24493A500F9D}.Release|x64.Build.0 = Release|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Bounds|x64.ActiveCfg = Release|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Bounds|x64.Build.0 = Release|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Debug|x64.ActiveCfg = Debug|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Debug|x64.Build.0 = Debug|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Release|x64.ActiveCfg = Release|x64 + {33123A2A-FD82-5134-B385-ADAC0A433B85}.Release|x64.Build.0 = Release|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Bounds|x64.ActiveCfg = Release|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Bounds|x64.Build.0 = Release|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Debug|x64.ActiveCfg = Debug|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Debug|x64.Build.0 = Debug|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Release|x64.ActiveCfg = Release|x64 + {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Release|x64.Build.0 = Release|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Bounds|x64.ActiveCfg = Release|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Bounds|x64.Build.0 = Release|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Debug|x64.ActiveCfg = Debug|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Debug|x64.Build.0 = Debug|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Release|x64.ActiveCfg = Release|x64 + {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Release|x64.Build.0 = Release|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Bounds|x64.ActiveCfg = Release|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Bounds|x64.Build.0 = Release|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Debug|x64.ActiveCfg = Debug|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Debug|x64.Build.0 = Debug|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Release|x64.ActiveCfg = Release|x64 + {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Release|x64.Build.0 = Release|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Bounds|x64.ActiveCfg = Release|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Bounds|x64.Build.0 = Release|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Debug|x64.ActiveCfg = Debug|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Debug|x64.Build.0 = Debug|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Release|x64.ActiveCfg = Release|x64 + {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Release|x64.Build.0 = Release|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Bounds|x64.ActiveCfg = Release|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Bounds|x64.Build.0 = Release|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Debug|x64.ActiveCfg = Debug|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Debug|x64.Build.0 = Debug|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Release|x64.ActiveCfg = Release|x64 + {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Release|x64.Build.0 = Release|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Bounds|x64.ActiveCfg = Release|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Bounds|x64.Build.0 = Release|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Debug|x64.ActiveCfg = Debug|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Debug|x64.Build.0 = Debug|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Release|x64.ActiveCfg = Release|x64 + {AA147037-F6BB-5556-858E-FC03DE028A37}.Release|x64.Build.0 = Release|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Bounds|x64.ActiveCfg = Release|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Bounds|x64.Build.0 = Release|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Debug|x64.ActiveCfg = Debug|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Debug|x64.Build.0 = Debug|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Release|x64.ActiveCfg = Release|x64 + {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Release|x64.Build.0 = Release|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Bounds|x64.ActiveCfg = Release|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Bounds|x64.Build.0 = Release|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Debug|x64.ActiveCfg = Debug|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Debug|x64.Build.0 = Debug|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Release|x64.ActiveCfg = Release|x64 + {221A2FA1-1710-5537-A125-5BE856B949CC}.Release|x64.Build.0 = Release|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Bounds|x64.ActiveCfg = Release|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Bounds|x64.Build.0 = Release|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Debug|x64.ActiveCfg = Debug|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Debug|x64.Build.0 = Debug|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Release|x64.ActiveCfg = Release|x64 + {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Release|x64.Build.0 = Release|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Bounds|x64.ActiveCfg = Release|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Bounds|x64.Build.0 = Release|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Debug|x64.ActiveCfg = Debug|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Debug|x64.Build.0 = Debug|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Release|x64.ActiveCfg = Release|x64 + {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Release|x64.Build.0 = Release|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Bounds|x64.ActiveCfg = Release|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Bounds|x64.Build.0 = Release|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Debug|x64.ActiveCfg = Debug|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Debug|x64.Build.0 = Debug|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Release|x64.ActiveCfg = Release|x64 + {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Release|x64.Build.0 = Release|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Bounds|x64.ActiveCfg = Release|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Bounds|x64.Build.0 = Release|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Debug|x64.ActiveCfg = Debug|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Debug|x64.Build.0 = Debug|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Release|x64.ActiveCfg = Release|x64 + {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Release|x64.Build.0 = Release|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Bounds|x64.ActiveCfg = Release|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Bounds|x64.Build.0 = Release|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Debug|x64.ActiveCfg = Debug|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Debug|x64.Build.0 = Debug|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Release|x64.ActiveCfg = Release|x64 + {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Release|x64.Build.0 = Release|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Bounds|x64.ActiveCfg = Release|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Bounds|x64.Build.0 = Release|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Debug|x64.ActiveCfg = Debug|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Debug|x64.Build.0 = Debug|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Release|x64.ActiveCfg = Release|x64 + {6E80DBC7-731A-5918-8767-9A402EC483E6}.Release|x64.Build.0 = Release|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Bounds|x64.ActiveCfg = Release|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Bounds|x64.Build.0 = Release|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Debug|x64.ActiveCfg = Debug|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Debug|x64.Build.0 = Debug|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Release|x64.ActiveCfg = Release|x64 + {1EF0C15D-DF42-5457-841A-2F220B77304D}.Release|x64.Build.0 = Release|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Bounds|x64.ActiveCfg = Release|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Bounds|x64.Build.0 = Release|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Debug|x64.ActiveCfg = Debug|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Debug|x64.Build.0 = Debug|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Release|x64.ActiveCfg = Release|x64 + {28A7428D-3BA0-576C-A7B6-BA998439A036}.Release|x64.Build.0 = Release|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Bounds|x64.ActiveCfg = Release|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Bounds|x64.Build.0 = Release|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Debug|x64.ActiveCfg = Debug|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Debug|x64.Build.0 = Debug|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Release|x64.ActiveCfg = Release|x64 + {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Release|x64.Build.0 = Release|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Bounds|x64.ActiveCfg = Release|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Bounds|x64.Build.0 = Release|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Debug|x64.ActiveCfg = Debug|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Debug|x64.Build.0 = Debug|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Release|x64.ActiveCfg = Release|x64 + {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Release|x64.Build.0 = Release|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Bounds|x64.ActiveCfg = Release|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Bounds|x64.Build.0 = Release|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Debug|x64.ActiveCfg = Debug|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Debug|x64.Build.0 = Debug|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Release|x64.ActiveCfg = Release|x64 + {B46A3242-AAB2-5984-9F88-C65B7537D558}.Release|x64.Build.0 = Release|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Bounds|x64.ActiveCfg = Release|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Bounds|x64.Build.0 = Release|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Debug|x64.ActiveCfg = Debug|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Debug|x64.Build.0 = Debug|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Release|x64.ActiveCfg = Release|x64 + {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Release|x64.Build.0 = Release|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Bounds|x64.ActiveCfg = Release|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Bounds|x64.Build.0 = Release|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Debug|x64.ActiveCfg = Debug|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Debug|x64.Build.0 = Debug|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Release|x64.ActiveCfg = Release|x64 + {FE438201-74A1-5236-AE07-E502B853EA18}.Release|x64.Build.0 = Release|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Bounds|x64.ActiveCfg = Release|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Bounds|x64.Build.0 = Release|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Debug|x64.ActiveCfg = Debug|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Debug|x64.Build.0 = Debug|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Release|x64.ActiveCfg = Release|x64 + {C7533C60-BF48-5844-8220-A488387AC016}.Release|x64.Build.0 = Release|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Bounds|x64.ActiveCfg = Release|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Bounds|x64.Build.0 = Release|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Debug|x64.ActiveCfg = Debug|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Debug|x64.Build.0 = Debug|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Release|x64.ActiveCfg = Release|x64 + {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Release|x64.Build.0 = Release|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Bounds|x64.ActiveCfg = Release|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Bounds|x64.Build.0 = Release|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Debug|x64.ActiveCfg = Debug|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Debug|x64.Build.0 = Debug|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Release|x64.ActiveCfg = Release|x64 + {A39B87BF-6846-559A-A01F-6251A0FE856E}.Release|x64.Build.0 = Release|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Bounds|x64.ActiveCfg = Release|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Bounds|x64.Build.0 = Release|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Debug|x64.ActiveCfg = Debug|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Debug|x64.Build.0 = Debug|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Release|x64.ActiveCfg = Release|x64 + {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Release|x64.Build.0 = Release|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Bounds|x64.ActiveCfg = Release|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Bounds|x64.Build.0 = Release|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Debug|x64.ActiveCfg = Debug|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Debug|x64.Build.0 = Debug|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Release|x64.ActiveCfg = Release|x64 + {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Release|x64.Build.0 = Release|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Bounds|x64.ActiveCfg = Release|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Bounds|x64.Build.0 = Release|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Debug|x64.ActiveCfg = Debug|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Debug|x64.Build.0 = Debug|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Release|x64.ActiveCfg = Release|x64 + {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Release|x64.Build.0 = Release|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Bounds|x64.ActiveCfg = Release|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Bounds|x64.Build.0 = Release|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Debug|x64.ActiveCfg = Debug|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Debug|x64.Build.0 = Debug|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Release|x64.ActiveCfg = Release|x64 + {ABC70BB4-125D-54DD-B962-6131F490AB10}.Release|x64.Build.0 = Release|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Bounds|x64.ActiveCfg = Release|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Bounds|x64.Build.0 = Release|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Debug|x64.ActiveCfg = Debug|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Debug|x64.Build.0 = Debug|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Release|x64.ActiveCfg = Release|x64 + {6DA137DD-449E-57F1-8489-686CC307A561}.Release|x64.Build.0 = Release|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Bounds|x64.ActiveCfg = Release|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Bounds|x64.Build.0 = Release|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Debug|x64.ActiveCfg = Debug|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Debug|x64.Build.0 = Debug|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Release|x64.ActiveCfg = Release|x64 + {A2FDE99A-204A-5C10-995F-FD56039385C8}.Release|x64.Build.0 = Release|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Bounds|x64.ActiveCfg = Release|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Bounds|x64.Build.0 = Release|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Debug|x64.ActiveCfg = Debug|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Debug|x64.Build.0 = Debug|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Release|x64.ActiveCfg = Release|x64 + {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Release|x64.Build.0 = Release|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Bounds|x64.ActiveCfg = Release|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Bounds|x64.Build.0 = Release|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Debug|x64.ActiveCfg = Debug|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Debug|x64.Build.0 = Debug|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Release|x64.ActiveCfg = Release|x64 + {1F87EA7A-211A-562D-95ED-00F935966948}.Release|x64.Build.0 = Release|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Bounds|x64.ActiveCfg = Release|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Bounds|x64.Build.0 = Release|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Debug|x64.ActiveCfg = Debug|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Debug|x64.Build.0 = Debug|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Release|x64.ActiveCfg = Release|x64 + {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Release|x64.Build.0 = Release|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Bounds|x64.ActiveCfg = Release|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Bounds|x64.Build.0 = Release|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Debug|x64.ActiveCfg = Debug|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Debug|x64.Build.0 = Debug|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Release|x64.ActiveCfg = Release|x64 + {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Release|x64.Build.0 = Release|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Bounds|x64.ActiveCfg = Release|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Bounds|x64.Build.0 = Release|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Debug|x64.ActiveCfg = Debug|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Debug|x64.Build.0 = Debug|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Release|x64.ActiveCfg = Release|x64 + {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Release|x64.Build.0 = Release|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Bounds|x64.ActiveCfg = Release|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Bounds|x64.Build.0 = Release|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Debug|x64.ActiveCfg = Debug|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Debug|x64.Build.0 = Debug|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Release|x64.ActiveCfg = Release|x64 + {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Release|x64.Build.0 = Release|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Bounds|x64.ActiveCfg = Release|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Bounds|x64.Build.0 = Release|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Debug|x64.ActiveCfg = Debug|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Debug|x64.Build.0 = Debug|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Release|x64.ActiveCfg = Release|x64 + {44E4C722-DCE1-5A8A-A586-81D329771F66}.Release|x64.Build.0 = Release|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Bounds|x64.ActiveCfg = Release|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Bounds|x64.Build.0 = Release|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Debug|x64.ActiveCfg = Debug|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Debug|x64.Build.0 = Debug|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Release|x64.ActiveCfg = Release|x64 + {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Release|x64.Build.0 = Release|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Bounds|x64.ActiveCfg = Release|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Bounds|x64.Build.0 = Release|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Debug|x64.ActiveCfg = Debug|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Debug|x64.Build.0 = Debug|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Release|x64.ActiveCfg = Release|x64 + {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Release|x64.Build.0 = Release|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Bounds|x64.ActiveCfg = Release|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Bounds|x64.Build.0 = Release|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Debug|x64.ActiveCfg = Debug|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Debug|x64.Build.0 = Debug|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Release|x64.ActiveCfg = Release|x64 + {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Release|x64.Build.0 = Release|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Bounds|x64.ActiveCfg = Release|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Bounds|x64.Build.0 = Release|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Debug|x64.ActiveCfg = Debug|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Debug|x64.Build.0 = Debug|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Release|x64.ActiveCfg = Release|x64 + {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Release|x64.Build.0 = Release|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Bounds|x64.ActiveCfg = Release|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Bounds|x64.Build.0 = Release|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Debug|x64.ActiveCfg = Debug|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Debug|x64.Build.0 = Debug|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Release|x64.ActiveCfg = Release|x64 + {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Release|x64.Build.0 = Release|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Bounds|x64.ActiveCfg = Release|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Bounds|x64.Build.0 = Release|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Debug|x64.ActiveCfg = Debug|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Debug|x64.Build.0 = Debug|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Release|x64.ActiveCfg = Release|x64 + {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Release|x64.Build.0 = Release|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Bounds|x64.ActiveCfg = Release|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Bounds|x64.Build.0 = Release|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Debug|x64.ActiveCfg = Debug|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Debug|x64.Build.0 = Debug|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Release|x64.ActiveCfg = Release|x64 + {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Release|x64.Build.0 = Release|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Bounds|x64.ActiveCfg = Release|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Bounds|x64.Build.0 = Release|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Debug|x64.ActiveCfg = Debug|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Debug|x64.Build.0 = Debug|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Release|x64.ActiveCfg = Release|x64 + {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Release|x64.Build.0 = Release|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Bounds|x64.ActiveCfg = Release|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Bounds|x64.Build.0 = Release|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Debug|x64.ActiveCfg = Debug|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Debug|x64.Build.0 = Debug|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Release|x64.ActiveCfg = Release|x64 + {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Release|x64.Build.0 = Release|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Bounds|x64.ActiveCfg = Release|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Bounds|x64.Build.0 = Release|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Debug|x64.ActiveCfg = Debug|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Debug|x64.Build.0 = Debug|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Release|x64.ActiveCfg = Release|x64 + {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Release|x64.Build.0 = Release|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Bounds|x64.ActiveCfg = Release|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Bounds|x64.Build.0 = Release|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Debug|x64.ActiveCfg = Debug|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Debug|x64.Build.0 = Debug|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Release|x64.ActiveCfg = Release|x64 + {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Release|x64.Build.0 = Release|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Bounds|x64.ActiveCfg = Release|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Bounds|x64.Build.0 = Release|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Debug|x64.ActiveCfg = Debug|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Debug|x64.Build.0 = Debug|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Release|x64.ActiveCfg = Release|x64 + {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Release|x64.Build.0 = Release|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Bounds|x64.ActiveCfg = Release|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Bounds|x64.Build.0 = Release|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Debug|x64.ActiveCfg = Debug|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Debug|x64.Build.0 = Debug|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Release|x64.ActiveCfg = Release|x64 + {83DC33D4-9323-56B1-865A-56CD516EE52A}.Release|x64.Build.0 = Release|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Bounds|x64.ActiveCfg = Release|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Bounds|x64.Build.0 = Release|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Debug|x64.ActiveCfg = Debug|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Debug|x64.Build.0 = Debug|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Release|x64.ActiveCfg = Release|x64 + {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Release|x64.Build.0 = Release|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Bounds|x64.ActiveCfg = Release|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Bounds|x64.Build.0 = Release|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Debug|x64.ActiveCfg = Debug|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Debug|x64.Build.0 = Debug|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Release|x64.ActiveCfg = Release|x64 + {1B8FE336-2272-5424-A36A-7C786F9FE388}.Release|x64.Build.0 = Release|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Bounds|x64.ActiveCfg = Release|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Bounds|x64.Build.0 = Release|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Debug|x64.ActiveCfg = Debug|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Debug|x64.Build.0 = Debug|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Release|x64.ActiveCfg = Release|x64 + {BF01268F-E755-5577-B8D7-9014D7591A2A}.Release|x64.Build.0 = Release|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Bounds|x64.ActiveCfg = Release|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Bounds|x64.Build.0 = Release|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Debug|x64.ActiveCfg = Debug|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Debug|x64.Build.0 = Debug|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Release|x64.ActiveCfg = Release|x64 + {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Release|x64.Build.0 = Release|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Bounds|x64.ActiveCfg = Release|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Bounds|x64.Build.0 = Release|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Debug|x64.ActiveCfg = Debug|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Debug|x64.Build.0 = Debug|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Release|x64.ActiveCfg = Release|x64 + {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Release|x64.Build.0 = Release|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Bounds|x64.ActiveCfg = Release|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Bounds|x64.Build.0 = Release|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Debug|x64.ActiveCfg = Debug|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Debug|x64.Build.0 = Debug|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Release|x64.ActiveCfg = Release|x64 + {66361165-1489-5B17-8969-4A6253C00931}.Release|x64.Build.0 = Release|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Bounds|x64.ActiveCfg = Release|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Bounds|x64.Build.0 = Release|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Debug|x64.ActiveCfg = Debug|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Debug|x64.Build.0 = Debug|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Release|x64.ActiveCfg = Release|x64 + {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Release|x64.Build.0 = Release|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Bounds|x64.ActiveCfg = Release|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Bounds|x64.Build.0 = Release|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Debug|x64.ActiveCfg = Debug|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Debug|x64.Build.0 = Debug|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Release|x64.ActiveCfg = Release|x64 + {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Release|x64.Build.0 = Release|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Bounds|x64.ActiveCfg = Release|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Bounds|x64.Build.0 = Release|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Debug|x64.ActiveCfg = Debug|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Debug|x64.Build.0 = Debug|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Release|x64.ActiveCfg = Release|x64 + {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Release|x64.Build.0 = Release|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Bounds|x64.ActiveCfg = Release|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Bounds|x64.Build.0 = Release|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Debug|x64.ActiveCfg = Debug|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Debug|x64.Build.0 = Debug|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Release|x64.ActiveCfg = Release|x64 + {2310A14E-5FFA-5939-885C-DA681EAFC168}.Release|x64.Build.0 = Release|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Bounds|x64.ActiveCfg = Release|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Bounds|x64.Build.0 = Release|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Debug|x64.ActiveCfg = Debug|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Debug|x64.Build.0 = Debug|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Release|x64.ActiveCfg = Release|x64 + {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Release|x64.Build.0 = Release|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Bounds|x64.ActiveCfg = Release|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Bounds|x64.Build.0 = Release|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Debug|x64.ActiveCfg = Debug|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Debug|x64.Build.0 = Debug|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Release|x64.ActiveCfg = Release|x64 + {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Release|x64.Build.0 = Release|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Bounds|x64.ActiveCfg = Release|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Bounds|x64.Build.0 = Release|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Debug|x64.ActiveCfg = Debug|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Debug|x64.Build.0 = Debug|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Release|x64.ActiveCfg = Release|x64 + {94AD32DE-8AA2-547E-90F9-99169687406F}.Release|x64.Build.0 = Release|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Bounds|x64.ActiveCfg = Release|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Bounds|x64.Build.0 = Release|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Debug|x64.ActiveCfg = Debug|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Debug|x64.Build.0 = Debug|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.ActiveCfg = Release|x64 + {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.Build.0 = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.ActiveCfg = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.Build.0 = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.ActiveCfg = Debug|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.Build.0 = Debug|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.ActiveCfg = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.Build.0 = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.ActiveCfg = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.Build.0 = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.ActiveCfg = Debug|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.Build.0 = Debug|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.ActiveCfg = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.Build.0 = Release|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.ActiveCfg = Release|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.Build.0 = Release|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Debug|x64.ActiveCfg = Debug|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Debug|x64.Build.0 = Debug|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Release|x64.ActiveCfg = Release|x64 + {8336DC7C-954B-5076-9315-D7DC5317282B}.Release|x64.Build.0 = Release|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Bounds|x64.ActiveCfg = Release|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Bounds|x64.Build.0 = Release|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Debug|x64.ActiveCfg = Debug|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Debug|x64.Build.0 = Debug|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Release|x64.ActiveCfg = Release|x64 + {04546E35-9A3A-5629-8282-3683A5D848F9}.Release|x64.Build.0 = Release|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Bounds|x64.ActiveCfg = Release|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Bounds|x64.Build.0 = Release|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Debug|x64.ActiveCfg = Debug|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Debug|x64.Build.0 = Debug|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Release|x64.ActiveCfg = Release|x64 + {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Release|x64.Build.0 = Release|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Bounds|x64.ActiveCfg = Release|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Bounds|x64.Build.0 = Release|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Debug|x64.ActiveCfg = Debug|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Debug|x64.Build.0 = Debug|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Release|x64.ActiveCfg = Release|x64 + {46A84616-92E0-567E-846E-DF0C203CF0D2}.Release|x64.Build.0 = Release|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Bounds|x64.ActiveCfg = Release|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Bounds|x64.Build.0 = Release|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Debug|x64.ActiveCfg = Debug|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Debug|x64.Build.0 = Debug|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Release|x64.ActiveCfg = Release|x64 + {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Release|x64.Build.0 = Release|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Bounds|x64.ActiveCfg = Release|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Bounds|x64.Build.0 = Release|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Debug|x64.ActiveCfg = Debug|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Debug|x64.Build.0 = Debug|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Release|x64.ActiveCfg = Release|x64 + {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Release|x64.Build.0 = Release|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Bounds|x64.ActiveCfg = Release|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Bounds|x64.Build.0 = Release|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Debug|x64.ActiveCfg = Debug|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Debug|x64.Build.0 = Debug|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Release|x64.ActiveCfg = Release|x64 + {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Release|x64.Build.0 = Release|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Bounds|x64.ActiveCfg = Release|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Bounds|x64.Build.0 = Release|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Debug|x64.ActiveCfg = Debug|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Debug|x64.Build.0 = Debug|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Release|x64.ActiveCfg = Release|x64 + {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Release|x64.Build.0 = Release|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Bounds|x64.ActiveCfg = Release|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Bounds|x64.Build.0 = Release|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Debug|x64.ActiveCfg = Debug|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Debug|x64.Build.0 = Debug|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Release|x64.ActiveCfg = Release|x64 + {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Release|x64.Build.0 = Release|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Bounds|x64.ActiveCfg = Release|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Bounds|x64.Build.0 = Release|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Debug|x64.ActiveCfg = Debug|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Debug|x64.Build.0 = Debug|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Release|x64.ActiveCfg = Release|x64 + {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Release|x64.Build.0 = Release|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Bounds|x64.ActiveCfg = Release|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Bounds|x64.Build.0 = Release|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Debug|x64.ActiveCfg = Debug|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Debug|x64.Build.0 = Debug|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Release|x64.ActiveCfg = Release|x64 + {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Release|x64.Build.0 = Release|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Bounds|x64.ActiveCfg = Release|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Bounds|x64.Build.0 = Release|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Debug|x64.ActiveCfg = Debug|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Debug|x64.Build.0 = Debug|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Release|x64.ActiveCfg = Release|x64 + {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Release|x64.Build.0 = Release|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Bounds|x64.ActiveCfg = Release|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Bounds|x64.Build.0 = Release|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Debug|x64.ActiveCfg = Debug|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Debug|x64.Build.0 = Debug|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Release|x64.ActiveCfg = Release|x64 + {70163155-93C1-5816-A1D4-1EEA0215298C}.Release|x64.Build.0 = Release|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Bounds|x64.ActiveCfg = Release|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Bounds|x64.Build.0 = Release|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Debug|x64.ActiveCfg = Debug|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Debug|x64.Build.0 = Debug|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Release|x64.ActiveCfg = Release|x64 + {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Release|x64.Build.0 = Release|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Bounds|x64.ActiveCfg = Release|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Bounds|x64.Build.0 = Release|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Debug|x64.ActiveCfg = Debug|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Debug|x64.Build.0 = Debug|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Release|x64.ActiveCfg = Release|x64 + {AB011392-76C6-5D67-9623-CA9B2680B899}.Release|x64.Build.0 = Release|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Bounds|x64.ActiveCfg = Release|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Bounds|x64.Build.0 = Release|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Debug|x64.ActiveCfg = Debug|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Debug|x64.Build.0 = Debug|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Release|x64.ActiveCfg = Release|x64 + {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Release|x64.Build.0 = Release|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Bounds|x64.ActiveCfg = Release|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Bounds|x64.Build.0 = Release|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Debug|x64.ActiveCfg = Debug|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Debug|x64.Build.0 = Debug|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Release|x64.ActiveCfg = Release|x64 + {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Release|x64.Build.0 = Release|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Bounds|x64.ActiveCfg = Release|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Bounds|x64.Build.0 = Release|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Debug|x64.ActiveCfg = Debug|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Debug|x64.Build.0 = Debug|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Release|x64.ActiveCfg = Release|x64 + {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Release|x64.Build.0 = Release|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Bounds|x64.ActiveCfg = Release|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Bounds|x64.Build.0 = Release|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Debug|x64.ActiveCfg = Debug|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Debug|x64.Build.0 = Debug|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Release|x64.ActiveCfg = Release|x64 + {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Release|x64.Build.0 = Release|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Bounds|x64.ActiveCfg = Release|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Bounds|x64.Build.0 = Release|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Debug|x64.ActiveCfg = Debug|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Debug|x64.Build.0 = Debug|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Release|x64.ActiveCfg = Release|x64 + {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Release|x64.Build.0 = Release|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Bounds|x64.ActiveCfg = Release|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Bounds|x64.Build.0 = Release|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Debug|x64.ActiveCfg = Debug|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Debug|x64.Build.0 = Debug|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Release|x64.ActiveCfg = Release|x64 + {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Release|x64.Build.0 = Release|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Bounds|x64.ActiveCfg = Release|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Bounds|x64.Build.0 = Release|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Debug|x64.ActiveCfg = Debug|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Debug|x64.Build.0 = Debug|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Release|x64.ActiveCfg = Release|x64 + {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Release|x64.Build.0 = Release|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Bounds|x64.ActiveCfg = Release|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Bounds|x64.Build.0 = Release|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Debug|x64.ActiveCfg = Debug|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Debug|x64.Build.0 = Debug|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Release|x64.ActiveCfg = Release|x64 + {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Release|x64.Build.0 = Release|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Bounds|x64.ActiveCfg = Release|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Bounds|x64.Build.0 = Release|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Debug|x64.ActiveCfg = Debug|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Debug|x64.Build.0 = Debug|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Release|x64.ActiveCfg = Release|x64 + {DA1CAEE2-340C-51E7-980B-916545074600}.Release|x64.Build.0 = Release|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Bounds|x64.ActiveCfg = Release|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Bounds|x64.Build.0 = Release|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Debug|x64.ActiveCfg = Debug|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Debug|x64.Build.0 = Debug|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Release|x64.ActiveCfg = Release|x64 + {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Release|x64.Build.0 = Release|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Bounds|x64.ActiveCfg = Release|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Bounds|x64.Build.0 = Release|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Debug|x64.ActiveCfg = Debug|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Debug|x64.Build.0 = Debug|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Release|x64.ActiveCfg = Release|x64 + {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Release|x64.Build.0 = Release|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Bounds|x64.ActiveCfg = Release|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Bounds|x64.Build.0 = Release|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Debug|x64.ActiveCfg = Debug|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Debug|x64.Build.0 = Debug|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Release|x64.ActiveCfg = Release|x64 + {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Release|x64.Build.0 = Release|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Bounds|x64.ActiveCfg = Release|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Bounds|x64.Build.0 = Release|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Debug|x64.ActiveCfg = Debug|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Debug|x64.Build.0 = Debug|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Release|x64.ActiveCfg = Release|x64 + {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Release|x64.Build.0 = Release|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Bounds|x64.ActiveCfg = Release|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Bounds|x64.Build.0 = Release|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Debug|x64.ActiveCfg = Debug|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Debug|x64.Build.0 = Debug|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Release|x64.ActiveCfg = Release|x64 + {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Release|x64.Build.0 = Release|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Bounds|x64.ActiveCfg = Release|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Bounds|x64.Build.0 = Release|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Debug|x64.ActiveCfg = Debug|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Debug|x64.Build.0 = Debug|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Release|x64.ActiveCfg = Release|x64 + {1308E147-8B51-55E0-B475-10A0053F9AAF}.Release|x64.Build.0 = Release|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Bounds|x64.ActiveCfg = Bounds|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Bounds|x64.Build.0 = Bounds|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Debug|x64.ActiveCfg = Debug|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Debug|x64.Build.0 = Debug|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Release|x64.ActiveCfg = Release|x64 + {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Release|x64.Build.0 = Release|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Bounds|x64.ActiveCfg = Debug|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Bounds|x64.Build.0 = Debug|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Debug|x64.ActiveCfg = Debug|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Debug|x64.Build.0 = Debug|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Release|x64.ActiveCfg = Release|x64 + {6396B488-4D34-48B2-8639-EEB90707405B}.Release|x64.Build.0 = Release|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Bounds|x64.ActiveCfg = Bounds|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Bounds|x64.Build.0 = Bounds|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Debug|x64.ActiveCfg = Debug|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Debug|x64.Build.0 = Debug|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Release|x64.ActiveCfg = Release|x64 + {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Release|x64.Build.0 = Release|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Bounds|x64.ActiveCfg = Release|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Bounds|x64.Build.0 = Release|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Debug|x64.ActiveCfg = Debug|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Debug|x64.Build.0 = Debug|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Release|x64.ActiveCfg = Release|x64 + {34442A32-31DE-45A8-AD36-0ECFE4095523}.Release|x64.Build.0 = Release|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Bounds|x64.ActiveCfg = Debug|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Bounds|x64.Build.0 = Debug|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Debug|x64.ActiveCfg = Debug|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Debug|x64.Build.0 = Debug|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Release|x64.ActiveCfg = Release|x64 + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Release|x64.Build.0 = Release|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Bounds|x64.ActiveCfg = Debug|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Bounds|x64.Build.0 = Debug|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Debug|x64.ActiveCfg = Debug|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Debug|x64.Build.0 = Debug|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Release|x64.ActiveCfg = Release|x64 + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Release|x64.Build.0 = Release|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Bounds|x64.ActiveCfg = Debug|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Bounds|x64.Build.0 = Debug|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Debug|x64.ActiveCfg = Debug|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Debug|x64.Build.0 = Debug|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Release|x64.ActiveCfg = Release|x64 + {AF250D69-786B-40FA-A125-FD3F448CC283}.Release|x64.Build.0 = Release|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Bounds|x64.ActiveCfg = Debug|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Bounds|x64.Build.0 = Debug|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Debug|x64.ActiveCfg = Debug|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Debug|x64.Build.0 = Debug|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Release|x64.ActiveCfg = Release|x64 + {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Release|x64.Build.0 = Release|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Bounds|x64.ActiveCfg = Debug|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Bounds|x64.Build.0 = Debug|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Debug|x64.ActiveCfg = Debug|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Debug|x64.Build.0 = Debug|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Release|x64.ActiveCfg = Release|x64 + {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Release|x64.Build.0 = Release|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Bounds|x64.ActiveCfg = Debug|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Bounds|x64.Build.0 = Debug|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Debug|x64.ActiveCfg = Debug|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Debug|x64.Build.0 = Debug|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Release|x64.ActiveCfg = Release|x64 + {EEE765C8-6812-4F9F-A100-42AA71921926}.Release|x64.Build.0 = Release|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Bounds|x64.ActiveCfg = Debug|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Bounds|x64.Build.0 = Debug|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Debug|x64.ActiveCfg = Debug|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Debug|x64.Build.0 = Debug|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Release|x64.ActiveCfg = Release|x64 + {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Release|x64.Build.0 = Release|x64 + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Bounds|x64.ActiveCfg = Release|Any CPU + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Bounds|x64.Build.0 = Release|Any CPU + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Debug|x64.Build.0 = Debug|Any CPU + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Release|x64.ActiveCfg = Release|Any CPU + {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Release|x64.Build.0 = Release|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Bounds|x64.ActiveCfg = Release|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Bounds|x64.Build.0 = Release|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Debug|x64.Build.0 = Debug|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Release|x64.ActiveCfg = Release|Any CPU + {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Release|x64.Build.0 = Release|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Bounds|x64.ActiveCfg = Release|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Bounds|x64.Build.0 = Release|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Debug|x64.Build.0 = Debug|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Release|x64.ActiveCfg = Release|Any CPU + {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Release|x64.Build.0 = Release|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Bounds|x64.ActiveCfg = Release|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Bounds|x64.Build.0 = Release|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Debug|x64.Build.0 = Debug|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Release|x64.ActiveCfg = Release|Any CPU + {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Release|x64.Build.0 = Release|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Bounds|x64.ActiveCfg = Release|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Bounds|x64.Build.0 = Release|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Debug|x64.Build.0 = Debug|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Release|x64.ActiveCfg = Release|Any CPU + {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Release|x64.Build.0 = Release|Any CPU + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Bounds|x64.ActiveCfg = Debug|x64 + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Bounds|x64.Build.0 = Debug|x64 + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Debug|x64.ActiveCfg = Debug|x64 + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Debug|x64.Build.0 = Debug|x64 + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Release|x64.ActiveCfg = Release|x64 + {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6D69D131-C928-6A46-F508-A4A608CBE30A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5} = {6D69D131-C928-6A46-F508-A4A608CBE30A} + {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE} = {6D69D131-C928-6A46-F508-A4A608CBE30A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9F385E4A-ED83-4896-ADB8-335A2065B865} + EndGlobalSection +EndGlobal diff --git a/FieldWorks.sln b/FieldWorks.sln index d664464ad2..7a8ebfd8c1 100644 --- a/FieldWorks.sln +++ b/FieldWorks.sln @@ -154,6 +154,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSite", "Src\Common\Root EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSiteTests", "Src\Common\RootSite\RootSiteTests\RootSiteTests.csproj", "{EC934204-1D3A-5575-A500-CB7923C440E2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecks", "Lib\src\ScrChecks\ScrChecks.csproj", "{0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecksTests", "Lib\src\ScrChecks\ScrChecksTests\ScrChecksTests.csproj", "{37555756-6D42-5E46-B455-E58E3D1E8E0C}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtils", "Src\Common\ScriptureUtils\ScriptureUtils.csproj", "{8336DC7C-954B-5076-9315-D7DC5317282B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtilsTests", "Src\Common\ScriptureUtils\ScriptureUtilsTests\ScriptureUtilsTests.csproj", "{04546E35-9A3A-5629-8282-3683A5D848F9}" @@ -721,6 +725,18 @@ Global {EC934204-1D3A-5575-A500-CB7923C440E2}.Debug|x64.Build.0 = Debug|x64 {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.ActiveCfg = Release|x64 {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.Build.0 = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.ActiveCfg = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.Build.0 = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.ActiveCfg = Debug|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.Build.0 = Debug|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.ActiveCfg = Release|x64 + {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.Build.0 = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.ActiveCfg = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.Build.0 = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.ActiveCfg = Debug|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.Build.0 = Debug|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.ActiveCfg = Release|x64 + {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.Build.0 = Release|x64 {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.ActiveCfg = Release|x64 {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.Build.0 = Release|x64 {8336DC7C-954B-5076-9315-D7DC5317282B}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/Lib/src/ScrChecks/BuildInclude.targets b/Lib/src/ScrChecks/BuildInclude.targets new file mode 100644 index 0000000000..5ed09f812f --- /dev/null +++ b/Lib/src/ScrChecks/BuildInclude.targets @@ -0,0 +1,29 @@ + + + + + $(MSBuildThisFileDirectory)..\..\..\DistFiles\Editorial Checks + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lib/src/ScrChecks/CapitalizationCheck.cs b/Lib/src/ScrChecks/CapitalizationCheck.cs new file mode 100644 index 0000000000..08ba391098 --- /dev/null +++ b/Lib/src/ScrChecks/CapitalizationCheck.cs @@ -0,0 +1,595 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ------------------------------------------------------------------------------------ + /// + /// Check for capitalization: styles that should begin with a capital letter and + /// words after sentence-final punctuation. + /// + /// ------------------------------------------------------------------------------------ + public class CapitalizationCheck : IScriptureCheck + { + #region Member variables + /// provides Scripture data to this check + IChecksDataSource m_chkDataSource; + + /// capitalization errors detected in this check + List m_capitalizationErrors; + + /// name of parameter to provide serialized style information for this check. + readonly string kStyleSheetInfoParameter = "StylesInfo"; +// /// name of parameter to provide punctuation that occurs sentence-finally +// readonly string kSentenceFinalPuncParameter = "SentenceFinalPunctuation"; + + private StylePropsInfo m_stylePropsInfo; + /// Dictionary keyed by the style name containing the type of style (character/paragraph) + /// and a value indicating why it should begin with a capital. + private Dictionary m_allCapitalizedStyles = new Dictionary(); + +// /// string containing punctuation that ends sentences. +// string m_SentenceFinalPunc = null; + #endregion + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The data source for the check. + /// ------------------------------------------------------------------------------------ + public CapitalizationCheck(IChecksDataSource _checksDataSource) + { + + m_chkDataSource = _checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_chkDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckName { get { return Localize("Capitalization"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidCapitalization; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup { get { return Localize("Basic"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 600; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description { get { return Localize("Checks for potential inconsistencies in capitalization."); } } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Style name or preceding punctuation."); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks the specified Scripture tokens for capitalization within styles. + /// + /// The tokens from scripture. + /// The record. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + GetReferences(toks); + + foreach (TextTokenSubstring tts in m_capitalizationErrors) + record(new RecordErrorEventArgs(tts, CheckId)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the references where capitalization errors occurred. + /// + /// The Scripture tokens. + /// list of capitalization errors. + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens) + { +// m_SentenceFinalPunc = m_chkDataSource.GetParameterValue(kSentenceFinalPuncParameter); + if (m_stylePropsInfo == null) + { + string styleInfo = m_chkDataSource.GetParameterValue(kStyleSheetInfoParameter); + Debug.Assert(!string.IsNullOrEmpty(styleInfo), "Style information not provided."); + m_stylePropsInfo = StylePropsInfo.Load(styleInfo); + CreateCapitalStyleDictionary(); + Debug.Assert(m_allCapitalizedStyles.Count > 0, "No styles require capitalization."); + } + + CapitalizationProcessor bodyPuncProcessor = new CapitalizationProcessor(m_chkDataSource, m_allCapitalizedStyles); + CapitalizationProcessor notePuncProcessor = new CapitalizationProcessor(m_chkDataSource, m_allCapitalizedStyles); + notePuncProcessor.ProcessParagraphsSeparately = true; + + m_capitalizationErrors = new List(); + VerseTextToken scrTok = new VerseTextToken(); + + ITextToken tok; + foreach (ITextToken token in tokens) + { + if (token.TextType == TextType.Note || token.TextType == TextType.PictureCaption) + tok = token; + else + { + // Make the token one of our special capitalization text tokens. + scrTok.Token = token; + tok = scrTok; + } + + if (tok.TextType == TextType.Note) + notePuncProcessor.ProcessToken(tok, m_capitalizationErrors); + else if (tok.TextType == TextType.Verse || tok.TextType == TextType.Other) + bodyPuncProcessor.ProcessToken(tok, m_capitalizationErrors); + } + + return m_capitalizationErrors; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Creates the dictionary of styles information that will be used in this check. + /// + /// ------------------------------------------------------------------------------------ + private void CreateCapitalStyleDictionary() + { + AddListToDictionary(m_stylePropsInfo.SentenceInitial, StyleCapInfo.CapCheckTypes.SentenceInitial); + AddListToDictionary(m_stylePropsInfo.ProperNouns, StyleCapInfo.CapCheckTypes.ProperNoun); + AddListToDictionary(m_stylePropsInfo.Table, StyleCapInfo.CapCheckTypes.Table); + AddListToDictionary(m_stylePropsInfo.List, StyleCapInfo.CapCheckTypes.List); + AddListToDictionary(m_stylePropsInfo.Special, StyleCapInfo.CapCheckTypes.Special); + AddListToDictionary(m_stylePropsInfo.Heading, StyleCapInfo.CapCheckTypes.Heading); + AddListToDictionary(m_stylePropsInfo.Title, StyleCapInfo.CapCheckTypes.Title); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Adds the list of styles to dictionary. + /// + /// The list of style info (name and type of style). + /// The reason this style should begin with a capital letter. + /// ------------------------------------------------------------------------------------ + private void AddListToDictionary(List list, StyleCapInfo.CapCheckTypes capType) + { + foreach (StyleInfo styleInfo in list) + { + m_allCapitalizedStyles.Add(styleInfo.StyleName.Replace("_", " "), + new StyleCapInfo(styleInfo.StyleType, capType)); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the error message given the style's reason for capitalization. + /// + /// The data source. + /// Reason why a character should have been capitalized. + /// Name of the style or string.Empty if not relevant. + /// error message. + /// ------------------------------------------------------------------------------------ + internal static string GetErrorMessage(IChecksDataSource dataSource, + StyleCapInfo.CapCheckTypes capReasonType, string styleName) + { + switch (capReasonType) + { + case StyleCapInfo.CapCheckTypes.SentenceInitial: + return dataSource.GetLocalizedString("Sentence should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.Heading: + return dataSource.GetLocalizedString("Heading should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.Title: + return dataSource.GetLocalizedString("Title should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.List: + return dataSource.GetLocalizedString("List paragraphs should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.Table: + return dataSource.GetLocalizedString("Table contents should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.ProperNoun: + return dataSource.GetLocalizedString("Proper nouns should begin with a capital letter"); + case StyleCapInfo.CapCheckTypes.Special: + return String.Format(dataSource.GetLocalizedString( + "Text in the {0} style should begin with a capital letter"), styleName); + } + + throw new Exception("Reason for capitalizing the style " + styleName + + " is not handled in GetErrorMessage (" + capReasonType.ToString() + ")"); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Information about a style that is useful for capitalization checking. + /// + /// ------------------------------------------------------------------------------------ + public class StyleCapInfo + { + /// categories of styles that should be capitalized + public enum CapCheckTypes + { + /// styles used sentence initially. + SentenceInitial, + /// styles used for proper nouns. + ProperNoun, + /// styles used in a table. + Table, + /// styles used in a list. + List, + /// styles used for special elements. + Special, + /// styles used in a heading. + Heading, + /// styles used in a title. + Title + } + + /// type of style either paragraph or character + public StyleInfo.StyleTypes m_type; + /// reason why the style is capitalized + public CapCheckTypes m_capCheck; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The type of style (character or paragraph). + /// The reason for the capitalization in this style. + /// ------------------------------------------------------------------------------------ + public StyleCapInfo(StyleInfo.StyleTypes type, CapCheckTypes capCheck) + { + m_type = type; + m_capCheck = capCheck; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Check capitalization for styles and sentences. + /// + /// ------------------------------------------------------------------------------------ + public class CapitalizationProcessor + { + #region Data members + /// provides Scripture data to this check. + private IChecksDataSource m_checksDataSource; + /// provides the category of characters, e.g. word-forming, etc. + CharacterCategorizer m_categorizer; + /// abbreviations relevant to this check. + string[] m_abbreviations; + /// valid punctuation at the end of a sentence. + List m_validSentenceFinalPuncts = new List(); + private bool m_fAtSentenceStart = true; + + /// the current paragraph style. + private string m_paragraphStyle = ""; + /// the current character style. + private string m_characterStyle = ""; + + private bool m_foundParagraphText = true; + private bool m_foundCharacterText = true; + + private bool m_processParagraphsSeparately = false; + + /// Dictionary keyed by the style name containing the type of style (character/paragraph) + /// and a value indicating why it should begin with a capital. + private Dictionary m_allCapitalizedStyles = null; + #endregion + + #region Constructor + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The source of data for Scripture checking. + /// Dictionary keyed by the style name containing the + /// type of style (character/paragraph) and a value indicating why it should begin with + /// a capital. + /// ------------------------------------------------------------------------------------ + public CapitalizationProcessor(IChecksDataSource checksDataSource, + Dictionary allCapitalizedStyles) + { + m_checksDataSource = checksDataSource; + m_categorizer = checksDataSource.CharacterCategorizer; + m_abbreviations = checksDataSource.GetParameterValue("Abbreviations").Split(); + m_allCapitalizedStyles = allCapitalizedStyles; + + string sentenceFinalPunc = checksDataSource.GetParameterValue("SentenceFinalPunctuation"); + if (!string.IsNullOrEmpty(sentenceFinalPunc)) + { + foreach (char ch in sentenceFinalPunc) + m_validSentenceFinalPuncts.Add(ch); + } + else + { + // No punctuation is set up for this writing system that contains sentence-final punctuation. + // Define sentence-final punctuation with these characters as a fallback: '.', '?', and '!' + m_validSentenceFinalPuncts.Add('.'); + m_validSentenceFinalPuncts.Add('?'); + m_validSentenceFinalPuncts.Add('!'); + } + } + + #endregion + + #region Public methods + public bool ProcessParagraphsSeparately + { + get { return m_processParagraphsSeparately; } + set { m_processParagraphsSeparately = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Processes the Scripture token. + /// + /// The token. + /// The result. + /// ------------------------------------------------------------------------------------ + public void ProcessToken(ITextToken tok, List result) + { + string tokenText = RemoveAbbreviations(tok); + + RecordParagraphStyle(tok); + RecordCharacterStyle(tok); + + // must be at least one character in token to check the case of + if (tok.Text == String.Empty) + return; + + for (int iChar = 0; iChar < tokenText.Length; iChar++) + { + char ch = tokenText[iChar]; + + if (IsSentenceFinalPunctuation(ch)) + { + m_fAtSentenceStart = iChar + 1 == tokenText.Length || + (iChar + 1 < tokenText.Length && !char.IsDigit(tokenText[iChar + 1])); + continue; + } + + if (!m_categorizer.IsWordFormingCharacter(ch)) + continue; + + if (m_categorizer.IsLower(ch)) + { + TextTokenSubstring tts = GetSubstring(tok, iChar); + + if (!CheckForParaCapitalizationError(tok, tts, result) && + !CheckForCharStyleCapilizationError(tok, tts, result) && + m_fAtSentenceStart) + { + tts.Message = CapitalizationCheck.GetErrorMessage(m_checksDataSource, + StyleCapInfo.CapCheckTypes.SentenceInitial, string.Empty); + result.Add(tts); + } + } + m_fAtSentenceStart = false; + m_foundCharacterText = true; + m_foundParagraphText = true; + } + } + #endregion + + #region Private Methods + /// ------------------------------------------------------------------------------------ + /// + /// Gets the substring for the character starting at position iChar. + /// + /// The token + /// The index of the character. + /// ------------------------------------------------------------------------------------ + private TextTokenSubstring GetSubstring(ITextToken tok, int iChar) + { + int iCharLength = GetLengthOfChar(tok, iChar); + TextTokenSubstring tts = new TextTokenSubstring((tok is VerseTextToken ? + ((VerseTextToken)tok).Token : tok), iChar, iCharLength); + return tts; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Records the paragraph style. + /// + /// The Scripture token. + /// ------------------------------------------------------------------------------------ + private void RecordParagraphStyle(ITextToken tok) + { + if (tok.IsParagraphStart) + { + m_paragraphStyle = tok.ParaStyleName; + m_foundParagraphText = false; + if (m_processParagraphsSeparately) + m_fAtSentenceStart = false; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Records the character style. + /// + /// The Scripture token. + /// ------------------------------------------------------------------------------------ + private void RecordCharacterStyle(ITextToken tok) + { + if (tok.CharStyleName != m_characterStyle) + { + m_characterStyle = tok.CharStyleName; + m_foundCharacterText = false; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Removes the abbreviations from a Scripture token. + /// + /// The Scripture token. + /// Scripture token with any abbreviations replaced with spaces. + /// ------------------------------------------------------------------------------------ + private string RemoveAbbreviations(ITextToken tok) + { + string tokenText = tok.Text; + foreach (string abbreviation in m_abbreviations) + { + if (abbreviation == "") + continue; + + string spaces = new string(' ', abbreviation.Length); + tokenText = tokenText.Replace(abbreviation, spaces); + } + + Debug.Assert(tok.Text.Length == tokenText.Length, + "Length of text should not change", + "Abbreviations are replaced by spaces, but the overall text length should stay the same."); + return tokenText; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Creates a checking error if paragraph style requires an initial uppercase letter, + /// but the tssFirstLetter is lowercase. + /// + /// The Scripture token. + /// The token substring of the first word-forming character + /// in the given token. + /// The error results. + /// true if an error was added to the list of results; otherwise + /// false + /// ------------------------------------------------------------------------------------ + private bool CheckForParaCapitalizationError(ITextToken tok, + TextTokenSubstring ttsFirstLetter, List result) + { + if (m_foundParagraphText) + return false; + + m_foundParagraphText = true; + + // The first character of the paragraph is lowercase. + // Look it up in the capitalized styles dictionary to determine if it should be uppercase. + StyleCapInfo styleCapInfo; + if (m_allCapitalizedStyles.TryGetValue(m_paragraphStyle, out styleCapInfo)) + { + ttsFirstLetter.InventoryText = m_paragraphStyle; + + ttsFirstLetter.Message = CapitalizationCheck.GetErrorMessage(m_checksDataSource, + styleCapInfo.m_capCheck, m_paragraphStyle); + result.Add(ttsFirstLetter); + return true; + } + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Creates a checking error if character style requires an initial uppercase letter, + /// but the tssFirstLetter is lowercase. + /// + /// The Scripture token. + /// The token substring of the first word-forming character + /// in the given token. + /// The result. + /// true if an error was added to the list of results; otherwise + /// false + /// ------------------------------------------------------------------------------------ + private bool CheckForCharStyleCapilizationError(ITextToken tok, + TextTokenSubstring ttsFirstLetter, List result) + { + if (m_foundCharacterText) + return false; + + m_foundCharacterText = true; + + // The first word-forming character of the character style is lowercase. + // Look it up in the capitalized styles dictionary to determine if it should be uppercase. + StyleCapInfo styleCapInfo; + if (m_allCapitalizedStyles.TryGetValue(m_characterStyle, out styleCapInfo) && + styleCapInfo.m_type == StyleInfo.StyleTypes.character) + { + ttsFirstLetter.InventoryText = m_characterStyle; + ttsFirstLetter.Message = CapitalizationCheck.GetErrorMessage(m_checksDataSource, + styleCapInfo.m_capCheck, m_characterStyle); + result.Add(ttsFirstLetter); + return true; + } + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether the specified character is sentence final punctuation. + /// + /// The specified character. + /// + /// true if the specified character is sentence final punctuation; otherwise, false. + /// + /// ------------------------------------------------------------------------------------ + private bool IsSentenceFinalPunctuation(char ch) + { + return m_validSentenceFinalPuncts.IndexOf(ch) >= 0; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the length of the character including any associated diacritics that follow + /// the base character. + /// + /// The text token. + /// The index of the base character in the text token. + /// length of the character, including all following diacritics + /// ------------------------------------------------------------------------------------ + private int GetLengthOfChar(ITextToken tok, int iBaseCharacter) + { + int charLength = 1; + int iChar = iBaseCharacter + 1; + while(iChar < tok.Text.Length && m_categorizer.IsDiacritic(tok.Text[iChar++])) + charLength++; + + return charLength; + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ChapterVerseCheck.cs b/Lib/src/ScrChecks/ChapterVerseCheck.cs new file mode 100644 index 0000000000..c7a8084fa1 --- /dev/null +++ b/Lib/src/ScrChecks/ChapterVerseCheck.cs @@ -0,0 +1,995 @@ +// Copyright (c) 2008-2017 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Core.Scripture; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Checks for missing, repeated, extraneous, and out-of-order chapters and verses + /// + /// ---------------------------------------------------------------------------------------- + public class ChapterVerseCheck : IScriptureCheck + { + private enum ParseVerseResult + { + Valid, + ValidWithSpaceInVerse, + ValidWithSpaceInVerseBridge, + Invalid, + InvalidFormat, + } + + private enum VersePart + { + NA, + PartA, + PartB + } + + private IChecksDataSource m_checksDataSource; + + private readonly string ksVerseSchemeParam = "Versification Scheme"; + private readonly string ksBookIdParam = "Book ID"; + private readonly string ksChapterParam = "Chapter Number"; + private readonly string ksVerseBridgeParam = "Verse Bridge"; + private readonly string ksScriptDigitZeroParam = "Script Digit Zero"; + private readonly string ksSubVerseLetterAParam = "Sub-verse Letter A"; + private readonly string ksSubVerseLetterBParam = "Sub-verse Letter B"; + + private string m_versificationScheme; + private VersificationTable m_versification; + + private string m_subVerseA = "a"; + private string m_subVerseB = "b"; + + private Regex m_verseNumberFormat; + private Regex m_chapterNumberFormat; + + private List m_chapTokens = new List(); + private ITextToken m_fallbackToken; + + private string m_sBookId; + private int m_nChapterToCheck; // 0 = all chapters in book + private RecordErrorHandler m_recordError; + +// /// Verses encountered in current chapter +// private List m_versesFound; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The checks data source. + /// ------------------------------------------------------------------------------------ + public ChapterVerseCheck(IChecksDataSource checksDataSource) : this(checksDataSource, null) + { + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. This + /// overload of the constructor is only used for testing. + /// + /// The checks data source. + /// The error recording handler. + /// ------------------------------------------------------------------------------------ + public ChapterVerseCheck(IChecksDataSource checksDataSource, + RecordErrorHandler recErrHandler) + { + m_checksDataSource = checksDataSource; + m_recordError = recErrHandler; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_checksDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckName + { + get {return Localize("Chapter and Verse Numbers"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidChapterVerse; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup + { + get { return Localize("Basic"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 100; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description + { + get { return Localize("Checks for potential inconsistencies in chapter and verse numbers."); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Sets the record error handler. Use this only for tests. + /// + /// ------------------------------------------------------------------------------------ + public RecordErrorHandler RecordError + { + set { m_recordError = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks the given tokens for chapter/verse errors and calls the given RecordError + /// handler for each one. + /// + /// The tokens to check. + /// Method to call to record errors. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + GetParameters(); + + m_recordError = record; +// m_versesFound = new List(); + m_chapTokens.Clear(); + + ChapterToken currChapterToken = null; + VerseToken currVerseToken = null; + + foreach (ITextToken token in toks) + { + // This token is only necessary when a chapter one is missing + // and we need a token to use for reporting that it's missing. + if (m_fallbackToken == null) + m_fallbackToken = token; + + if (token.TextType == TextType.ChapterNumber) + { + currChapterToken = new ChapterToken(token, m_chapterNumberFormat); + currVerseToken = null; + m_chapTokens.Add(currChapterToken); + } + else if (token.TextType == TextType.VerseNumber) + { + if (currChapterToken == null) + { + //assume chapter one + currChapterToken = new ChapterToken(token, 1); + m_chapTokens.Add(currChapterToken); + } + + currVerseToken = new VerseToken(token); + currChapterToken.VerseTokens.Add(currVerseToken); + } + else if (token.TextType == TextType.Verse) + { + if (currChapterToken == null) + { + // no chapter token and no verse number token + // oh no! use verse text token as default, but system + // should error on missing verse first. + if (currVerseToken == null) + { + //assume chapter one + currChapterToken = new ChapterToken( token, 1); + m_chapTokens.Add(currChapterToken); + + //assume verse one + currVerseToken = new VerseToken(token, 1); + currChapterToken.VerseTokens.Add(currVerseToken); + } + // no chapter token, but we have verse number token + // then use the verse number token + else + { + // this case should not happen because chapter tokens + // are automatically created if a verse number token is + // encountered first + Debug.Assert(false, "verse number token found without chapter number token"); + } + } + else + { + // we have a chapter token, but no verse number token + // use the chapter token as the default token. + if (currVerseToken == null) + { + //assume verse one + currVerseToken = new VerseToken(token, 1); + currChapterToken.VerseTokens.Add(currVerseToken); + } + // we have a chapter token, and a verse number token + // we are happy + else + { + // do nothing + } + } + currVerseToken.IncrementVerseTextCount(token); + } + } + + CheckChapterNumbers(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the parameters needed for this check. + /// + /// ------------------------------------------------------------------------------------ + private void GetParameters() + { + m_versificationScheme = m_checksDataSource.GetParameterValue(ksVerseSchemeParam); + ScrVers scrVers; + try + { + scrVers = (ScrVers)Enum.Parse(typeof(ScrVers), + m_versificationScheme); + } + catch + { + // Default to English + scrVers = ScrVers.English; + } + + m_versification = VersificationTable.Get(scrVers); + + m_sBookId = m_checksDataSource.GetParameterValue(ksBookIdParam); + if (!int.TryParse(m_checksDataSource.GetParameterValue(ksChapterParam), out m_nChapterToCheck)) + m_nChapterToCheck = 0; + + string temp = m_checksDataSource.GetParameterValue(ksVerseBridgeParam); + string verseBridge = (string.IsNullOrEmpty(temp)) ? "-" : temp; + + temp = m_checksDataSource.GetParameterValue(ksScriptDigitZeroParam); + char scriptDigitZero = (string.IsNullOrEmpty(temp)) ? '0' : temp[0]; + string numberRange = string.Format("[{1}-{2}][{0}-{2}]*", scriptDigitZero, + (char)(scriptDigitZero + 1), (char)(scriptDigitZero + 9)); + + temp = m_checksDataSource.GetParameterValue(ksSubVerseLetterAParam); + if (!string.IsNullOrEmpty(temp)) + m_subVerseA = temp; + + temp = m_checksDataSource.GetParameterValue(ksSubVerseLetterBParam); + if (!string.IsNullOrEmpty(temp)) + m_subVerseB = temp; + string subverseRange = string.Format("[{0}{1}]?", m_subVerseA, m_subVerseB); + + // Original Regex for Roman script: "^[1-9][0-9]{0,2}[ab]?(-[1-9][0-9]{0,2}[ab]?)?$" + m_verseNumberFormat = new Regex(String.Format("^{0}{1}({2}{0}{1})?$", + numberRange, subverseRange, verseBridge)); + m_chapterNumberFormat = new Regex("^" + numberRange + "$"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks for missing chapters. + /// + /// ------------------------------------------------------------------------------------ + private void CheckChapterNumbers() + { + int bookId = BCVRef.BookToNumber(m_sBookId); + int lastChapInBook = m_versification.LastChapter(bookId); + int nextExpectedChapter = 1; + int prevChapNumber = 0; + bool[] chaptersFound = new bool[lastChapInBook + 1]; + + foreach (ChapterToken chapToken in m_chapTokens) + { + if (m_nChapterToCheck != 0 && chapToken.ChapterNumber != m_nChapterToCheck) + continue; + + string msg = null; + int errorArg = chapToken.ChapterNumber; + ITextToken token = chapToken.Token; + + if (!chapToken.Valid) + { + // Chapter number is invalid + AddError(token, 0, token.Text.Length, Localize("Invalid chapter number"), errorArg); + } + + if (chapToken.ChapterNumber >= 1) + { + if (chapToken.ChapterNumber > lastChapInBook) + { + // Chapter number is out of range + msg = Localize("Chapter number out of range"); + } + else if (chapToken.ChapterNumber == prevChapNumber) + { + // Chapter number is repeated + msg = Localize("Duplicate chapter number"); + } + else if (chapToken.ChapterNumber < nextExpectedChapter) + { + // Chapter number is out of order + msg = Localize("Chapter out of order; expected chapter {0}"); + errorArg = nextExpectedChapter; + } + + if (msg != null) + AddError(token, 0, token.Text.Length, msg, errorArg); + else + { + chaptersFound[chapToken.ChapterNumber] = true; + CheckVerseNumbers(chapToken, bookId); + } + } + + prevChapNumber = chapToken.ChapterNumber; + nextExpectedChapter = + Math.Max(chapToken.ChapterNumber + 1, nextExpectedChapter); + } + + CheckForMissingChapters(chaptersFound); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks for missing chapters in the current book. + /// + /// ------------------------------------------------------------------------------------ + private void CheckForMissingChapters(bool[] chaptersFound) + { + for (int chap = 1; chap < chaptersFound.Length; chap++) + { + if (chaptersFound[chap] || (m_nChapterToCheck != 0 && chap != m_nChapterToCheck)) + continue; + + // Find the first chapter token that immediately precedes where the + // missing chapter would have a token if it weren't missing. + ChapterToken precedingChapter = null; + foreach (ChapterToken chapToken in m_chapTokens) + { + if (chapToken.ChapterNumber > chap) + break; + precedingChapter = chapToken; + } + + // TODO: Deal with what token to use if a book has no chapters at all. + // This should always succeed + int offset = 0; + ITextToken token = null; + if (precedingChapter != null) + { + token = precedingChapter.Token; + offset = precedingChapter.Implicit ? 0 : token.Text.Length; + } + else if (m_chapTokens.Count > 0) + { + token = m_chapTokens[0].Token; + } + + if (token != null) + { + BCVRef scrRefStart = new BCVRef(BCVRef.BookToNumber(token.ScrRefString), chap, 0); + token.MissingStartRef = scrRefStart; + token.MissingEndRef = null; + AddError(token, offset, 0, Localize("Missing chapter number {0}"), chap); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Check verse numbers. + /// + /// ------------------------------------------------------------------------------------ + private void CheckVerseNumbers(ChapterToken chapToken, int bookId) + { + int lastVrsInChap = m_versification.LastVerse(bookId, chapToken.ChapterNumber); + int nextExpectedVerse = 1; + bool expectingPartB = false; + int prevVerseStart = 0; + int prevVerseEnd = 0; + ITextToken[] versesFound = new ITextToken[lastVrsInChap + 1]; + versesFound[0] = chapToken.Token; + + foreach (VerseToken verseToken in chapToken.VerseTokens) + { + ITextToken token = verseToken.VerseNumber; + ITextToken reportedToken = token; + string msg = null; + int offset = 0; + int length = token.Text.Length; + object[] errorArgs = null; + bool countFoundVerses = false; + int curVerseStart; + int curVerseEnd; + VersePart vrsPart; + + if (verseToken.ImplicitVerseNumber == 1) + { + versesFound[1] = token; + continue; + } + + ParseVerseResult parseResult = ParseVerseNumber(token.Text, + out curVerseStart, out curVerseEnd, out vrsPart); + + if (parseResult == ParseVerseResult.ValidWithSpaceInVerse) + { + // Log error telling user there are spaces before or after the verse + // number. This means the space(s) have the verse number style. This isn't + // considered an invalid verse number, but we do need to tell the user. + AddError(token, 0, token.Text.Length, + Localize("Space found in verse number"), token.Text); + } + else if (parseResult == ParseVerseResult.ValidWithSpaceInVerseBridge) + { + // Log error telling user there are spaces in a verse bridge. This + // means the space(s) have the verse number style. This isn't considered + // an invalid verse number, but we do need to tell the user. + AddError(token, 0, token.Text.Length, + Localize("Space found in verse bridge"), token.Text); + } + + if (parseResult == ParseVerseResult.Invalid) + { + msg = Localize("Invalid verse number"); + } + else if ((parseResult != ParseVerseResult.InvalidFormat) && VersesAlreadyFound(curVerseStart, curVerseEnd, versesFound) && + !(expectingPartB && vrsPart == VersePart.PartB)) + { + if (AnyOverlappingVerses(curVerseStart, curVerseEnd, + prevVerseStart, prevVerseEnd, out errorArgs)) + { + // Duplicate verse(s) found. + msg = (errorArgs.Length == 1 ? + Localize("Duplicate verse number") : + Localize("Duplicate verse numbers")); + } + else + { + // Verse number(s) are unexpected + msg = (curVerseStart == curVerseEnd ? + Localize("Unexpected verse number") : + Localize("Unexpected verse numbers")); + } + } + else if (AnyOverlappingVerses(curVerseStart, curVerseEnd, + lastVrsInChap + 1, int.MaxValue, out errorArgs)) + { + countFoundVerses = true; + // Start and/or end verse is out of range + msg = (errorArgs.Length == 1 ? + Localize("Verse number out of range") : + Localize("Verse numbers out of range")); + } + else if (curVerseStart < nextExpectedVerse) + { + // Verse number(s) are out of order + countFoundVerses = true; + if (nextExpectedVerse <= lastVrsInChap) + { + errorArgs = new object[] { nextExpectedVerse }; + msg = (curVerseStart == curVerseEnd ? + Localize("Verse number out of order; expected verse {0}") : + Localize("Verse numbers out of order; expected verse {0}")); + } + else + { + msg = (curVerseStart == curVerseEnd ? + Localize("Verse number out of order") : + Localize("Verse numbers out of order")); + } + } + else if (((vrsPart == VersePart.PartB) != expectingPartB) && + (curVerseStart == curVerseEnd)) + { + // Missing part A or B + // TODO: cover cases like "4a 5-7" and "4 5b-7". This would require + // ParseVerseNumber() to detect verse parts at the beginning of bridges. + reportedToken = (vrsPart == VersePart.PartB ? token : versesFound[prevVerseEnd]); + msg = Localize("Missing verse number {0}"); + offset = (vrsPart == VersePart.PartB ? 0 : reportedToken.Text.Length); + length = 0; + int reportedVrsNum = (vrsPart == VersePart.PartB ? curVerseStart : prevVerseEnd); + string fmt = (vrsPart == VersePart.PartB ? "{0}a" : "{0}b"); + errorArgs = new object[] { string.Format(fmt, reportedVrsNum) }; + countFoundVerses = true; + } + else if ((vrsPart == VersePart.PartB && curVerseStart > prevVerseEnd) && + (curVerseStart == curVerseEnd)) + { + // Missing both a part B and A + reportedToken = versesFound[prevVerseEnd]; + + AddError(reportedToken, reportedToken.Text.Length, 0, + Localize("Missing verse number {0}"), + new object[] { string.Format("{0}b", prevVerseEnd) }); + + AddError(token, 0, 0, Localize("Missing verse number {0}"), + new object[] { string.Format("{0}a", curVerseStart) }); + } + + if (msg != null) + { + // Report the error found. + if (errorArgs == null) + AddError(reportedToken, offset, length, msg); + else + AddError(reportedToken, offset, length, msg, errorArgs); + } + + if (msg == null || countFoundVerses) + { + // No error was found for the current verse range so set all the verses + // in our found verse list corresponding to those in the range. + for (int i = curVerseStart; i <= Math.Min(curVerseEnd, lastVrsInChap); i++) + versesFound[i] = token; + } + + if (parseResult == ParseVerseResult.InvalidFormat) + AddError(token, 0, token.Text.Length, Localize("Invalid verse number"), token.Text); + + // only worry about this if the chapter and/or verse tokens are in order + if (verseToken.VerseTextCount < 1) + { + AddError(verseToken.VerseNumber, 0, verseToken.VerseNumber.Text.Length, + Localize("Missing verse text in verse {0}"), verseToken.VerseNumber.Text); + } + + // Determine next expected verse. + // Don't expect a partB if there was an error with partA + expectingPartB = (vrsPart == VersePart.PartA && msg == null); + if (!expectingPartB && curVerseEnd <= lastVrsInChap) + nextExpectedVerse = curVerseEnd + 1; + + prevVerseStart = curVerseStart; + prevVerseEnd = curVerseEnd; + } + + CheckForMissingVerses(versesFound, bookId, chapToken.ChapterNumber); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks the list of found verses to see if any verses in the specified range have + /// already been found. + /// + /// ------------------------------------------------------------------------------------ + private bool VersesAlreadyFound(int curVerseStart, int curVerseEnd, + ITextToken[] versesFound) + { + for (int verse = curVerseStart; verse <= curVerseEnd; verse++) + { + if (verse < versesFound.Length && verse > 0 && versesFound[verse] != null) + return true; + } + + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks for missing verses in the current chapter. + /// + /// ------------------------------------------------------------------------------------ + private void CheckForMissingVerses(ITextToken[] versesFound, int bookId, int chapNumber) + { + ITextToken prevToken = versesFound[0]; + + for (int verse = 1; verse < versesFound.Length; verse++) + { + if (versesFound[verse] != null) + { + prevToken = versesFound[verse]; + continue; + } + + // At this point, we know we've found a missing verse. Now we need + // to determine whether or not this is the first verse in a range + // of missing verses or just a single missing verse. + int startVerse = verse; + int endVerse = verse; + while (endVerse < versesFound.Length - 1 && versesFound[endVerse + 1] == null) + endVerse++; + + prevToken.MissingStartRef = new BCVRef(bookId, chapNumber, startVerse); + + // If previous token is a verse token and it's verse 1 that's missing, + // then we know we're dealing with the case of a missing chapter token + // and a missing verse 1 token in that chapter. In that case, we want + // the offset to fall just before the verse of the token (which is the + // first verse token we found in the chapter and which we're assuming + // is associated with a verse that would come after verse 1). + int offset = (prevToken.TextType == TextType.VerseNumber && verse == 1 ? + 0 : prevToken.Text.Length); + + if (startVerse == endVerse) + AddError(prevToken, offset, 0, Localize("Missing verse number {0}"), startVerse); + else + { + prevToken.MissingEndRef = new BCVRef(bookId, chapNumber, endVerse); + AddError(prevToken, offset, 0, Localize("Missing verse numbers {0}-{1}"), + startVerse, endVerse); + } + + verse = endVerse; + } + } + + #region Helper methods + /// ------------------------------------------------------------------------------------ + /// + /// Parses a verse number. + /// + /// The text of the token containing the verse number. + /// The cur verse start. + /// The cur verse end. + /// The VRS part. + /// + /// ------------------------------------------------------------------------------------ + private ParseVerseResult ParseVerseNumber(string runChars, out int curVerseStart, + out int curVerseEnd, out VersePart vrsPart) + { + string literalVerse; + string remainingText; + BCVRef firstRefer = new BCVRef(); + BCVRef lastRefer = new BCVRef(); + string trimmedRun = runChars.TrimStart(null); + bool hasPrecedingWhiteSpace = (runChars.Length != trimmedRun.Length); + vrsPart = VersePart.NA; + + //Check for a correct format + if (!m_verseNumberFormat.IsMatch(runChars.Replace(" ", string.Empty))) + { + // Even though the verse number is invalid, we'll still attempt to interpret it + // as a verse number (or bridge) since that might avoid spurious "missing verse" + // errors. + if (BCVRef.VerseToScrRef(trimmedRun, out literalVerse, out remainingText, + ref firstRefer, ref lastRefer) && firstRefer.Verse > 0) + { + curVerseStart = firstRefer.Verse; + curVerseEnd = lastRefer.Verse; + return ParseVerseResult.InvalidFormat; + } + else + { + curVerseStart = 0; + curVerseEnd = 0; + return ParseVerseResult.Invalid; + } + } + + // Useful method VerseToScrRef existing in BCVRef returns the parts of a verse + // bridge and any non-numerical remaining text in the run. Allows accounting for + // possible verse bridges. Allows accounting for verse parts, 10a 10b, account + // for valid case: 7-8a 8b, if encounter "a", expectedVerse repeats in order to + // expect 8b . + if (!BCVRef.VerseToScrRef(trimmedRun, out literalVerse, out remainingText, + ref firstRefer, ref lastRefer)) + { + curVerseStart = 0; + curVerseEnd = 0; + return ParseVerseResult.Invalid; + } + + curVerseStart = firstRefer.Verse; + curVerseEnd = lastRefer.Verse; + string remainingVerse = remainingText.Trim(); + bool hasWhiteSpace = (hasPrecedingWhiteSpace || + (remainingVerse.Length != remainingText.Length)); + + // note: if verse bridge, assumes, 'a' is on verse end + // checks for a part "a" in verse + if (remainingVerse == m_subVerseA) + vrsPart = VersePart.PartA; + else if (remainingVerse == m_subVerseB) + vrsPart = VersePart.PartB; + + // If there was a non-numerical part or verse number > 999 that caused an error + // making verseStart or verseEnd returned as 0 it will parse the trimmed + // remainingVerse string to an integer and assign it as the verse number + if (remainingVerse.Length != 0) + { + if (curVerseStart == 0 && !int.TryParse(remainingVerse, out curVerseStart)) + return ParseVerseResult.Invalid; + + if (curVerseEnd == 0 && !int.TryParse(remainingVerse, out curVerseEnd)) + return ParseVerseResult.Invalid; + + // adds error if verse part is not 'a' or 'b', for example "10c" would + // be invalid verse number if " 13", still invalid format + if (remainingVerse != m_subVerseA && remainingVerse != m_subVerseB) + return ParseVerseResult.Invalid; + } + + if (!hasWhiteSpace) + return ParseVerseResult.Valid; + + return (curVerseStart == curVerseEnd ? ParseVerseResult.ValidWithSpaceInVerse : + ParseVerseResult.ValidWithSpaceInVerseBridge); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether or not the two specified verse ranges contain any overlapping + /// verses and returns the range of overlapping verses and a flag indicating whether + /// there are overlapping verses. + /// + /// ------------------------------------------------------------------------------------ + public bool AnyOverlappingVerses(int start1, int end1, int start2, int end2, + out object[] commonVerses) + { + commonVerses = null; + List common = new List(); + for (int i = start1; i <= end1; i++) + { + if (i >= start2 && i <= end2) + common.Add(i); + } + + if (common.Count > 0) + { + // When there are two or less overlapping verse(s) the returned array contains + // all overlapping verses. Otherwise, it contains only the first and last + // overlapping verses, since we only want to know the range. + commonVerses = (common.Count <= 2 ? common.ToArray() : + new object[] { common[0], common[common.Count - 1] }); + } + + return (commonVerses != null); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Records an error. + /// + /// The current token being processed. + /// Offset in the token where the offending text begins. + /// The length of the offending text. + /// The message. + /// The arguments to format the message. + /// ------------------------------------------------------------------------------------ + private void AddError(ITextToken token, int offset, int length, string message, + params object[] args) + { + string formattedMsg = (args != null) ? string.Format(message, args) : + String.Format(message); + + TextTokenSubstring tts = new TextTokenSubstring(token, offset, length, formattedMsg); + m_recordError(new RecordErrorEventArgs(tts, CheckId)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Adds an error for a missing chapter number. + /// + /// ------------------------------------------------------------------------------------ + private void AddMissingChapterError(ITextToken token, int missingChapter, int offset) + { + BCVRef scrRef = new BCVRef(token.ScrRefString); + scrRef.Chapter = missingChapter; + scrRef.Verse = 0; + token.MissingStartRef = scrRef; + AddError(token, offset, 0, Localize("Missing chapter number {0}"), missingChapter); + } + + #endregion + } + + #region ChapterToken class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + internal class ChapterToken + { + internal ITextToken Token; + internal bool Implicit; + internal bool Valid = true; + private int m_chapNumber; + private List m_verseTokens = new List(); + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal ChapterToken(ITextToken token, Regex chapterNumberFormat) + { + Token = token; + m_chapNumber = 0; + if (!chapterNumberFormat.IsMatch(Token.Text)) + Valid = false; + foreach (char ch in token.Text) + { + if (Char.IsDigit(ch)) + { + m_chapNumber *= 10; + m_chapNumber += (int) Char.GetNumericValue(ch); + } + else + { + Valid = false; + m_chapNumber = -1; + break; + } + } + Implicit = false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal ChapterToken(ITextToken token, int chapNumber) + { + Token = token; + m_chapNumber = chapNumber; + Implicit = true; + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal int ChapterNumber + { + get { return m_chapNumber; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal List VerseTokens + { + get { return m_verseTokens; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the last verse number token associated with this chapter. If there isn't one, then + /// this chapter's token is returned. + /// + /// ------------------------------------------------------------------------------------ + internal ITextToken LastVerseToken + { + get + { + return (m_verseTokens.Count == 0 ? + Token : m_verseTokens[m_verseTokens.Count - 1].VerseNumber); + } + } + } + + #endregion + + #region VerseToken class + /// ---------------------------------------------------------------------------------------- + /// + /// Class to represent a verse number and a set of one or more verses + /// + /// ---------------------------------------------------------------------------------------- + internal class VerseToken + { + // the one and only verse number + private ITextToken m_verseNumberToken=null; + // one or more verse text tokens (most probably a paragraph) + private int m_nbrTextTokens = 0; + private int m_implicitVerseNumber = -1; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal VerseToken( ITextToken verseNumber) + { + m_verseNumberToken = verseNumber; + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + internal VerseToken(ITextToken implicitVerseNumber, int verseNumber) + { + m_verseNumberToken = implicitVerseNumber; + m_implicitVerseNumber = verseNumber; + } + + /// ------------------------------------------------------------------------------------ + /// + /// A getter for the verse text body. + /// + /// ------------------------------------------------------------------------------------ + internal ITextToken VerseNumber + { + get { return m_verseNumberToken; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Increment verse text count + /// + /// ------------------------------------------------------------------------------------ + internal void IncrementVerseTextCount(ITextToken token) + { + // only count tokens that aren't all whitespace. + if (token.Text.Trim().Length > 0) + m_nbrTextTokens++; + } + + /// ------------------------------------------------------------------------------------ + /// + /// return verse text count + /// + /// ------------------------------------------------------------------------------------ + internal int VerseTextCount + { + get { return m_nbrTextTokens; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// return flag indicating if this is an implied verse number + /// + /// ------------------------------------------------------------------------------------ + internal bool Implicit + { + get { return m_implicitVerseNumber != -1; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// return the implied verse number + /// + /// ------------------------------------------------------------------------------------ + internal int ImplicitVerseNumber + { + get { return m_implicitVerseNumber; } + } + } + + #endregion + +} diff --git a/Lib/src/ScrChecks/CharactersCheck.cs b/Lib/src/ScrChecks/CharactersCheck.cs new file mode 100644 index 0000000000..70ab6d91be --- /dev/null +++ b/Lib/src/ScrChecks/CharactersCheck.cs @@ -0,0 +1,409 @@ +// Copyright (c) 2015-2017 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Checks all characters to see if they are valid + /// + /// ---------------------------------------------------------------------------------------- + public class CharactersCheck : IScrCheckInventory + { + #region Constants + private const string kValidItemsParameter = "ValidCharacters"; + private const string kInvalidItemsParameter = "InvalidCharacters"; + private const string kAlwaysValidItemsParameter = "AlwaysValidCharacters"; + #endregion + + #region Data Members + IChecksDataSource m_checksDataSource; + CharacterCategorizer m_categorizer; + List m_characterSequences; + private string m_alwaysValidCharacters; + string m_validItems; + string m_invalidItems; + Dictionary m_validItemsDictionary; + #endregion + + #region Constructors + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + public CharactersCheck(IChecksDataSource _checksDataSource) + { + m_checksDataSource = _checksDataSource; + } + #endregion + + #region Properties + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems + { + get + { + List itemsList = new List(m_validItemsDictionary.Keys); + return string.Join(" ", itemsList.ToArray()); + } + set + { + m_validItemsDictionary = StringToDictionary(value); + m_validItems = value.Trim(); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return m_invalidItems; } + set + { + // REVIEW: Why doesn't this add items to the dictionary as well. + m_invalidItems = value.Trim(); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckName + { + get { return m_checksDataSource.GetLocalizedString("Characters"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidCharacters; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup + { + get { return m_checksDataSource.GetLocalizedString("Basic"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 200; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description + { + get { return m_checksDataSource.GetLocalizedString("Checks for potentially invalid characters."); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return m_checksDataSource.GetLocalizedString("Character"); } + } + #endregion + + /// ------------------------------------------------------------------------------------ + /// + /// Converts a string to a dictionary + /// + /// Space-delimited list of valid characters + /// + /// Dictionary containing the items in the list passed + /// + /// ------------------------------------------------------------------------------------ + private Dictionary StringToDictionary(string value) + { + Dictionary dict = new Dictionary(); + + if (value.Length > 0) + { + // 02 JUN 2008, Phil Hopper: Check if space is a valid character. + + if (value[0] == ' ') + dict[value.Substring(0, 1)] = true; + + value = value.Trim(); + + if (value != string.Empty) + { + foreach (string item in value.Split()) + dict[item] = true; + } + } + + return dict; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Sets the list of default valid items. + /// + /// ------------------------------------------------------------------------------------ + private void SetDefaultValidItems() + { + m_validItemsDictionary = new Dictionary(); + + foreach (char cc in m_categorizer.WordFormingCharacters) + m_validItemsDictionary[cc.ToString()] = true; + + //foreach (char cc in m_categorizer.PunctuationCharacters) + // m_validItemsDictionary[cc.ToString()] = true; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Update the parameter values for storing the valid and invalid lists in CheckDataSource + /// and then save them. This is here because the inventory form does not know the names of + /// the parameters that need to be saved for a given check, only the check knows this. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_checksDataSource.SetParameterValue(kValidItemsParameter, m_validItems); + m_checksDataSource.SetParameterValue(kInvalidItemsParameter, m_invalidItems); + m_checksDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Runs the Characters Scripture checks. + /// + /// The Scripture tokens to check. + /// Method to record the error. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + // This method is called in ScrChecksDataSource.cs - RunCheck(IScriptureCheck check) + m_categorizer = m_checksDataSource.CharacterCategorizer; + + // Get parameters needed to run this check. + GetParameters(); + + // Find all invalid characters and place them in 'm_characterSequences' + GetReferences(toks, string.Empty, true); + + foreach (TextTokenSubstring tts in m_characterSequences) + { + tts.Message = (tts.ToString().Length > 1) ? + m_checksDataSource.GetLocalizedString("Invalid or unknown character diacritic combination") : + m_checksDataSource.GetLocalizedString("Invalid or unknown character"); + + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Get (invalid) character references. + /// + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { + return GetReferences(tokens, desiredKey, false); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Get (invalid) character references. + /// + /// ------------------------------------------------------------------------------------ + private List GetReferences(IEnumerable tokens, string desiredKey, + bool invalidCharactersOnly) + { + if (m_categorizer == null) + m_categorizer = m_checksDataSource.CharacterCategorizer; + + m_characterSequences = new List(); + Dictionary> htValidChars = + new Dictionary>(); + Dictionary currentDictionary = null; + string preferredLocale = m_checksDataSource.GetParameterValue("PreferredLocale") ?? string.Empty; + + foreach (ITextToken tok in tokens) + { + string locale = tok.Locale ?? string.Empty; + + if (tok.Text == null || (!invalidCharactersOnly && locale != preferredLocale)) + continue; + + if (!htValidChars.TryGetValue(locale, out currentDictionary)) + { + currentDictionary = StringToDictionary(GetValidCharacters(locale)); + htValidChars.Add(locale, currentDictionary); + } + + int offset = 0; + + foreach (string key in ParseCharacterSequences(tok.Text)) + { + bool lookingForASpecificKey = (desiredKey != ""); + bool keyMatches = (desiredKey == key); + bool invalidItem = false; + + if (invalidCharactersOnly) + { + // REVIEW (BobbydV): IndexOf causes false positives for certain + // characters (e.g., U+0234 & U+1234). I think Contains is easier to read + // and should work for both TE and Paratext for the "AlwaysValidCharacters" + // list. (TomB) + if (!m_alwaysValidCharacters.Contains(key) && + !currentDictionary.ContainsKey(key)) + invalidItem = true; + } + + if ((lookingForASpecificKey && keyMatches) || + (!lookingForASpecificKey && !invalidCharactersOnly) || + (invalidCharactersOnly && invalidItem)) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, offset, key.Length); + m_characterSequences.Add(tts); + } + + offset += key.Length; + } + } + + return m_characterSequences; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the valid characters list for the specified locale. + /// + /// ------------------------------------------------------------------------------------ + private string GetValidCharacters(string locale) + { + string parameter = (string.IsNullOrEmpty(locale) ? + kValidItemsParameter : kValidItemsParameter + "_" + locale); + + string validChars = m_checksDataSource.GetParameterValue(parameter); + validChars = validChars ?? string.Empty; + + return validChars; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Parses a string into character sequences. + /// + /// ------------------------------------------------------------------------------------ + public IEnumerable ParseCharacterSequences(string text) + { + string key = ""; + bool diacricsFollow = m_categorizer.DiacriticsFollowBaseCharacters(); + + foreach (char cc in text) + { + if (m_categorizer.IsDiacritic(cc)) + { + if (diacricsFollow) + { + key += cc; + } + else + { + if (key != "") yield return key; + key = cc.ToString(); + } + } + else + { + if (key != "") yield return key; + key = cc.ToString(); + } + } + + if (key != "") yield return key; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the parameters needed for this check. + /// + /// ------------------------------------------------------------------------------------ + private void GetParameters() + { + string validItemsText = m_checksDataSource.GetParameterValue(kValidItemsParameter); + if (validItemsText == null || validItemsText.Trim() == "") + SetDefaultValidItems(); + else + ValidItems = validItemsText; + + m_alwaysValidCharacters = m_checksDataSource.GetParameterValue(kAlwaysValidItemsParameter); + if (String.IsNullOrEmpty(m_alwaysValidCharacters)) + m_alwaysValidCharacters = " \r\n*+\\0123456789"; + + InvalidItems = m_checksDataSource.GetParameterValue(kInvalidItemsParameter); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Creates an inventory of the tokens. + /// + /// The book number. + /// The inventory. + /// The tokens. + /// ------------------------------------------------------------------------------------ + public void InventoryTokens(int bookNum, TextInventory inventory, + IEnumerable tokens) + { + foreach (ITextToken tok in tokens) + { + foreach (string key in ParseCharacterSequences(tok.Text)) + { + // Don't inventory spaces, lf, cr. + if (key == " " || key == "\r" || key == "\n") + continue; + + inventory.GetValue(key).AddReference(bookNum); + } + } + } + } +} diff --git a/Lib/src/ScrChecks/MatchedPairsCheck.cs b/Lib/src/ScrChecks/MatchedPairsCheck.cs new file mode 100644 index 0000000000..312652177b --- /dev/null +++ b/Lib/src/ScrChecks/MatchedPairsCheck.cs @@ -0,0 +1,471 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + #region MatchedPairsCheck class + /// ---------------------------------------------------------------------------------------- + /// + /// The matched pairs check has an inventory mode in Paratext. TE doesn't use the inventory + /// stuff. + /// + /// ---------------------------------------------------------------------------------------- + public class MatchedPairsCheck : IScrCheckInventory + { + private const string kValidItemsParameter = "MatchedPairingCharacters"; + private const string kInvalidItemsParameter = "UnmatchedPairingCharacters"; + + private IChecksDataSource m_checksDataSource; +// private CharacterCategorizer m_characterCategorizer; + private List m_unmatchedPairs; + private string m_validItems; + private string m_invalidItems; + private List m_validItemsList; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The checks data source. + /// ------------------------------------------------------------------------------------ + public MatchedPairsCheck(IChecksDataSource checksDataSource) + { + m_checksDataSource = checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_checksDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems + { + get { return m_validItems; } + set + { + m_validItems = (value == null ? string.Empty : value.Trim()); + m_validItemsList = new List(); + if (m_validItems != string.Empty) + m_validItemsList = new List(m_validItems.Split()); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return m_invalidItems; } + set { m_invalidItems = (value == null ? string.Empty : value.Trim());} + } + + /// ------------------------------------------------------------------------------------ + /// + /// The full name of the check, e.g. "Matched Pairs". After replacing any spaces + /// with underscores, this can also be used as a key for looking up a localized + /// string if the application supports localization. If this is ever changed, + /// DO NOT change the CheckId! + /// + /// ------------------------------------------------------------------------------------ + public string CheckName { get { return Localize("Matching Punctuation Pairs"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidMatchedPairs; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup { get { return Localize("Basic"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 300; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description { get { return Localize("Checks for unmatched parentheses or other punctuation that normally occurs in pairs."); } } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error (not used in TE). + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Pairs"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Update all parameter values in CheckDataSource and then save them. + /// This is here because the inventory form does not know what parameters + /// need to be saved for a given check, only the check knows this. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_checksDataSource.SetParameterValue(kValidItemsParameter, m_validItems); + m_checksDataSource.SetParameterValue(kInvalidItemsParameter, m_invalidItems); + m_checksDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + m_unmatchedPairs = GetReferences(toks, string.Empty); + + foreach (TextTokenSubstring tts in m_unmatchedPairs) + { + if (!m_validItemsList.Contains(tts.ToString())) + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { +#if DEBUG + List AllTokens = new List(tokens); + if (AllTokens.Count == 0) + { + // Keep the compiler from complaining about assigning to a variable, but not using it. + } +#endif +// m_characterCategorizer = m_checksDataSource.CharacterCategorizer; + ValidItems = m_checksDataSource.GetParameterValue(kValidItemsParameter); + InvalidItems = m_checksDataSource.GetParameterValue(kInvalidItemsParameter); + + string preferredLocale = + m_checksDataSource.GetParameterValue("PreferredLocale") ?? string.Empty; + + string poeticStyles = + m_checksDataSource.GetParameterValue("PoeticStyles"); + + string introductionOutlineStyles = + m_checksDataSource.GetParameterValue("IntroductionOutlineStyles"); + + MatchedPairList pairList = + MatchedPairList.Load(m_checksDataSource.GetParameterValue("MatchedPairs"), + m_checksDataSource.GetParameterValue("DefaultWritingSystemName")); + + StyleCategorizer styleCategorizer = + new StyleCategorizer(poeticStyles, introductionOutlineStyles); + + ProcessMatchedPairTokens bodyProcessor = new ProcessMatchedPairTokens( + m_checksDataSource, pairList, styleCategorizer); + + ProcessMatchedPairTokens noteProcessor = new ProcessMatchedPairTokens( + m_checksDataSource, pairList, styleCategorizer); + + m_unmatchedPairs = new List(); + + foreach (ITextToken tok in tokens) + { + if (tok.Text == null || (tok.Locale ?? string.Empty) != preferredLocale) + continue; + + if (tok.TextType == TextType.Note) + { + // if a new note is starting finalize any sequences from the previous note + if (tok.IsNoteStart) + noteProcessor.FinalizeResult(desiredKey, m_unmatchedPairs); + noteProcessor.ProcessToken(tok, desiredKey, m_unmatchedPairs); + } + else if (tok.TextType == TextType.Verse || tok.TextType == TextType.Other || tok.IsParagraphStart) + { + // body text: finalize any note that was in progress and continue with body text + noteProcessor.FinalizeResult(desiredKey, m_unmatchedPairs); + bodyProcessor.ProcessToken(tok, desiredKey, m_unmatchedPairs); + } + } + + noteProcessor.FinalizeResult(desiredKey, m_unmatchedPairs); + bodyProcessor.FinalizeResult(desiredKey, m_unmatchedPairs); + + return m_unmatchedPairs; + } + } + + #endregion + + #region ProcessMatchedPairTokens class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + class ProcessMatchedPairTokens + { + private List m_pairTokensFound = new List(); + private MatchedPairList m_pairList; + private StyleCategorizer m_styleCategorizer; + private IChecksDataSource m_checksDataSource; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public ProcessMatchedPairTokens(IChecksDataSource checksDataSource, + MatchedPairList pairList, StyleCategorizer styleCategorizer) + { + m_checksDataSource = checksDataSource; + m_pairList = pairList; + m_styleCategorizer = styleCategorizer; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a value indicating whether or not any of the found pair tokens is part of a + /// pair that should be closed by the end of the paragraph in which it is found. + /// + /// ------------------------------------------------------------------------------------ + private bool AnyFoundPairsClosedByPara + { + get + { + foreach (TextTokenSubstring tok in m_pairTokensFound) + { + MatchedPair pair = m_pairList.GetPairForOpen(tok.Text); + if (pair != null && !pair.PermitParaSpanning) + return true; + } + + return false; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void ProcessToken(ITextToken tok, string desiredKey, List result) + { + if (AnyFoundPairsClosedByPara && tok.IsParagraphStart && + !m_styleCategorizer.IsPoeticStyle(tok.ParaStyleName)) + { + FinalizeResult(desiredKey, result); + } + + for (int i = 0; i < tok.Text.Length; i++) + { + string cc = tok.Text.Substring(i, 1); + if (m_pairList.BelongsToPair(cc)) + { + StoreFoundPairToken(tok, i); + RemoveMatchedPunctAtEndOfFirstWordInIntroOutline(tok, i); + RemoveIfMatchedPairFound(); + RecordOverlappingPairs(); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void StoreFoundPairToken(ITextToken tok, int i) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, i, 1); + + // Assign an initial, default message which may be changed later + tts.Message = m_checksDataSource.GetLocalizedString("Unmatched punctuation"); + m_pairTokensFound.Add(tts); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void RemoveMatchedPunctAtEndOfFirstWordInIntroOutline(ITextToken tok, int i) + { + if (!m_styleCategorizer.IsIntroductionOutlineStyle(tok.ParaStyleName)) + return; + + // See if we are at the end of the first word + string[] words = tok.Text.Split(); + string firstWord = words[0]; + if (i + 1 != firstWord.Length) + return; + + int lastFoundPairToken = m_pairTokensFound.Count - 1; + + // If the current matched pair is in an introduction outline, + // ends the first word, and is a closing punct, remove it. + if (m_pairList.IsClose(m_pairTokensFound[lastFoundPairToken].Text)) + m_pairTokensFound.RemoveAt(lastFoundPairToken); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks if the last two pair tokens in the found pair tokens are a matched pair. + /// If so, they are removed from the found list since a matched set has been complete. + /// + /// ------------------------------------------------------------------------------------ + private void RemoveIfMatchedPairFound() + { + if (m_pairTokensFound.Count < 2) + return; + + TextTokenSubstring possibleClose = m_pairTokensFound[m_pairTokensFound.Count - 1]; + TextTokenSubstring possibleOpen = m_pairTokensFound[m_pairTokensFound.Count - 2]; + + if (m_pairList.IsMatchedPair(possibleOpen.Text, possibleClose.Text)) + { + // Found a matched pair, remove last two tokens + m_pairTokensFound.RemoveAt(m_pairTokensFound.Count - 1); + m_pairTokensFound.RemoveAt(m_pairTokensFound.Count - 1); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void RecordOverlappingPairs() + { + if (m_pairTokensFound.Count < 4) + return; + + TextTokenSubstring tok1 = m_pairTokensFound[m_pairTokensFound.Count - 4]; + TextTokenSubstring tok2 = m_pairTokensFound[m_pairTokensFound.Count - 3]; + TextTokenSubstring tok3 = m_pairTokensFound[m_pairTokensFound.Count - 2]; + TextTokenSubstring tok4 = m_pairTokensFound[m_pairTokensFound.Count - 1]; + + // Check if pairs are overlapping. + if (m_pairList.IsOpen(tok1.Text) && m_pairList.IsOpen(tok2.Text) && + m_pairList.IsMatchedPair(tok1.Text, tok3.Text) && + m_pairList.IsMatchedPair(tok2.Text, tok4.Text)) + { + // Found overlapping pairs, so record this by changing + // the message in the needed TextTokenSubstrings + string msg = m_checksDataSource.GetLocalizedString("Overlapping pair"); + tok1.Message = tok2.Message = tok3.Message = tok4.Message = msg; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void FinalizeResult(string desiredKey, List result) + { + // Each matched pair character left in the list is invalid, + // so add to the list of unmatchedPairs + foreach (TextTokenSubstring tok in m_pairTokensFound) + { + if (desiredKey == string.Empty || desiredKey == tok.InventoryText) + result.Add(tok); + } + + m_pairTokensFound.Clear(); + } + } + + #endregion + + #region StyleCategorizer class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + class StyleCategorizer + { + private List m_poeticStyles; + private List m_introOutlineStyles; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public StyleCategorizer(string customPoeticStyles, string customIntroOutlineStyles) + { + m_poeticStyles = new List( + customPoeticStyles.Split(CheckUtils.kStyleNamesDelimiter)); + + m_introOutlineStyles = new List( + customIntroOutlineStyles.Split(CheckUtils.kStyleNamesDelimiter)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public bool IsPoeticStyle(string style) + { + return (m_poeticStyles.IndexOf(style) >= 0); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public bool IsIntroductionOutlineStyle(string style) + { + return (m_introOutlineStyles.IndexOf(style) >= 0); + } + } + + #endregion +} diff --git a/Lib/src/ScrChecks/MixedCapitalizationCheck.cs b/Lib/src/ScrChecks/MixedCapitalizationCheck.cs new file mode 100644 index 0000000000..e77d2e4688 --- /dev/null +++ b/Lib/src/ScrChecks/MixedCapitalizationCheck.cs @@ -0,0 +1,425 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + #region MixedCapitalizationCheck class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + public class MixedCapitalizationCheck : IScrCheckInventory + { + private const string kValidItemsParameter = "ValidMixedCapitalization"; + private const string kInvalidItemsParameter = "InvalidMixedCapitalization"; + + private IChecksDataSource m_checksDataSource; + private CharacterCategorizer m_characterCategorizer; + private List m_mixedCapitalization; + private string m_validItems; + private string m_invalidItems; + private List m_validItemsList; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public MixedCapitalizationCheck(IChecksDataSource checksDataSource) + { + m_checksDataSource = checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_checksDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems + { + get { return m_validItems; } + set + { + m_validItems = (value == null ? string.Empty : value.Trim()); + m_validItemsList = new List(); + if (m_validItems != string.Empty) + m_validItemsList = new List(m_validItems.Split()); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return m_invalidItems; } + set { m_invalidItems = (value == null ? string.Empty : value.Trim()); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public string CheckName { get { return Localize("Mixed Capitalization"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidMixedCapitalization; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup { get { return Localize("Basic"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 700; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description { get { return Localize("Checks for words with a potentially invalid mix of uppercase and lowercase letters."); } } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Mixed Capitalization Word"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Update the parameter values for storing the valid and invalid lists in CheckDataSource + /// and then save them. This is here because the inventory form does not know the names of + /// the parameters that need to be saved for a given check, only the check knows this. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_checksDataSource.SetParameterValue(kValidItemsParameter, m_validItems); + m_checksDataSource.SetParameterValue(kInvalidItemsParameter, m_invalidItems); + m_checksDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Execute the check. Call 'RecordError' for every error found. + /// + /// ITextTokens corresponding to the text to be checked. + /// Typically this is one books worth. + /// Call this delegate to report each error found. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + m_mixedCapitalization = GetReferences(toks, string.Empty); + + string msg = Localize("Word has mixed capitalization"); + + foreach (TextTokenSubstring tts in m_mixedCapitalization) + { + if (!m_validItemsList.Contains(tts.ToString())) + { + tts.Message = msg; + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Get all instances of the item being checked in the token list passed. + /// This includes both valid and invalid instances. + /// This is used 1) to create an inventory of these items. + /// To show the user all instance of an item with a specified key. + /// 2) With a "desiredKey" in order to fetch instance of a specific + /// item (e.g. all the places where "the" is a repeated word. + /// + /// Tokens for text to be scanned + /// If you only want instance of a specific key (e.g. one word, + /// one punctuation pattern, one character, etc.) place it here. Empty string returns + /// all items. + /// List of token substrings + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { +#if DEBUG + List AllTokens = new List(tokens); + if (AllTokens.Count == 0) + { + // Keep the compiler from complaining about assigning to a variable, but not using it. + } +#endif + m_characterCategorizer = m_checksDataSource.CharacterCategorizer; + ValidItems = m_checksDataSource.GetParameterValue(kValidItemsParameter); + InvalidItems = m_checksDataSource.GetParameterValue(kInvalidItemsParameter); + + string preferredLocale = + m_checksDataSource.GetParameterValue("PreferredLocale") ?? string.Empty; + + m_mixedCapitalization = new List(); + ProcessMixedCapitalization processor = + new ProcessMixedCapitalization(m_checksDataSource, m_mixedCapitalization); + + foreach (ITextToken tok in tokens) + { + if ((tok.Locale ?? string.Empty) != preferredLocale) + continue; + + foreach (WordAndPunct wap in m_characterCategorizer.WordAndPuncts(tok.Text)) + processor.ProcessWord(tok, wap, desiredKey); + } + + return m_mixedCapitalization; + } + } + + #endregion + + #region AWord class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + public class AWord + { + private CharacterCategorizer m_categorizer; + private string m_text = string.Empty; + private string m_prefix = string.Empty; + private string m_suffix = string.Empty; + private int m_upperCaseLetters = 0; + private int m_lowerCaseLetters = 0; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public AWord(string text, CharacterCategorizer categorizer) + { + this.m_text = text; + this.m_categorizer = categorizer; + + string word = CountLettersAndReturnWordWithOnlyWordFormingCharacters(text); + if (m_lowerCaseLetters == 0 || m_upperCaseLetters == 0) + return; + FindPrefixAndSuffixIfAny(word); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private string CountLettersAndReturnWordWithOnlyWordFormingCharacters(string text) + { + for (int i = 0; i < text.Length; i++) + { + char cc = text[i]; + if (m_categorizer.IsUpper(cc)) + m_upperCaseLetters++; + if (m_categorizer.IsLower(cc)) + m_lowerCaseLetters++; + if (m_categorizer.IsTitle(cc)) + { + m_upperCaseLetters++; + m_lowerCaseLetters++; + } + if (!m_categorizer.IsWordFormingCharacter(cc)) + { + text = text.Remove(i, 1); + i--; + } + } + return text; + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void FindPrefixAndSuffixIfAny(string word) + { + for (int i = 1; i < word.Length; i++) + { + char cc = word[i]; + if (m_categorizer.IsUpper(cc) || m_categorizer.IsTitle(cc)) + { + m_prefix = word.Substring(0, i); + m_suffix = word.Substring(i); + return; + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public string Prefix + { + get { return m_prefix; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public string Suffix + { + get { return m_suffix; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public override string ToString() + { + return "(" + m_prefix + ")" + m_text + "(" + m_suffix + ")"; + } + } + + #endregion + + #region ProcessMixedCapitalization class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + class ProcessMixedCapitalization + { + private CharacterCategorizer m_categorizer; + private List m_uncapitalizedPrefixes; + private List m_capitalizedSuffixes; + private List m_capitalizedPrefixes; + private List m_result; + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public ProcessMixedCapitalization(IChecksDataSource checksDataSource, + List result) + { + m_categorizer = checksDataSource.CharacterCategorizer; + m_result = result; + + m_uncapitalizedPrefixes = new List( + checksDataSource.GetParameterValue("UncapitalizedPrefixes").Split()); + + m_capitalizedSuffixes = new List( + checksDataSource.GetParameterValue("CapitalizedSuffixes").Split()); + + m_capitalizedPrefixes = new List( + checksDataSource.GetParameterValue("CapitalizedPrefixes").Split()); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void ProcessWord(ITextToken tok, WordAndPunct wap, string desiredKey) + { + AWord word = new AWord(wap.Word, m_categorizer); + + if (word.Prefix == string.Empty && word.Suffix == string.Empty) + return; + if (m_uncapitalizedPrefixes.Contains(word.Prefix)) + return; + if (m_uncapitalizedPrefixes.Contains("*" + word.Prefix[word.Prefix.Length - 1])) + return; + if (m_uncapitalizedPrefixes.Contains("*")) + return; + if (m_capitalizedSuffixes.Contains(word.Suffix)) + return; + if (m_capitalizedPrefixes.Contains(word.Prefix)) + return; + + AddWord(tok, wap, desiredKey); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void AddWord(ITextToken tok, WordAndPunct wap, string desiredKey) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, wap.Offset, wap.Word.Length); + if (String.IsNullOrEmpty(desiredKey) || desiredKey == tts.InventoryText) + m_result.Add(tts); + } + } + + #endregion +} diff --git a/Lib/src/ScrChecks/Properties/AssemblyInfo.cs b/Lib/src/ScrChecks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d665ee0af7 --- /dev/null +++ b/Lib/src/ScrChecks/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Checks")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Lib/src/ScrChecks/Properties/Resources.Designer.cs b/Lib/src/ScrChecks/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..b1853f76ff --- /dev/null +++ b/Lib/src/ScrChecks/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.239 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SILUBS.ScriptureChecks.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SILUBS.ScriptureChecks.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Lib/src/ScrChecks/Properties/Resources.resx b/Lib/src/ScrChecks/Properties/Resources.resx new file mode 100644 index 0000000000..b6da5948be --- /dev/null +++ b/Lib/src/ScrChecks/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Lib/src/ScrChecks/Properties/Settings.Designer.cs b/Lib/src/ScrChecks/Properties/Settings.Designer.cs new file mode 100644 index 0000000000..e57e4ed0ff --- /dev/null +++ b/Lib/src/ScrChecks/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.239 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SILUBS.ScriptureChecks.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Lib/src/ScrChecks/Properties/Settings.settings b/Lib/src/ScrChecks/Properties/Settings.settings new file mode 100644 index 0000000000..1d7f063843 --- /dev/null +++ b/Lib/src/ScrChecks/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Lib/src/ScrChecks/PunctuationCheck.cs b/Lib/src/ScrChecks/PunctuationCheck.cs new file mode 100644 index 0000000000..5c3fb5651d --- /dev/null +++ b/Lib/src/ScrChecks/PunctuationCheck.cs @@ -0,0 +1,818 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + public enum PunctuationTokenType { whitespace, punctuation, number, paragraph, quoteSeparator }; + public enum CheckingLevel { Advanced, Intermediate, Basic }; + + #region PunctuationCheck class + /// ---------------------------------------------------------------------------------------- + /// + /// Checks sequences of punctuation (in relation to their positions in surrounding text). + /// + /// ---------------------------------------------------------------------------------------- + public class PunctuationCheck : IScrCheckInventory + { + private const string kValidItemsParameter = "ValidPunctuation"; + private const string kInvalidItemsParameter = "InvalidPunctuation"; + + private IChecksDataSource m_checksDataSource; + private CharacterCategorizer m_characterCategorizer; + private List m_punctuationSequences; + //! (PARATEXT) need to think about the possibility that _ is a punctuation mark in this language. + internal static string s_whitespaceRep = "_"; + private List m_validItemsList; + private List m_invalidItemsList; + private string m_validItems; + private string m_invalidItems; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The checks data source. + /// ------------------------------------------------------------------------------------ + public PunctuationCheck(IChecksDataSource checksDataSource) + { + m_checksDataSource = checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_checksDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems + { + get { return m_validItems; } + set + { + m_validItems = (value == null ? string.Empty : value.Trim()); + m_validItemsList = new List(); + if (m_validItems != string.Empty) + m_validItemsList = new List(m_validItems.Split()); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return m_invalidItems; } + set + { + m_invalidItems = (value == null ? string.Empty : value.Trim()); + m_invalidItemsList = new List(); + if (m_invalidItems != string.Empty) + m_invalidItemsList = new List(m_invalidItems.Split()); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// The full name of the check, e.g. "Punctuation". After replacing any spaces + /// with underscores, this can also be used as a key for looking up a localized + /// string if the application supports localization. If this is ever changed, + /// DO NOT change the CheckId! + /// + /// ------------------------------------------------------------------------------------ + public string CheckName { get { return Localize("Punctuation Patterns"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidPunctuation; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup { get { return Localize("Basic"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 500; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description { get { return Localize("Checks for potential inconsistencies in the use of punctuation."); } } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Punctuation"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Update the parameter values for storing Paratext's valid and invalid lists in + /// CheckDataSource and then save them. This is here because the Paratext inventory form + /// does not know the names of the parameters that need to be saved for a given check, + /// only the check knows this. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_checksDataSource.SetParameterValue(kValidItemsParameter, ValidItems); + m_checksDataSource.SetParameterValue(kInvalidItemsParameter, InvalidItems); + m_checksDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Execute the check. Call 'RecordError' for every error found. + /// + /// ITextToken's corresponding to the text to be checked. + /// Typically this is one books worth. + /// Call this delegate to report each error found. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + m_punctuationSequences = GetReferences(toks, string.Empty); + + string msgInvalid = Localize("Invalid punctuation pattern"); + string msgUnspecified = Localize("Unspecified use of punctuation pattern"); + + foreach (TextTokenSubstring tts in m_punctuationSequences) + { + string punctCharacter = tts.InventoryText; + + if (!m_validItemsList.Contains(punctCharacter)) + { + tts.Message = m_invalidItemsList.Contains(punctCharacter) ? + msgInvalid : msgUnspecified; + + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Return a TextTokenSubstring for all occurances of the desiredKey. + /// + /// + /// e.g., _[_ or empty string to look for all patterns + /// + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { +#if DEBUG + List AllTokens = new List(tokens); + if (AllTokens.Count == 0) + { + // Keep the compiler from complaining about assigning to a variable, but not using it. + } +#endif + m_characterCategorizer = m_checksDataSource.CharacterCategorizer; + string sXmlMatchedPairs = m_checksDataSource.GetParameterValue("PunctuationPatterns"); + if (sXmlMatchedPairs != null && sXmlMatchedPairs.Trim().Length > 0) + { + m_validItemsList = new List(); + m_invalidItemsList = new List(); + PuncPatternsList puncPatternsList = PuncPatternsList.Load(sXmlMatchedPairs, + m_checksDataSource.GetParameterValue("DefaultWritingSystemName")); + foreach (PuncPattern pattern in puncPatternsList) + { + if (pattern.Valid) + m_validItemsList.Add(pattern.Pattern); + else + m_invalidItemsList.Add(pattern.Pattern); + } + } + else + { + ValidItems = m_checksDataSource.GetParameterValue(kValidItemsParameter); + InvalidItems = m_checksDataSource.GetParameterValue(kInvalidItemsParameter); + } + + string sLevel = m_checksDataSource.GetParameterValue("PunctCheckLevel"); + CheckingLevel level; + switch (sLevel) + { + case "Advanced": level = CheckingLevel.Advanced; break; + case "Intermediate": level = CheckingLevel.Intermediate; break; + case "Basic": + default: + level = CheckingLevel.Basic; + break; + } + string sWhitespaceRep = m_checksDataSource.GetParameterValue("PunctWhitespaceChar"); + if (!String.IsNullOrEmpty(sWhitespaceRep)) + s_whitespaceRep = sWhitespaceRep.Substring(0, 1); + string preferredLocale = + m_checksDataSource.GetParameterValue("PreferredLocale") ?? string.Empty; + + QuotationMarkCategorizer quotationCategorizer = + new QuotationMarkCategorizer(m_checksDataSource); + + // create processing state machines, one for body text, one for notes + ProcessPunctationTokens bodyProcessor = new ProcessPunctationTokens( + m_characterCategorizer, quotationCategorizer, level); + + ProcessPunctationTokens noteProcessor = new ProcessPunctationTokens( + m_characterCategorizer, quotationCategorizer, level); + + m_punctuationSequences = new List(); + + // build list of note and non-note tokens + foreach (ITextToken tok in tokens) + { + if (tok.Text == null || (tok.Locale ?? string.Empty) != preferredLocale) + continue; + + if (tok.TextType == TextType.Note) + { + // if a new note is starting finalize any punctuation sequences from the previous note + if (tok.IsNoteStart) + noteProcessor.FinalizeResult(desiredKey, m_punctuationSequences, true); + noteProcessor.ProcessToken(tok, desiredKey, m_punctuationSequences); + } + else if (tok.TextType == TextType.Verse || tok.TextType == TextType.Other) + { + // body text: finalize any note that was in progress and continue with body text + noteProcessor.FinalizeResult(desiredKey, m_punctuationSequences, true); + bodyProcessor.ProcessToken(tok, desiredKey, m_punctuationSequences); + } + else if (tok.IsParagraphStart) + { + bodyProcessor.FinalizeResult(desiredKey, m_punctuationSequences, true); + bodyProcessor.TreatAsParagraphStart = true; + } + } + + noteProcessor.FinalizeResult(desiredKey, m_punctuationSequences, true); + bodyProcessor.FinalizeResult(desiredKey, m_punctuationSequences, true); + + return m_punctuationSequences; + } + } + + #endregion + + #region PunctuationToken class + /// ---------------------------------------------------------------------------------------- + /// + /// Record information about one component of a punctuation sequence. + /// + /// ---------------------------------------------------------------------------------------- + class PunctuationToken + { + public PunctuationTokenType TokenType; + public TextTokenSubstring Tts; + + // is initial (opening) quotation punctuation, e.g. U+201C LEFT DOUBLE QUOTATION MARK + public bool IsInitial; + + // is final (closing) quotation punctuation, e.g. U+201D RIGHT DOUBLE QUOTATION MARK + public bool IsFinal; + + // is token a paragraph break + public bool IsParaBreak; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class for a + /// whitespace character. + /// + /// if set to true is opening quotation mark. + /// if set to true is closing quotation mark. + /// if set to true the whitespace represents a newline. + /// + /// ------------------------------------------------------------------------------------ + public PunctuationToken(bool isInitial, bool isFinal, bool isParaBreak) : + this(PunctuationTokenType.whitespace, null, isInitial, isFinal) + { + IsParaBreak = isParaBreak; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// Type of the token. + /// The TextTokenSubstring. + /// if set to true is opening quotation mark. + /// if set to true is closing quotation mark. + /// ------------------------------------------------------------------------------------ + public PunctuationToken(PunctuationTokenType tokenType, TextTokenSubstring tts, + bool isInitial, bool isFinal) + { + TokenType = tokenType; + Tts = tts; + IsInitial = isInitial; + IsFinal = isFinal; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a value indicating whether this instance is punctuation or a special whitespace + /// character to separate same-direction quote marks (which should behave like + /// punctuation). + /// + /// ------------------------------------------------------------------------------------ + public bool IsPunctuation + { + get + { + return TokenType == PunctuationTokenType.punctuation || + TokenType == PunctuationTokenType.quoteSeparator; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a that represents the current PunctuationToken. + /// + /// ------------------------------------------------------------------------------------ + public override string ToString() + { + switch (TokenType) + { + case PunctuationTokenType.whitespace: + case PunctuationTokenType.quoteSeparator: + return PunctuationCheck.s_whitespaceRep; + case PunctuationTokenType.number: + return string.Empty; + default: + return Tts.Text; + } + } + } + + #endregion + + #region ProcessPunctationTokens class + /// ---------------------------------------------------------------------------------------- + /// + /// State machine to process sequences of punctuation tokens. + /// We have one of these objects for note text, one for body text. + /// + /// ---------------------------------------------------------------------------------------- + class ProcessPunctationTokens + { + // current punctuation sequence, emptied when finalized or a new word starts + private List m_puncts = new List(); + private CharacterCategorizer m_categorizer = null; // this lets us know what a punctuation character is + private QuotationMarkCategorizer m_quotationCategorizer; + private CheckingLevel m_level; + private bool m_finalizedWithNumber = false; + private bool m_fTreatAsParagraphStart = true; + + /// ------------------------------------------------------------------------------------ + /// + /// Sets a value indicating whether to treat a token as a paragraph start even if it + /// isn't (because a previous token that wasn't processed represented the start of a + /// para). + /// + /// ------------------------------------------------------------------------------------ + internal bool TreatAsParagraphStart + { + set { m_fTreatAsParagraphStart = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The categorizer. + /// The quotation categorizer. + /// Indicator to determine how much to combine contiguous + /// punctuation sequences into patterns. Advanced = All contiguous punctuation and + /// whitespace characters form a single pattern; Intermediate = Contiguous punctuation + /// forms a single pattern (delimeted by whitespace); Basic = Each punctuation character + /// stands alone. In all three modes, whitespace before and/or after a punctuation token + /// indicates whether is is word-initial, word-medial, word-final, or isolated + /// ------------------------------------------------------------------------------------ + public ProcessPunctationTokens(CharacterCategorizer categorizer, + QuotationMarkCategorizer quotationCategorizer, CheckingLevel level) + { + m_categorizer = categorizer; + m_quotationCategorizer = quotationCategorizer; + m_level = level; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Extract the punctuation sequences from this token + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void ProcessToken(ITextToken tok, string desiredKey, List result) + { + if (tok.IsParagraphStart || m_fTreatAsParagraphStart) + { + ProcessWhitespaceOrParagraph(true); + m_fTreatAsParagraphStart = false; + } + + // for each character in token + for (int i = 0; i < tok.Text.Length; ++i) + { + char cc = tok.Text[i]; + if (m_categorizer.IsPunctuation(cc)) + ProcessPunctuation(tok, i); + else if (char.IsDigit(cc)) + { + // If the previous finalized was done with a number, + // and we have a single punctuation mark + // followed by another number, ignore this sequence, + // e.g. 3:14 + if (m_finalizedWithNumber && m_puncts.Count == 1 && + m_puncts[0].TokenType == PunctuationTokenType.punctuation) + { + m_puncts.Clear(); + } + else + { + ProcessDigit(tok, i); + FinalizeResult(desiredKey, result, false); + } + } + else if (char.IsWhiteSpace(cc)) + ProcessWhitespaceOrParagraph(false); + else + { + // if not punctuation, whitespace, or digit; it must be the start of a new word + // therefore finalize any open punctuation sequence + FinalizeResult(desiredKey, result, false); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Add punctuation to list + /// + /// The text token + /// The index of the punctuation character + /// ------------------------------------------------------------------------------------ + private void ProcessPunctuation(ITextToken tok, int i) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, i, 1); + bool isInitial = m_quotationCategorizer.IsInitialPunctuation(tts.Text); + bool isFinal = m_quotationCategorizer.IsFinalPunctuation(tts.Text); + m_puncts.Add(new PunctuationToken(PunctuationTokenType.punctuation, tts, isInitial, isFinal)); + + // special case: treat a sequence like + // opening quotation punctuation/space/opening quotation punctuation + // as if the space were not there. an example of this would be + // U+201C LEFT DOUBLE QUOTATION MARK + // U+0020 SPACE + // U+2018 LEFT SINGLE QUOTATION MARK + // this allows a quotation mark to be considered word initial even if it is followed by a space + if (m_puncts.Count >= 3) + { + // If the last three tokens are punctuation/whitespace/punctuation + if (m_puncts[m_puncts.Count - 2].TokenType == PunctuationTokenType.whitespace && + !m_puncts[m_puncts.Count - 2].IsParaBreak && + m_puncts[m_puncts.Count - 3].TokenType == PunctuationTokenType.punctuation) + { + // And both punctuation have quote directions which point in the same direction, + if (m_puncts[m_puncts.Count - 3].IsInitial && m_puncts[m_puncts.Count - 1].IsInitial || + m_puncts[m_puncts.Count - 3].IsFinal && m_puncts[m_puncts.Count - 1].IsFinal) + { + // THEN mark the whitespace as a quote separator. + m_puncts[m_puncts.Count - 2].TokenType = PunctuationTokenType.quoteSeparator; + } + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Add a number to the list + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private void ProcessDigit(ITextToken tok, int i) + { + m_puncts.Add(new PunctuationToken(PunctuationTokenType.number, null, false, false)); + +#if UNUSED + // special case: treat a sequence like + // number/punctuation/number + // as if the punctuation were not there. an example of this would be 1:2 + // this allows the : in 1:2 not to be counted as punctuation + if (tokens.Count >= 3) + { + // If the last three tokens are number/select punctuation/number + if (tokens[tokens.Count - 3].TokenType == PunctuationTokenType.number) + { + string separator = tokens[tokens.Count - 2].ToString(); + //! make the list of separator characters configurable + if (separator == "," || separator == "." || separator == "-" || separator == ":") + { + tokens.RemoveAt(tokens.Count - 2); + + // The offset (-2) stays the same as the line of code above + // since after the previous line is executed some of the tokens shift position. + tokens.RemoveAt(tokens.Count - 2); + } + } + } +#endif + } + + /// ------------------------------------------------------------------------------------ + /// + /// Add whitespace to the list unless the last item in the list is already whitespace + /// + /// True for the token to be a paragraph start, false otherwise. + /// + /// ------------------------------------------------------------------------------------ + public void ProcessWhitespaceOrParagraph(bool fIsParaStart) + { + if (m_puncts.Count > 0 && m_puncts[m_puncts.Count - 1].TokenType == PunctuationTokenType.whitespace) + { + if (!m_puncts[m_puncts.Count - 1].IsParaBreak && fIsParaStart) + m_puncts[m_puncts.Count - 1].IsParaBreak = true; + return; + } + + m_puncts.Add(new PunctuationToken(false, false, fIsParaStart)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public void FinalizeResult(string desiredKey, List result, bool addWhitespace) + { + // If a digit caused FinalizeResult() to be called set a flag, otherwise clear the flag. + // This flag is tested to help see if a punctuation character occurs between two digits. + m_finalizedWithNumber = + (m_puncts.Count > 0 && m_puncts[m_puncts.Count - 1].TokenType == PunctuationTokenType.number); + + // if no punctuation character is found clear sequence and quit + PunctuationToken currentPTok = null; + foreach (PunctuationToken pTok in m_puncts) + { + if (pTok.TokenType == PunctuationTokenType.punctuation) + { + currentPTok = pTok; + break; + } + } + if (currentPTok == null) + { + m_puncts.Clear(); + return; + } + + // if we have been requested to treat this sequence as if it were followed by whitespace, + // then add a space to the sequence. This happens, for example, at the end of a footnote. + // \f + text.\f* otherwise the . would be considered word medial instead of word final + if (addWhitespace) + ProcessWhitespaceOrParagraph(false); + + switch (m_level) + { + case CheckingLevel.Advanced: + AdvancedFinalize(currentPTok, desiredKey, result); + break; + case CheckingLevel.Intermediate: + IntermediateFinalize(desiredKey, result); + break; + case CheckingLevel.Basic: + BasicFinalize(desiredKey, result); + break; + } + + m_puncts.Clear(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Treat each punctuation and whitespace sequence as a single string. It is called + /// advanced since many more inventory items for the user to look at, and only advanced + /// users (we hope) will look at these results. + /// + /// The current punctuation token, whose TextToken substring is + /// modified to indicate a pattern of multiple punctuation characters + /// If specified, indicates a specific punctuation pattern to + /// seek (all others will be discarded); To retrieve all punctation substrings, specify + /// the empty string. + /// List of TextTokenSubstring items that will be added to + /// ------------------------------------------------------------------------------------ + private void AdvancedFinalize(PunctuationToken pTok, string desiredKey, + List result) + { + // concatanate all the punctuation sequences into one string + string pattern = String.Empty; + foreach (PunctuationToken pTok2 in m_puncts) + { + //System.Diagnostics.Debug.Assert(pTok2.Tts == null || pTok2.Tts.Token == pTok.Tts.Token); + pattern += pTok2.ToString(); + } + pTok.Tts.InventoryText = pattern; + + if (desiredKey == String.Empty || desiredKey == pTok.Tts.InventoryText) + result.Add(pTok.Tts); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Treat each punctuation sequence as a single string, breaking the pattern at each + /// whitespace (Except for whitespace between pairs of quotes that are both in the + /// same direction (both opening or closing quotes). + /// + /// If specified, indicates a specific punctuation pattern to + /// seek (all others will be discarded); To retrieve all punctation substrings, specify + /// the empty string. + /// List of TextTokenSubstring items that will be added to + /// ------------------------------------------------------------------------------------ + private void IntermediateFinalize(string desiredKey, List result) + { + // concatanate all the punctuation sequences into one string + string pattern = ""; + PunctuationToken pTok = null; + PunctuationToken tok2; + + for (int i = 0; i < m_puncts.Count; ++i) + { + tok2 = m_puncts[i]; + pattern += tok2.ToString(); + + // Every generated result must start with a punctuation character. + // If we do not currently have a punctuation character (because it + // null'ed below) remember this one. + if (tok2.TokenType == PunctuationTokenType.punctuation || tok2.TokenType == PunctuationTokenType.quoteSeparator) + { + Debug.Assert(pTok != null || tok2.TokenType == PunctuationTokenType.punctuation, "Quote separator should never be the first non-whitespace character in a sequence (after all, it IS whitespace!)"); + if (pTok == null) + pTok = tok2; + else + { + if (tok2.Tts != null && pTok.Tts.LastToken != tok2.Tts.FirstToken) + { + Debug.Assert(tok2.Tts.FirstToken == tok2.Tts.LastToken); + pTok.Tts.AddToken(tok2.Tts.FirstToken); + } + pTok.Tts++; + } + } + + // Generate a pattern when you see a non-leading whitespace or end of list + if (tok2.TokenType == PunctuationTokenType.whitespace || i == m_puncts.Count - 1) + { + if (pTok != null) // Must have a punctuation token + { + pTok.Tts.InventoryText = pattern; + if (desiredKey == "" || desiredKey == pTok.Tts.InventoryText) + result.Add(pTok.Tts); + } + + // Reset pattern to match this token + if (tok2.TokenType == PunctuationTokenType.whitespace) + pattern = tok2.ToString(); + else + pattern = ""; + + pTok = null; + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Basic finalize is complicated because it generates a pattern for every punctuation + /// mark. An exception is if multiple consecutive periods occur, then a string of + /// periods will be in one pattern. + /// + /// If specified, indicates a specific punctuation pattern to + /// seek (all others will be discarded); To retrieve all punctation substrings, specify + /// the empty string. + /// List of TextTokenSubstring items that will be added to + /// ------------------------------------------------------------------------------------ + private void BasicFinalize(string desiredKey, List result) + { + PunctuationToken pTok; + for (int i = 0; i < m_puncts.Count; ++i) + { + pTok = m_puncts[i]; + if (pTok.TokenType != PunctuationTokenType.punctuation) + continue; + + // Normally i and j end up the same. + // When multiple consecutive periods occur (e.g. blah...blah) + // i will be the first period and j the last period. + int j = i; + while (m_puncts[i].ToString() == "." && j + 1 < m_puncts.Count && + m_puncts[j + 1].ToString() == ".") + { + ++j; + } + + string pattern = PunctuationSequencePatternPrefix(i); + for (int k = i; k <= j; ++k) + pattern += m_puncts[k].ToString(); + + pattern += PunctuationSequencePatternSuffix(j); + pTok.Tts.InventoryText = pattern; + + if (desiredKey == String.Empty || desiredKey == pTok.Tts.InventoryText) + result.Add(pTok.Tts); + + i = j; + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Look at previous non-punctuation tokens, the first token that is found determines + /// the prefix for this pattern for example, if it finds whitespace token, it returns + /// a prefix of _ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private string PunctuationSequencePatternPrefix(int index) + { + for (int i = index - 1; i >= 0; --i) + { + if (!m_puncts[i].IsPunctuation) + return m_puncts[i].ToString(); + } + + return string.Empty; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Look at following non-punctuation tokens, the first token that is found determines + /// the suffix for this pattern for example, if it finds whitespace token, it returns + /// a suffix of _ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + private string PunctuationSequencePatternSuffix(int index) + { + for (int i = index; i < m_puncts.Count; ++i) + { + if (!m_puncts[i].IsPunctuation) + return m_puncts[i].ToString(); + } + + return string.Empty; + } + } + + #endregion +} diff --git a/Lib/src/ScrChecks/QuotationCheck.cs b/Lib/src/ScrChecks/QuotationCheck.cs new file mode 100644 index 0000000000..89c8f70a02 --- /dev/null +++ b/Lib/src/ScrChecks/QuotationCheck.cs @@ -0,0 +1,1190 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + #region QuotationCheck class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + public class QuotationCheck : IScrCheckInventory + { + private IChecksDataSource m_chkDataSource; + private CharacterCategorizer m_charCategorizer; + private List m_qmProblems; + private readonly string m_validItemsParameter = "ValidQuotationMarks"; + private readonly string m_invalidItemsParameter = "InvalidQuotationMarks"; + private string m_validItems; + private string m_invalidItems; + private List m_validItemsList; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + public QuotationCheck(IChecksDataSource checksDataSource) + { + m_chkDataSource = checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_chkDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems + { + get { return m_validItems; } + set + { + m_validItems = value.Trim(); + m_validItemsList = (m_validItems == string.Empty ? + new List() : new List(m_validItems.Split())); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return m_invalidItems; } + set { m_invalidItems = value.Trim(); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// The full name of the check, e.g. "Quotation Errors". After replacing any spaces + /// with underscores, this can also be used as a key for looking up a localized + /// string if the application supports localization. If this is ever changed, + /// DO NOT change the CheckId! + /// + /// ------------------------------------------------------------------------------------ + public string CheckName + { + get { return Localize("Quotation Marks"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidQuotations; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup + { + get { return Localize("Basic"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 400; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description + { + get { return Localize("Checks for potential inconsistencies in the markup of quotations."); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Quotations"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Saves the parameters for this check. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_chkDataSource.SetParameterValue(m_validItemsParameter, m_validItems); + m_chkDataSource.SetParameterValue(m_invalidItemsParameter, m_invalidItems); + m_chkDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Checks the given tokens for quotation errors. Ignores any found in 'validItemsList'. + /// Calls the given RecordError handler for each one. + /// + /// The tokens to check. + /// Method to call to record errors. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + foreach (TextTokenSubstring tts in GetReferences(toks, string.Empty)) + { + string punctChar = tts.ToString(); + if (!m_validItemsList.Contains(punctChar)) + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a list if TextTokenSubstrings containing the references and character offsets + /// where quotation problems occur. + /// + /// The tokens (from the data source) to check for quotation problems. + /// empty string. + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { + m_charCategorizer = m_chkDataSource.CharacterCategorizer; + ValidItems = m_chkDataSource.GetParameterValue(m_validItemsParameter); + InvalidItems = m_chkDataSource.GetParameterValue(m_invalidItemsParameter); + + QuotationMarkCategorizer qmCategorizer = new QuotationMarkCategorizer(m_chkDataSource); + m_qmProblems = new List(); + + QTokenProcessor bodyProcessor = new QTokenProcessor(m_chkDataSource, + m_charCategorizer, qmCategorizer, desiredKey, m_qmProblems); + + QTokenProcessor noteProcessor = new QTokenProcessor(m_chkDataSource, + m_charCategorizer, qmCategorizer, desiredKey, m_qmProblems); + + VerseTextToken scrToken = new VerseTextToken(); + foreach (ITextToken tok in tokens) + { + if (tok.TextType == TextType.Note) + { + // If a new note is starting finalize any sequences from the previous note. + if (tok.IsNoteStart) + noteProcessor.FinalizeResult(); + noteProcessor.ProcessToken(tok, null); + } + else if (tok.TextType == TextType.Verse || tok.TextType == TextType.Other || + tok.IsParagraphStart) + { + scrToken.Token = tok; + // body text: finalize any note that was in progress and continue with body text + noteProcessor.FinalizeResult(); + bodyProcessor.ProcessToken(tok, scrToken); + } + } + + noteProcessor.FinalizeResult(); + bodyProcessor.FinalizeResult(); + return m_qmProblems; + } + } + + #endregion + + #region QTokenProcessor class + /// ---------------------------------------------------------------------------------------- + /// + /// Class dedicated to the processing of quotation-related tokens + /// + /// ---------------------------------------------------------------------------------------- + internal class QTokenProcessor + { + private Regex m_regExQuotes; + private Regex m_regExNonQuotes; + private string m_desiredKey; + private bool m_verboseQuotes; + private bool m_fFoundMissingContinuer = false; + private string m_noCloserMsg; + private string m_noOpenerMsg; + private IChecksDataSource m_chkDataSource; + private CharacterCategorizer m_charCategorizer; + private QuotationMarkCategorizer m_qmCategorizer; + private List m_results; + private List m_quotationRelatedTokens = new List(); + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The checks data source. + /// The character categorizer. + /// The quotation mark categorizer. + /// The desired key (can be string.Empty). + /// The result. + /// ------------------------------------------------------------------------------------ + internal QTokenProcessor(IChecksDataSource dataSource, + CharacterCategorizer charCategorizer, QuotationMarkCategorizer qmCategorizer, + string desiredKey, List results) + { + m_chkDataSource = dataSource; + m_charCategorizer = charCategorizer; + m_qmCategorizer = qmCategorizer; + m_desiredKey = desiredKey; + m_results = results; + m_verboseQuotes = (m_chkDataSource.GetParameterValue("VerboseQuotes") == "Yes"); + m_noCloserMsg = Localize("Unmatched opening mark: level {0}"); + m_noOpenerMsg = Localize("Unmatched closing mark: level {0}"); + m_regExQuotes = new Regex(qmCategorizer.Pattern); + + m_regExNonQuotes = new Regex(string.Format("[^{0}|\\s]", + qmCategorizer.Pattern.Replace("]", "\\]"))); // Make sure brackets are escaped + } + + /// ------------------------------------------------------------------------------------ + /// + /// Localizes the specified string. + /// + /// The string to localize. + /// The localized string + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_chkDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// If the token starts a typographic paragraph, store it as a paragraph-start token and + /// highlight (shows up on user interface) its text. Otherwise, if the token is + /// a quotation mark (either opening or closing, as defined by the quotation + /// categorizer), store it as a quotation mark token. + /// + /// The token being processed + /// ------------------------------------------------------------------------------------ + internal void ProcessToken(ITextToken tok, VerseTextToken verseTok) + { + if (tok == null) + throw new ArgumentNullException("tok"); + + Debug.Assert(!(tok is VerseTextToken)); + + if (tok.IsParagraphStart) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, 0, 0); + ParaStartToken pstok = new ParaStartToken(tts, tok.ParaStyleName); + m_quotationRelatedTokens.Add(pstok); + } + + AddTextToParaStartTokens(tok); + + // Find the first non whitespace, non quotation mark character in the token's + // text. This will be used in the following loop to determine what quotation + // marks precede all other characters in the token (i.e. what quotation marks + // begin the paragraph and are possible continuers). + Match match = m_regExNonQuotes.Match(tok.Text); + int iFirstNoneQMarkChar = (match.Success ? match.Index : -1); + + // Now find all the quotation marks in the token's text. + MatchCollection mc = m_regExQuotes.Matches(tok.Text); + + // Go through all the quotation marks found, creating quotation tokens + // for each. + foreach (Match m in mc) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, m.Index, m.Length); + + bool fIsParaStart = verseTok != null ? verseTok.IsParagraphStart : tok.IsParagraphStart; + bool fIsOpener = m_qmCategorizer.IsInitialPunctuation(tts.Text); + bool fPossibleContinuer = (m.Index < iFirstNoneQMarkChar && fIsParaStart); + QuotationMarkToken qmt = new QuotationMarkToken(tts, m_qmCategorizer, + fIsOpener, fPossibleContinuer); + m_quotationRelatedTokens.Add(qmt); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Finds paragraph-start tokens (REVIEW: I think there can only be one) in the list of + /// quotation-related substring tokens that are in the given token replace them (if + /// possible) with new paragraph-start tokens that include the first word or character + /// of the token so that there will be some text to highlight in the view when this + /// error is selected in the list being displayed + /// + /// The token being processed + /// ------------------------------------------------------------------------------------ + private void AddTextToParaStartTokens(ITextToken tok) + { + Debug.Assert(!(tok is VerseTextToken)); + + // We need to have some text in the token in order to highlight + if (tok.Text.Length == 0) + return; + + // Find something to highlight + int offset = 0; + int length = 0; + List words = m_charCategorizer.WordAndPuncts(tok.Text); + if (words.Count > 0) + { + offset = words[0].Offset; + length = Math.Max(words[0].Word.Length, words[0].Punct.Length); + } + + // If nothing is found, highlight the first character in the token + if (length == 0) + length = 1; + + for (int i = m_quotationRelatedTokens.Count - 1; i >= 0; i--) + { + ParaStartToken qrtok = m_quotationRelatedTokens[i] as ParaStartToken; + + if (qrtok != null) + { + if (qrtok.Tts.Text == string.Empty || + qrtok.Tts.FirstToken.TextType == TextType.VerseNumber || + qrtok.Tts.FirstToken.TextType == TextType.ChapterNumber) + { + // We have now found a paragraph-start token with no text to highlight + // in a list window. Update the text of the token with the new text + // that was found. + qrtok.Tts = new TextTokenSubstring(tok, offset, length); + } + break; // Don't change the text of earlier start tokens + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Goes through the list of quotation related tokens and generates errors for missing + /// continuers and quotations. + /// + /// ------------------------------------------------------------------------------------ + internal void FinalizeResult() + { + if (m_quotationRelatedTokens.Count == 0) + return; + + OpenQuotes openQuotes = new OpenQuotes(); + string prevStyleName = string.Empty; + for (int i = 0; i < m_quotationRelatedTokens.Count; i++) + { + QToken qrtok = m_quotationRelatedTokens[i]; + if (qrtok is QuotationMarkToken) + { + QuotationMarkToken qt = (QuotationMarkToken)qrtok; + CheckQuote(qt, openQuotes); + openQuotes.MostRecent = qt; + m_fFoundMissingContinuer = false; + } + else if (qrtok is ParaStartToken) + { + ParaStartToken pstok = qrtok as ParaStartToken; + List continuersExpected = + GetContinuersNeeded(pstok.StyleName, prevStyleName, openQuotes.Level); + + prevStyleName = pstok.StyleName; + + if (continuersExpected.Count > 0) + { + if (MatchContinuers(i, continuersExpected, openQuotes.Level)) + { + i += continuersExpected.Count; + } + else + { + int contLevel = GetExpectedContinuerLevel( + continuersExpected[continuersExpected.Count - 1], openQuotes.Level); + ReportError(pstok, string.Format(continuersExpected.Count == 1 ? + Localize("Missing continuation mark: level {0}") : + Localize("Missing continuation marks: levels 1-{0}"), + contLevel)); + + m_fFoundMissingContinuer = true; + } + } + } + } + + CheckForRemaining(openQuotes); + m_quotationRelatedTokens.Clear(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the expected continuer level. + /// + /// The continuer. + /// The current level. + /// + /// ------------------------------------------------------------------------------------ + private int GetExpectedContinuerLevel(string continuer, int currentLevel) + { + ParagraphContinuationType paraCont = m_qmCategorizer.ContinuationType; + switch (paraCont) + { + case ParagraphContinuationType.RequireOutermost: return 1; + case ParagraphContinuationType.RequireInnermost: return currentLevel; + case ParagraphContinuationType.RequireAll: + for (int i = 1; i <= currentLevel; i++) + { + if (m_qmCategorizer.GetContinuationMarkForLevel(i) == continuer) + return i; + } + break; + } + + return 0; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Processes paragraph-start tokens encountered and returns a list of the continuers + /// that should follow the marker, based on the marker, the currently open quotes, and + /// the quotation categorizer settings. + /// + /// The style being checked in the token for whether it should have + /// continuers + /// Name of the prev style. + /// The current level + /// + /// ------------------------------------------------------------------------------------ + private List GetContinuersNeeded(string styleName, string prevStyleName, + int level) + { + List continuers = new List(); + + // Check if the quotation categorizer is set to continue at this style + // or when it follows the previous style. + if (!m_qmCategorizer.CanStyleContinueQuotation(styleName, prevStyleName) || + level < 1) + { + return continuers; + } + + ParagraphContinuationType paraCont = m_qmCategorizer.ContinuationType; + + if (paraCont == ParagraphContinuationType.None) + return continuers; + + if (paraCont == ParagraphContinuationType.RequireOutermost) + continuers.Add(m_qmCategorizer.GetContinuationMarkForLevel(1)); + else if (paraCont == ParagraphContinuationType.RequireInnermost) + continuers.Add(m_qmCategorizer.GetContinuationMarkForLevel(level)); + else + { + for (int i = 1; i <= level; i++) + continuers.Add(m_qmCategorizer.GetContinuationMarkForLevel(i)); + } + + return continuers; + } + + /// ------------------------------------------------------------------------------------ + /// + /// This function is called each time a marker is encountered that should be followed by one + /// or more continuers. Returns true if the expected continuers are present, false otherwise. + /// + /// The index of the token being processed + /// A list of the continuers expected + /// The currect level + /// ------------------------------------------------------------------------------------ + private bool MatchContinuers(int qrToksIndex, List continuersNeeded, int currentLevel) + { + for (int i = 0; i < continuersNeeded.Count; i++) + { + if ((qrToksIndex + i + 1 >= m_quotationRelatedTokens.Count) || + (continuersNeeded[i] != m_quotationRelatedTokens[qrToksIndex + i + 1].Tts.Text)) + { + return false; + } + + QuotationMarkToken qmTok = + m_quotationRelatedTokens[qrToksIndex + i + 1] as QuotationMarkToken; + + + if (qmTok == null || !qmTok.PossibleContinuer) + return false; + + GenerateTraceMsg(qmTok, + string.Format(Localize("Level {0} quote continuer"), qmTok.PossibleLevel)); + } + + return true; + } + + /// ------------------------------------------------------------------------------------ + /// + /// This function is called after all the quotation related tokens have been processed. + /// It generates errors for quotes that are still open at this point. + /// + /// The currently open quotes + /// ------------------------------------------------------------------------------------ + private void CheckForRemaining(OpenQuotes current) + { + if (current.Level == 0) + return; + + // If the last quotation mark encountered was a closer, and this quotation mark system + // collapses adjacent quotes, assume it was multiple quotes collapsed into one + if (m_qmCategorizer.CollapseAdjacentQuotes && !current.MostRecent.IsOpener) + return; + + // Print errors starting with inner quotes + for (int i = current.Level; i > 1; i--) + { + if (current.Openers[i - 1] is QToken) + ReportError(current.Openers[i - 1] as QToken, string.Format(m_noCloserMsg, i)); + } + + // Prints error for the outermost quote + if (m_qmCategorizer.TopLevelClosingExists && current.Openers[0] is QToken) + ReportError(current.Openers[0] as QToken, string.Format(m_noCloserMsg, 1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Outputs (the text token substring of) the given quotation related token, attaching + /// the given error message to it. + /// + /// The token being processed + /// The error message to be displayed + /// ------------------------------------------------------------------------------------ + private void ReportError(QToken qrTok, string message) + { + Output(new TextTokenSubstring(qrTok.Tts, message)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// If verbose quotes are activated, outputs (the text token substring of) the given + /// quotation related token, attaching the given trace message to it. + /// + /// The token being processed + /// The trace message to be displayed + /// ------------------------------------------------------------------------------------ + private void GenerateTraceMsg(QToken qmTok, string message) + { + // Only output the trace message in verbose mode + if (m_verboseQuotes) + ReportError(qmTok, message); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Adds the text token substring of a quotation related token to the list of results. + /// At this point the message of the substring is either an error or trace message. + /// + /// The text token substring being processed + /// ------------------------------------------------------------------------------------ + private void Output(TextTokenSubstring tts) + { + if (m_desiredKey == string.Empty || m_desiredKey == tts.InventoryText) + m_results.Add(tts); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Given the information of the currently open quotation marks, process the next encountered + /// quotation mark, updating the information and generating errors where appropriate. + /// + /// The quotation mark token being processed + /// The currently open quotes + /// ------------------------------------------------------------------------------------ + private void CheckQuote(QuotationMarkToken qmtok, OpenQuotes openQuotes) + { + GenerateTraceMsg(qmtok, string.Format(qmtok.IsOpener ? + Localize("Level {0} quote opened") : Localize("Level {0} quote closed"), + (qmtok.IsOpener ? openQuotes.Level + 1 : openQuotes.Level))); + + if (m_qmCategorizer.IsMarkForLevel(qmtok.Tts.Text, openQuotes.Level + 1) && qmtok.IsOpener) + { + // The quote is opened properly + openQuotes.Level++; + openQuotes.Openers.Add(qmtok); + return; + } + else if (m_qmCategorizer.IsMarkForLevel(qmtok.Tts.Text, openQuotes.Level) && + (!qmtok.IsOpener || m_qmCategorizer.OpeningAndClosingAreIdentical(openQuotes.Level))) + { + // The quote is closed properly + openQuotes.Level--; + openQuotes.Openers.RemoveAt(openQuotes.Level); + return; + } + + int possibleQuoteMarkLevel = m_qmCategorizer.Level(qmtok.Tts.Text, + openQuotes.Level, qmtok.IsOpener); + + if (m_fFoundMissingContinuer) + { + //Debug.Assert(openQuotes.Level == openQuotes.Openers.Count); + Debug.Assert(possibleQuoteMarkLevel != 0); + + int newLevel = qmtok.IsOpener ? possibleQuoteMarkLevel : possibleQuoteMarkLevel - 1; + if (newLevel < openQuotes.Openers.Count) + { + while (openQuotes.Openers.Count > newLevel) + openQuotes.Openers.RemoveAt(openQuotes.Openers.Count - 1); + } + else if (newLevel > openQuotes.Openers.Count) + { + while (openQuotes.Openers.Count < newLevel) + openQuotes.Openers.Add("Missing Quote"); + } + openQuotes.Level = newLevel; + if (qmtok.IsOpener) + openQuotes.Openers.Add(qmtok); + else if (openQuotes.Openers.Count > 0 && openQuotes.Openers.Count > openQuotes.Level) + openQuotes.Openers.RemoveAt(openQuotes.Level); + return; + } + else if (!m_qmCategorizer.TopLevelClosingExists && possibleQuoteMarkLevel == 1 && + openQuotes.Level == 1) + { + // Opens a top-level quote when top-level closing quotes do not exist + openQuotes.Openers.RemoveAt(0); + openQuotes.Openers.Add(qmtok); + return; + } + else if (possibleQuoteMarkLevel > openQuotes.Level && !qmtok.IsOpener) + { + // The quote was closed, but was not opened + if (!m_qmCategorizer.CollapseAdjacentQuotes || openQuotes.MostRecent == null) + { + ReportError(qmtok, string.Format(Localize(m_noOpenerMsg), + possibleQuoteMarkLevel)); + } + return; + } + else if (possibleQuoteMarkLevel > openQuotes.Level + 1 && qmtok.IsOpener) + { + // The opener for the quote belongs to a quote level that is too high + ReportError(qmtok, string.Format( + Localize("Unexpected opening mark: level {0}"), + possibleQuoteMarkLevel)); + + // Add missing tokens for skipped levels + while (openQuotes.Openers.Count < possibleQuoteMarkLevel - 1) + openQuotes.Openers.Add("Missing Quote"); + + openQuotes.Level = possibleQuoteMarkLevel; + openQuotes.Openers.Add(qmtok); + return; + } + else if (possibleQuoteMarkLevel <= openQuotes.Level && qmtok.IsOpener) + { + // Opens a quote at the level already open or at too far out a level + for (int i = openQuotes.Level; i >= possibleQuoteMarkLevel; i--) + { + if (!(openQuotes.Openers[i - 1] is QToken)) + continue; + + ReportError(openQuotes.Openers[i - 1] as QToken, + string.Format(m_noCloserMsg, i)); + } + + openQuotes.Openers.RemoveRange(possibleQuoteMarkLevel - 1, + openQuotes.Level - possibleQuoteMarkLevel + 1); + openQuotes.Level = possibleQuoteMarkLevel; + openQuotes.Openers.Add(qmtok); + return; + } + + // A quote outside the current one is closed before the current one + for (int i = possibleQuoteMarkLevel; i < openQuotes.Level; i++) + { + if (!(openQuotes.Openers[i] is QToken)) + continue; + + ReportError(openQuotes.Openers[i] as QToken, + string.Format(m_noCloserMsg, i + 1)); + } + + openQuotes.Openers.RemoveRange(possibleQuoteMarkLevel - 1, openQuotes.Level - possibleQuoteMarkLevel); + openQuotes.Level = possibleQuoteMarkLevel - 1; + } + } + + #endregion + + #region QuotationMarkCategorizer class + /// ---------------------------------------------------------------------------------------- + /// + /// Quotation Mark Categorizer (don't ask!) + /// + /// ---------------------------------------------------------------------------------------- + internal class QuotationMarkCategorizer + { + private QuotationMarksList m_quoteMarks; + public bool CollapseAdjacentQuotes; + private StylePropsInfo m_styleInfo; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + internal QuotationMarkCategorizer(IChecksDataSource source) + { + m_quoteMarks = QuotationMarksList.Load(source.GetParameterValue("QuotationMarkInfo"), + source.GetParameterValue("DefaultWritingSystemName")); + m_styleInfo = StylePropsInfo.Load(source.GetParameterValue("StylesInfo")); + CollapseAdjacentQuotes = source.GetParameterValue("CollapseAdjacentQuotes") == "No"; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the paragraph continuation type. + /// + /// ------------------------------------------------------------------------------------ + internal ParagraphContinuationType ContinuationType + { + get { return m_quoteMarks.ContinuationType; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the paragraph continuation mark (i.e. opening or closing). + /// + /// ------------------------------------------------------------------------------------ + internal ParagraphContinuationMark ContinuationMark + { + get { return m_quoteMarks.ContinuationMark; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the paragraph continuation mark for the specified level. + /// + /// ------------------------------------------------------------------------------------ + internal string GetContinuationMarkForLevel(int level) + { + if (level > m_quoteMarks.Levels) + return null; + + return (m_quoteMarks.ContinuationMark == ParagraphContinuationMark.Opening ? + m_quoteMarks[level - 1].Opening : m_quoteMarks[level - 1].Closing); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the opening quotation mark for the specified level. + /// + /// ------------------------------------------------------------------------------------ + internal string GetOpenerForLevel(int level) + { + return (level > m_quoteMarks.Levels ? null : m_quoteMarks[level - 1].Opening); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the closing quotation mark for the specified level. + /// + /// ------------------------------------------------------------------------------------ + internal string GetCloserForLevel(int level) + { + return (level > m_quoteMarks.Levels ? null : m_quoteMarks[level - 1].Closing); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether the specified style name is legitimate for continuing + /// a quotation. + /// + /// ------------------------------------------------------------------------------------ + internal bool CanStyleContinueQuotation(string styleName, string prevStyleName) + { + if (m_styleInfo != null && m_styleInfo.SentenceInitial != null) + { + foreach (StyleInfo spi in m_styleInfo.SentenceInitial) + { + if (spi.StyleName == styleName) + { + if (spi.UseType == StyleInfo.UseTypes.prose) + return true; + + if (spi.UseType == StyleInfo.UseTypes.line && IsProseOrStanzaBreak(prevStyleName)) + return true; + } + } + } + + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether or not the specified style name represents a style whose use is + /// prose or stanzabreak. + /// + /// ------------------------------------------------------------------------------------ + private bool IsProseOrStanzaBreak(string styleName) + { + foreach (StyleInfo spi in m_styleInfo.SentenceInitial) + { + if (spi.StyleName == styleName && spi.UseType == StyleInfo.UseTypes.prose) + return true; + } + + foreach (StyleInfo spi in m_styleInfo.Special) + { + if (spi.StyleName == styleName && spi.UseType == StyleInfo.UseTypes.stanzabreak) + return true; + } + + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a value indicating whether the first level closing quotation does not exist. + /// + /// ------------------------------------------------------------------------------------ + internal bool TopLevelClosingExists + { + get { return m_quoteMarks.Levels >= 1 && !string.IsNullOrEmpty(m_quoteMarks[0].Closing); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns whether or not the specified level contains an opening and closing quote + /// mark that are identical. + /// + /// The level to check + /// True if the opening and closing quote marks are identical, false otherwise + /// + /// ------------------------------------------------------------------------------------ + internal bool OpeningAndClosingAreIdentical(int level) + { + if (level > m_quoteMarks.QMarksList.Count) + return false; // Just in case + + QuotationMarks qmark = m_quoteMarks.QMarksList[level - 1]; + return (qmark.Opening == qmark.Closing); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether the specified quotation mark is initial punctuation. + /// + /// ------------------------------------------------------------------------------------ + internal bool IsInitialPunctuation(string opening) + { + foreach (QuotationMarks qmark in m_quoteMarks.QMarksList) + { + if (opening == qmark.Opening) + return true; + } + + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether [is final punctuation] [the specified quotation mark]. + /// + /// The quotation mark. + /// ------------------------------------------------------------------------------------ + internal bool IsFinalPunctuation(string closing) + { + foreach (QuotationMarks qmark in m_quoteMarks.QMarksList) + { + if (closing == qmark.Closing) + return true; + } + + return false; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Determines whether the specified quotation mark is for the specified level. + /// + /// The quotation mark to check. + /// The level. + /// + /// true if the specified quotation mark is for the specified level; false otherwise. + /// + /// ------------------------------------------------------------------------------------ + internal bool IsMarkForLevel(string qmark, int level) + { + return Level(qmark, level) == level; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the level of the specified quotaion mark. + /// + /// The mark to get the level of + /// The level to start searching at + /// True to search forward from the starting level, false + /// to search backwards from the starting level + /// ------------------------------------------------------------------------------------ + internal int Level(string qmark, int startingLevel, bool fSearchForward) + { + if (startingLevel <= m_quoteMarks.Levels && startingLevel > 0) + { + int endLevel = (fSearchForward ? m_quoteMarks.Levels - 1 : 0); + for (int i = startingLevel - 1; i != endLevel; i += (fSearchForward ? 1 : -1)) + { + if (qmark == m_quoteMarks[i].Opening || qmark == m_quoteMarks[i].Closing) + return i + 1; + } + } + + return Level(qmark, 0); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the level of the specified quotaion mark. + /// + /// The mark to get the level of + /// The level that the mark is expected to have. This level + /// is checked first. + /// ------------------------------------------------------------------------------------ + internal int Level(string qmark, int expectedLevel) + { + if (expectedLevel > 0 && expectedLevel <= m_quoteMarks.Levels) + { + // Check the expected level first. If it matches the opening or the closing, then + // the expected level is considered to be the level of the mark. + if (m_quoteMarks[expectedLevel - 1].Opening == qmark || + m_quoteMarks[expectedLevel - 1].Closing == qmark) + return expectedLevel; + } + + for (int i = 0; i < m_quoteMarks.Levels; i++) + { + if (qmark == m_quoteMarks[i].Opening || qmark == m_quoteMarks[i].Closing) + return i + 1; + } + + return 0; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the pattern. + /// + /// ------------------------------------------------------------------------------------ + internal string Pattern + { + get + { + List quotationMarks = new List(); + + foreach (QuotationMarks qmark in m_quoteMarks.QMarksList) + { + if (!string.IsNullOrEmpty(qmark.Opening) && !quotationMarks.Contains(qmark.Opening)) + quotationMarks.Add(Regex.Escape(qmark.Opening)); + + if (!string.IsNullOrEmpty(qmark.Closing) && !quotationMarks.Contains(qmark.Closing)) + quotationMarks.Add(Regex.Escape(qmark.Closing)); + } + + return string.Join("|", quotationMarks.ToArray()); + } + } + } + + #endregion + + #region OpenQuotes class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + internal class OpenQuotes + { + /// + /// A list of the open quotes starting with the most deeply nested. + /// + internal ArrayList Openers = new ArrayList(); + + /// + /// The number of currently open quotes. + /// + internal int Level; + + /// + /// The most recently encountered quotation mark. + /// + internal QuotationMarkToken MostRecent; + } + + #endregion + + #region QToken class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + internal class QToken + { + internal TextTokenSubstring Tts; + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a that represents the current + /// . + /// + /// ------------------------------------------------------------------------------------ + public override string ToString() + { + if (Tts == null) + return ""; + + return string.Format("Text: {0}, Message: {1}", + string.IsNullOrEmpty(Tts.Text) ? "-" : Tts.Text, + string.IsNullOrEmpty(Tts.Message) ? "-" : Tts.Message); + } + } + + #endregion + + #region QuotationMarkToken class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + internal class QuotationMarkToken : QToken + { + private QuotationMarkCategorizer m_categorizer; + private bool m_fPossibleContinuer; + private bool m_fIsOpener; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + internal QuotationMarkToken(TextTokenSubstring tts, QuotationMarkCategorizer categorizer, + bool fIsOpener, bool fPossibleContinuer) + { + Tts = tts; + m_categorizer = categorizer; + m_fIsOpener = fIsOpener; + m_fPossibleContinuer = fPossibleContinuer; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a value indicating whether the quotation mark was found at the beginning of + /// a paragraph and is thus, a possible continuer. + /// + /// ------------------------------------------------------------------------------------ + internal bool PossibleContinuer + { + get { return m_fPossibleContinuer; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the possible level that this . + /// + /// ------------------------------------------------------------------------------------ + internal int PossibleLevel + { + get { return m_categorizer.Level(Tts.Text, 0); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a value indicating whether this instance is opener. + /// + /// ------------------------------------------------------------------------------------ + internal bool IsOpener + { + get { return m_fIsOpener; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public override string ToString() + { + return "Quote-" + Tts.Text + (IsOpener ? ", Opener" : ", Closer") + + (m_fPossibleContinuer ? ", PossibleContinuer" : string.Empty) + ", " + Tts.Message; + } + } + + #endregion + + #region ParaStartToken class + /// ---------------------------------------------------------------------------------------- + /// + /// + /// + /// ---------------------------------------------------------------------------------------- + internal class ParaStartToken : QToken + { + internal string StyleName; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + internal ParaStartToken(TextTokenSubstring tts, string styleName) + { + Tts = tts; + StyleName = styleName; + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + public override string ToString() + { + return "New paragraph-" + StyleName + ", " + Tts.ParagraphStyle + " " + Tts.Message; + } + } + + #endregion +} diff --git a/Lib/src/ScrChecks/RepeatedWordsCheck.cs b/Lib/src/ScrChecks/RepeatedWordsCheck.cs new file mode 100644 index 0000000000..db8ee2aae5 --- /dev/null +++ b/Lib/src/ScrChecks/RepeatedWordsCheck.cs @@ -0,0 +1,300 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using SIL.FieldWorks.Common.FwUtils; + +// NOT Paratext dependendent + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Check to detect repeated words + /// + /// ---------------------------------------------------------------------------------------- + public class RepeatedWordsCheck : IScrCheckInventory + { + IChecksDataSource m_checksDataSource; + CharacterCategorizer characterCategorizer; + + List m_repeatedWords; + List goodWords = new List(); + string validItems; + string invalidItems; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The checks data source. + /// ------------------------------------------------------------------------------------ + public RepeatedWordsCheck(IChecksDataSource checksDataSource) + { + m_checksDataSource = checksDataSource; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns a localized version of the specified string. + /// + /// ------------------------------------------------------------------------------------ + private string Localize(string strToLocalize) + { + return m_checksDataSource.GetLocalizedString(strToLocalize); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Maintain string containing validly repeatable words. + /// Also keep this a List in goodWords + /// + /// ------------------------------------------------------------------------------------ + public string ValidItems { + get { return validItems; } + set + { + validItems = value.Trim(); + goodWords = new List(); + if (validItems != "") + goodWords = new List(validItems.Split()); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// + /// + /// ------------------------------------------------------------------------------------ + public string InvalidItems + { + get { return invalidItems; } + set { invalidItems = value.Trim(); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Returns name of check for use in UI. + /// + /// ------------------------------------------------------------------------------------ + public string CheckName { get { return Localize("Repeated Words"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// The unique identifier of the check. This should never be changed! + /// + /// ------------------------------------------------------------------------------------ + public Guid CheckId { get { return StandardCheckIds.kguidRepeatedWords; } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the name of the group which contains this check. + /// + /// ------------------------------------------------------------------------------------ + public string CheckGroup { get { return Localize("Basic"); } } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// + /// ------------------------------------------------------------------------------------ + public float RelativeOrder + { + get { return 800; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the description for this check. + /// + /// ------------------------------------------------------------------------------------ + public string Description { get { return Localize("Checks for repeated words."); } } + + /// ------------------------------------------------------------------------------------ + /// + /// This is the column header of the first column when you create an + /// inventory of this type of error. + /// + /// ------------------------------------------------------------------------------------ + public string InventoryColumnHeader + { + get { return Localize("Words"); } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Update the parameter values for storing the valid and invalid lists in CheckDataSource + /// and then save them. This is here because the inventory form does not know the names of + /// the parameters that need to be saved for a given check, only the check knows this. + /// + /// ------------------------------------------------------------------------------------ + public void Save() + { + m_checksDataSource.SetParameterValue("RepeatableWords", validItems); + m_checksDataSource.SetParameterValue("NonRepeatableWords", invalidItems); + m_checksDataSource.Save(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Find all repeated words. Ignore any found in 'validItemsList'. Call RecordError + /// delegate whenever any other repeated key is found. + /// + /// ITextToken's corresponding to the text to be checked. + /// Typically this is one books worth. + /// Call this delegate to report each error found. + /// ------------------------------------------------------------------------------------ + public void Check(IEnumerable toks, RecordErrorHandler record) + { + // Find all repeated words. Put them in 'm_repeatedWords'. + GetReferences(toks, string.Empty); + + string msg = Localize("Repeated word"); + + foreach (TextTokenSubstring tts in m_repeatedWords) + { + if (!goodWords.Contains(tts.ToString())) + { + tts.Message = msg; + record(new RecordErrorEventArgs(tts, CheckId)); + } + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets a list if TextTokenSubstrings conataining the references and character offsets + /// where repeated words occur. + /// + /// The tokens (from the data source) to check for repeated words. + /// + /// If looking for occurrences of a specific repeated word, + /// set this to be that word; otherwise pass an empty string. + /// + /// ------------------------------------------------------------------------------------ + public List GetReferences(IEnumerable tokens, string desiredKey) + { +#if DEBUG + List AllTokens = new List(tokens); + if (AllTokens.Count == 0) + { + // Keep the compiler from complaining about assigning to a variable, but not using it. + } +#endif + characterCategorizer = m_checksDataSource.CharacterCategorizer; + // Get a string of words that may be validly repeated. + // Words are separated by blanks. + ValidItems = m_checksDataSource.GetParameterValue("RepeatableWords"); + // List of words that are known to be not repeatable. + InvalidItems = m_checksDataSource.GetParameterValue("NonRepeatableWords"); + + TextType prevTextType = TextType.Other; + m_repeatedWords = new List(); + ProcessRepeatedWords bodyProcessor = + new ProcessRepeatedWords(characterCategorizer, m_repeatedWords, desiredKey); + ProcessRepeatedWords noteProcessor = + new ProcessRepeatedWords(characterCategorizer, m_repeatedWords, desiredKey); + + foreach (ITextToken tok in tokens) + { + if (tok.IsParagraphStart) + { + noteProcessor.Reset(); + bodyProcessor.Reset(); + } + + if (tok.TextType == TextType.Note) + { + if (tok.IsNoteStart) + noteProcessor.Reset(); + noteProcessor.ProcessToken(tok); + } + + // When we leave a caption, we start over checking for repeated words. + // A caption is a start of a paragraph, so we already start over + // when we encounter a picture caption. + if (prevTextType == TextType.PictureCaption) + noteProcessor.Reset(); + + if (tok.TextType == TextType.Verse || tok.TextType == TextType.Other) + { + noteProcessor.Reset(); + bodyProcessor.ProcessToken(tok); + } + + if (tok.TextType == TextType.ChapterNumber) + bodyProcessor.Reset(); + + prevTextType = tok.TextType; + } + + return m_repeatedWords; + } + } + + class ProcessRepeatedWords + { + CharacterCategorizer characterCategorizer; + List result; + string desiredKey; + string prevWord = ""; + + public ProcessRepeatedWords(CharacterCategorizer characterCategorizer, + List result, string desiredKey) + { + this.characterCategorizer = characterCategorizer; + this.result = result; + this.desiredKey = desiredKey.ToLower(); + } + + public void ProcessToken(ITextToken tok) + { + foreach (WordAndPunct wap in characterCategorizer.WordAndPuncts(tok.Text)) + ProcessWord(tok, wap); + } + + private void ProcessWord(ITextToken tok, WordAndPunct wap) + { + if (wap.Word == "") + return; + + string nextWord = wap.Word.ToLower(); + + if (prevWord == nextWord) + AddWord(tok, wap); + + prevWord = nextWord; + + // If there are characters (such as quotes) between words, + // then two words are not considered repeating, even if they are identical + foreach (char cc in wap.Punct) + { + if (!char.IsWhiteSpace(cc)) + { + Reset(); + break; + } + } + } + + private void AddWord(ITextToken tok, WordAndPunct wap) + { + TextTokenSubstring tts = new TextTokenSubstring(tok, wap.Offset, wap.Word.Length); + if (desiredKey == "" || desiredKey == tts.InventoryText) + result.Add(tts); + } + + public void Reset() + { + prevWord = ""; + } + } +} diff --git a/Lib/src/ScrChecks/ScrChecks.csproj b/Lib/src/ScrChecks/ScrChecks.csproj new file mode 100644 index 0000000000..4901c48e56 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecks.csproj @@ -0,0 +1,41 @@ + + + + ScrChecks + SILUBS.ScriptureChecks + net48 + Library + true + 168,169,219,414,649,1635,1702,1701 + false + false + + + true + portable + false + DEBUG;TRACE + + + portable + true + TRACE + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckSilUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckSilUnitTest.cs new file mode 100644 index 0000000000..3d422079e4 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckSilUnitTest.cs @@ -0,0 +1,988 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using NUnit.Framework; +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Utils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// TE-style Unit tests for the CapitalizationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class CapitalizationCheckSilUnitTest : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + // A subset of serialized style information for seven different classes of styles + // that require capitalization: + // sentence intial styles, proper nouns, tables, lists, special, headings and titles. + string stylesInfo = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "<StyleInfo StyleName=\"Title_Main\" StyleType=\"paragraph\" />" + + "<StyleInfo StyleName=\"Title_Secondary\" StyleType=\"character\" />" + + "<StyleInfo StyleName=\"Title_Tertiary\" StyleType=\"character\" />" + + "
"; + + #region Initialization + /// ------------------------------------------------------------------------------------ + /// + /// Set up that happens before every test runs. + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_dataSource.SetParameterValue("StylesInfo", stylesInfo); + m_dataSource.SetParameterValue("SentenceFinalPunctuation", ".?!"); + m_check = new CapitalizationCheck(m_dataSource); + } + #endregion + + #region Tests + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// not capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_Uncapitalized() + { + m_dataSource.m_tokens.Add(new DummyTextToken("this is nice, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("And is this another nice sentence? ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Yes, this is nice.", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// not a capitalized letter with a diacritic. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedWithDiacritic_SeveralTokens() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "e\u0301 is small latin 'e' with acute in decomposed format, my friend! " + + "a\u0301 is an 'a' with the same. i\u0301 is an 'i' with the same.", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("o\u0303 is small latin 'o' with tilde, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u00FC is small latin 'u' with diaeresis, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(5)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "e\u0301", "Sentence should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[0].Text, 66, "a\u0301", "Sentence should begin with a capital letter"); + CheckError(2, m_dataSource.m_tokens[0].Text, 94, "i\u0301", "Sentence should begin with a capital letter"); + CheckError(3, m_dataSource.m_tokens[1].Text, 0, "o\u0303", "Sentence should begin with a capital letter"); + CheckError(4, m_dataSource.m_tokens[2].Text, 0, "\u00FC", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a list paragraph is + /// not a capitalized letter with a diacritic. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedWithDiacritic_SeveralTokensInNotes() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "e\u0301 is small latin 'e' with acute in decomposed format, my friend! " + + "a\u0301 is an 'a' with the same. i\u0301 is an 'i' with the same.", + TextType.Verse, true, true, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("o\u0303 is small latin 'o' with tilde, my friend! ", + TextType.Verse, true, true, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u00FC is small latin 'u' with diaeresis, my friend! ", + TextType.Verse, true, false, "Line1")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(5)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "e\u0301", "Sentence should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[0].Text, 66, "a\u0301", "Sentence should begin with a capital letter"); + CheckError(2, m_dataSource.m_tokens[0].Text, 94, "i\u0301", "Sentence should begin with a capital letter"); + CheckError(3, m_dataSource.m_tokens[1].Text, 0, "o\u0303", "Sentence should begin with a capital letter"); + CheckError(4, m_dataSource.m_tokens[2].Text, 0, "\u00FC", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// not capitalized letter with a diacritic that is preceeded by quotes. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedWithDiacritic_QuotesBefore() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "\"e\u0301 is small latin 'e' with acute in decomposed format, my friend! ", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 1, "e\u0301", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// not capitalized letter with multiple diacritics. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedWithMultipleDiacritics() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "u\u0301\u0302\u0327 is small latin 'u' with circumflex, acute accent and cedilla. ", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "u\u0301\u0302\u0327", + "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// uncapitalized letter with a diacritic made of two decomposed characters. TE-6862 + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedDecomposedLetter() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "\u0061\u0301 is small latin a with a combining acute accent, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "\u0061\u0301", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// a non-Roman character in a writing system that does not use capitalization. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithNoCaseNonRoman() + { + m_dataSource.m_tokens.Add(new DummyTextToken("\u0E01 is the Thai letter Ko Kai.", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// a no case PUA. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithNoCasePUA() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "Character in next sentence is no case PUA character. \uEE00", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is a + /// latin capital letter D with tsmall letter z with caron. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLatinExtendedCap() + { + m_dataSource.m_tokens.Add(new DummyTextToken("\u01C5 is a latin extended capital.", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a chapter + /// number followed by verse number followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterChapterVerse() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a verse + /// followed by lowercase letter + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterVerse() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a chapter + /// number followed by lowercase letter (verse number one is implied). + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterChapter() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a chapter + /// number, verse number and footnote marker followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterChapterVerseAndNote() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + false, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a verse number + /// and footnote marker followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterVerseAndNote() + { + // Check when the footnote marker run is considered a run that starts a paragraph. + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + // Check when the footnote marker run is not considered + // a run that starts a paragraph. + m_errors.Clear(); + m_dataSource.m_tokens.Clear(); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + false, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a chapter number + /// and footnote marker followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterChapterAndNote() + { + // Check when the footnote marker run is considered a run that starts a paragraph. + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + // Check when the footnote marker run is not considered + // a run that starts a paragraph. + m_errors.Clear(); + m_dataSource.m_tokens.Clear(); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + false, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins a footnote marker + /// followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterNote() + { + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", TextType.Note, + true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check one footnote ends with a period and the next + /// footnote begins with lower case. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Footnotes_TreatedSeparately() + { + m_dataSource.m_tokens.Add(new DummyTextToken("This is footnote one.", TextType.Note, + true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("footnote two", TextType.Note, + true, false, "Note General Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a picture ORC + /// followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterPicture() + { + m_dataSource.m_tokens.Add(new DummyTextToken("Picture Caption", TextType.PictureCaption, + true, false, "Caption")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the paragraph begins with a verse number + /// and a picture ORC followed by lowercase letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_StartsWithLCaseAfterVersePicture() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Picture Caption", TextType.PictureCaption, + true, false, "Caption")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one", TextType.Verse, + false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void LCaseInRunAfterNote() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("This is before a footnote marker", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Footnote Text", + TextType.Note, true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("this is after a footnote marker", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void LCaseInRunAfterPicture() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("This is before a picture", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("My picture caption", + TextType.PictureCaption, true, true, "Caption")); + m_dataSource.m_tokens.Add(new DummyTextToken("this is after the picture", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void LCaseInRunAfterVerse() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("This is before a verse", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, true, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("this is after a verse", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test that the check catches the case where a verse starts with a lowercase letter + /// when the preceding verse ended with sentence-end punctuation. TE-8050 + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void LCaseInRunAfterSentenceEndPunctAndVerse() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("This is verse one.", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, true, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("this is verse two.", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[3].Text, 0, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// not capitalized and para begins with quotes. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_UncapitalizedWithQuotes() + { + //201C = Left double quotation mark + //2018 = Left single quotation mark + m_dataSource.m_tokens.Add(new DummyTextToken( + "\u201C \u2018this is an uncaptialized para with quotes, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 3, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first word-forming character is ' : 'tis so. + /// The ' is the first character of the sentence and the 't' should be capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Sentence_UncapitalizedWithApostrophe() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "Yes! 'tis an uncaptialized sentence with apostrophe before the first lowercase letter!", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 6, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first word-forming character is ' : 'Tis so. + /// The ' is the first character of the sentence and the 'T' is capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Sentence_CapitalizedWithApostrophe() + { + m_dataSource.m_tokens.Add(new DummyTextToken( + "Yes! 'Tis an uncaptialized sentence with apostrophe before the first lowercase letter!", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// capitalized and para begins with quotes. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Paragraph_CapitalizedWithQuotes() + { + //201C = Left double quotation mark + //2018 = Left single quotation mark + m_dataSource.m_tokens.Add(new DummyTextToken( + "\u201C \u2018This is an uncaptialized para with quotes, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// name is capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedProperName_ParaStart() + { + m_dataSource.m_tokens.Add(new DummyTextToken("This", + TextType.Verse, true, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken( + " is a proper name, my friend! ", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// name is uncapitalized at the start of a paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedProperName_ParaStart() + { + m_dataSource.m_tokens.Add(new DummyTextToken("this", + TextType.Verse, true, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken( + " is a proper name, my friend! ", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + // This word should be capitalized for two reasons: it occurs sentence initially and it + // is a proper noun. + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// name is uncapitalized at the start of the paragraph that does not have to be + /// capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedProperName_ParaStart2() + { + m_dataSource.m_tokens.Add(new DummyTextToken("this", + TextType.Verse, true, false, "UncapitalizedParaStyle", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken( + " is a proper name, my friend! ", + TextType.Verse, false, false, "UncapitalizedParaStyle")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + // This word should be capitalized for two reasons: it occurs sentence initially and it + // is a proper noun. + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Proper nouns should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// name is uncapitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedProperName_NotParaStart() + { + m_dataSource.m_tokens.Add(new DummyTextToken("The ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Lord", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken(" is ", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("God!", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// names are uncapitalized when they are not at the start of the paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedProperName_NotParaStart() + { + m_dataSource.m_tokens.Add(new DummyTextToken("The ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("lord", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken(" is ", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("god!", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[1].Text, 0, "l", "Proper nouns should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[3].Text, 0, "g", "Proper nouns should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// names are uncapitalized when they are not at the start of the paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedParagraph_WithCapProperName() + { + m_dataSource.m_tokens.Add(new DummyTextToken("the ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Lord", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken(" is ", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("God!", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the proper + /// names are uncapitalized when they are not at the start of the paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedParaStartAndProperName() + { + m_dataSource.m_tokens.Add(new DummyTextToken("the ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("lord", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_dataSource.m_tokens.Add(new DummyTextToken(" is ", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("god!", + TextType.Verse, false, false, "Paragraph", "Name Of God")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Sentence should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[1].Text, 0, "l", "Proper nouns should begin with a capital letter"); + CheckError(2, m_dataSource.m_tokens[3].Text, 0, "g", "Proper nouns should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// uncapitalized. A non-initial sentence is also not capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedPara_WithEmbeddedUncapitalizedSentence() + { + m_dataSource.m_tokens.Add(new DummyTextToken("this sentence isn't capitalized. " + + "this one isn't either.", TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Sentence should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[0].Text, 33, "t", "Sentence should begin with a capital letter"); + } + + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of the paragraph is + /// uncapitalized. Followed by an uncapitalized sentence containing the words of Christ, + /// i.e. it should be capitalized for two reasons but we want only one error report + /// from this uncapitalized letter. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedPara_WithEmbeddedWordsOfChrist() + { + m_dataSource.m_tokens.Add(new DummyTextToken("and the Lord said! ", TextType.Verse, + true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\"if you love me, you will obey my commands.\"", TextType.Verse, + false, false, "Paragraph", "Words Of Christ")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "a", "Sentence should begin with a capital letter"); + CheckError(1, m_dataSource.m_tokens[1].Text, 1, "i", "Sentence should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a heading is + /// capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedHeading() + { + m_dataSource.m_tokens.Add(new DummyTextToken("The title of this section", + TextType.Other, true, false, "Section Head")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a heading is not + /// uncapitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedHeading() + { + m_dataSource.m_tokens.Add(new DummyTextToken("the title of this section", + TextType.Other, true, false, "Section Head")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Heading should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a title is + /// capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedTitle() + { + m_dataSource.m_tokens.Add(new DummyTextToken("The title of this book", + TextType.Other, true, false, "Title Main")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a title is not + /// uncapitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedTitle() + { + m_dataSource.m_tokens.Add(new DummyTextToken("the title of this book", + TextType.Other, true, false, "Title Main")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "t", "Title should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a list item is + /// capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedList() + { + m_dataSource.m_tokens.Add(new DummyTextToken("An item in a list", + TextType.Other, true, false, "List Item1")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a list item is not + /// uncapitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedList() + { + m_dataSource.m_tokens.Add(new DummyTextToken("an item in a list", + TextType.Other, true, false, "List Item1")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "a", "List paragraphs should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a table entry is + /// capitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void CapitalizedTableCellHead() + { + m_dataSource.m_tokens.Add(new DummyTextToken("An entry in a table", + TextType.Other, true, false, "Table Cell Head")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Uncapitalized styles check when the first letter of a table entry is not + /// uncapitalized. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UncapitalizedTableCellHead() + { + m_dataSource.m_tokens.Add(new DummyTextToken("an item in a list", + TextType.Other, true, false, "Table Cell Head")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "a", "Table contents should begin with a capital letter"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests getting the length of a character (including diacritics) from a specified offset. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void GetLengthOfChar() + { + CapitalizationProcessor processor = new CapitalizationProcessor(m_dataSource, null); + + Assert.That(ReflectionHelper.GetIntResult(processor, "GetLengthOfChar", + new DummyTextToken("a has no diacritics.", TextType.Verse, true, false, + "Paragraph"), 0), Is.EqualTo(1)); + Assert.That(ReflectionHelper.GetIntResult(processor, "GetLengthOfChar", + new DummyTextToken("a\u0303 has a tilde.", TextType.Verse, true, false, + "Paragraph"), 0), Is.EqualTo(2)); + Assert.That(ReflectionHelper.GetIntResult(processor, "GetLengthOfChar", + new DummyTextToken("a\u0303\u0301 has a tilde and grave accent.", + TextType.Verse, true, false, "Paragraph"), 0), Is.EqualTo(3)); + Assert.That(ReflectionHelper.GetIntResult(processor, "GetLengthOfChar", + new DummyTextToken("a\u0303\u0301\u0302 has a tilde, grave accent and circumflex accent.", + TextType.Verse, true, false, "Paragraph"), 0), Is.EqualTo(4)); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckUnitTest.cs new file mode 100644 index 0000000000..1dc765683d --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/CapitalizationCheckUnitTest.cs @@ -0,0 +1,250 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// USFM-style unit tests for the CapitalizationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class CapitalizationCheckUnitTest + { + UnitTestChecksDataSource source = new UnitTestChecksDataSource(); + + // A subset of serialized style information for seven different classes of styles + // that require capitalization: + // sentence intial styles, proper nouns, tables, lists, special, headings and titles. + string stylesInfo = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "<StyleInfo StyleName=\"imt\" StyleType=\"paragraph\" />" + + "<StyleInfo StyleName=\"imt2\" StyleType=\"character\" />" + + "<StyleInfo StyleName=\"imt3\" StyleType=\"character\" />" + + "
"; + + void Test(string[] result, string text) + { + source.Text = text; + + source.SetParameterValue("StylesInfo", stylesInfo); + source.SetParameterValue("SentenceFinalPunctuation", ".!?"); + CapitalizationCheck check = new CapitalizationCheck(source); + List tts = + check.GetReferences(source.TextTokens()); + + Assert.That(tts.Count, Is.EqualTo(result.Length), "A different number of results was returned from what was expected."); + + for (int i = 0; i < result.Length; i++) + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i]), "Result number: " + i); + } + + #region Test capitalization of styles + [Test] + public void ParagraphCapitalized() + { + Test(new string[] { }, @"\p \v 1 The earth"); + } + + [Test] + public void ParagraphNoCaseNonRoman() + { + Test(new string[] { }, "\\p \\v 1 \u0E01"); + } + + [Test] + public void ParagraphNoCasePUA() + { + Test(new string[] { }, "\\p \\v 1 \uEE00"); + } + + [Test] + public void ParagraphTitleCase() + { + Test(new string[] { }, "\\p \\v 1 \u01C5"); + } + + [Test] + public void ParagraphUnCapitalized() + { + Test(new string[] { "p" }, @"\p \v 1 the earth"); + } + + [Test] + public void ParagraphUnCapitalizedWithQuotes() + { + Test(new string[] { "p" }, "\\p \\v 1 \u201C \u2018the earth"); + } + + [Test] + public void ParagraphCapitalizedWithQuotes() + { + Test(new string[] { }, "\\p \\v 1 \u201C \u2018The earth"); + } + + [Test] + public void Capitalized() + { + Test(new string[] { }, @"\p \v 1 \nd Lord\nd* in"); + } + + [Test] + public void UnCapitalized() + { + // test used to be { "p", "nd" } - was changed to reflect that we didn't want duplicated + // results. + Test(new string[] { "p" }, @"\p \v 1 \nd lord\nd* in"); + } + + [Test] + public void AllCapitalized() + { + Test(new string[] { }, @"\p \v 1 The \nd Lord\nd* in"); + } + + [Test] + public void ParagraphCapitalizedCharacterUnCapitalized() + { + Test(new string[] { "nd" }, @"\p \v 1 The \nd lord\nd* in"); + } + + [Test] + public void ParagraphUnCapitalizedCharacterCapitalized() + { + Test(new string[] { "p" }, @"\p \v 1 the \nd Lord\nd* in"); + } + + [Test] + public void AllUnCapitalized() + { + Test(new string[] { "p", "nd" }, @"\p \v 1 the \nd lord\nd* in"); + } + #endregion + + #region Test capitalization after sentence-final punctuation + [Test] + public void UpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo. Bar"); + } + + [Test] + public void LowerCase() + { + Test(new string[] { "b" }, @"\p \v 1 Foo. bar"); + } + + [Test] + public void NoCaseNonRoman() + { + Test(new string[] { }, "\\p \\v 1 Foo. \u0E01"); + } + + [Test] + public void NoCasePUA() + { + Test(new string[] { }, "\\p \\v 1 Foo. \uEE00"); + } + + [Test] + public void TitleCase() + { + Test(new string[] { }, "\\p \\v 1 Foo. \u01C5"); + } + + [Test] + public void MultipleUpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo. Bar! Baz"); + } + + [Test] + public void MultipleLowerCase() + { + Test(new string[] { "b", "b" }, @"\p \v 1 Foo. bar! baz"); + } + + [Test] + public void MultipleMixedCase() + { + Test(new string[] { "b" }, @"\p \v 1 Foo. Bar! baz"); + } + + [Test] + public void MultiplePunctUpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo!? Bar"); + } + + [Test] + public void MultiplePunctLowerCase() + { + Test(new string[] { "b" }, @"\p \v 1 Foo!? bar"); + } + + [Test] + public void Quotes() + { + Test(new string[] { "b" }, "\\p \\v 1 \u201CFoo!\u201D bar"); + } + + [Test] + public void Digits() + { + Test(new string[] { }, @"\p \v 1 Foo 1.2 bar"); + } + + [Test] + public void AbbreviationError() + { + Test(new string[] { "h" }, @"\p \v 1 The E.U. headquarters."); + } + + [Test] + public void AbbreviationOK() + { + source.SetParameterValue("Abbreviations", "E.U."); + Test(new string[] { }, @"\p \v 1 The E.U. headquarters."); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/ChapterVerseTests.cs b/Lib/src/ScrChecks/ScrChecksTests/ChapterVerseTests.cs new file mode 100644 index 0000000000..61a31ee4ef --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/ChapterVerseTests.cs @@ -0,0 +1,2138 @@ + // --------------------------------------------------------------------------------------------- +// Copyright (c) 2008-2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) +// +// File: ChapterVerseTests.cs +// Responsibility: TE Team +// --------------------------------------------------------------------------------------------- +using System; +using NUnit.Framework; +using System.Reflection; +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Core.Scripture; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// TeCheckingToolTests class, runs tests on ChapterVerseCheck.cs + /// Tests are done by creating a version of Haggai with chapter or verse problems in the + /// text and then verifying the the problems are correctly reported by the checking code. + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class ChapterVerseTests : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes the versification tables. + /// + /// ------------------------------------------------------------------------------------ + [OneTimeSetUp] + public void FixtureSetup() + { + BCVRefTests.InitializeVersificationTable(); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Set up that happens before every test runs. + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_check = new ChapterVerseCheck(m_dataSource, RecordError); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the instance of the Chapter-Verse check that we're testing. + /// + /// ------------------------------------------------------------------------------------ + private ChapterVerseCheck Check + { + get { return m_check as ChapterVerseCheck; } + } + + // Future test cases to account for: + // Allowing an app to regard missing chapter 1 and/or missing verse 1 as an error + // errors in verse bridges: 6-5, 5- ,... worry about it? 5- returns 5 right now + // duplicate chapter error? + // case: 7a-8, ok for now, will expect another verse 8, get 1 missing verse error, user then able to find problem + // it is just not optimized to tell exactly that "part b" is missing + // Unique Case: 2:1515 returns out of range error if located following 14 where verse 15 + // would normally occur and this error could occur. But if a verse number out of range is found elsewhere + // it logs an out of range error and a duplicate error, which it should not log a duplicate error + // Overlapping verse bridges: 1-5 3-8 + // + // Special test case examples that are done, accounted for: + // Case: 1 2 6 multiple missing verses + // Case: verse # > last verse for that chapter (out of range) + // Case: no chapter 1 (not error, chapter 1 is optional) + // Case: chapter # > chapter total for the book + // Case: 1 2-3 5 allow bridges + // still catches last verse out of range, if bridge out of range + // Case: what about just missing then out of order 1 2 4 5 3 . Look back through errorList + // Case: verse bridge: 1 2 3-45 6 7 or 1 2 3-8 6 7 , treats same as repeated verse numbers with an out of range error + // Case: missing verse at end of chapter + // Case: when expected verse reaches last verse, say 26 and then hit verse 27, now assumes + // all verses before are done and sets next expected verse to 1 for next chapter + // Case: 19 20 chap1 1 2 where verse total should have reached 21 or mor, catch missing verses of previous chap + // Case: 1 2 45 3 vs. 1 2 45 1 (45 out of range) + // Case: 2:1515 returns out of range error when at verse 15 where error would most likely occur + // Case: 19a 19b 20 allow verse parts, correctly does not allow verse part c + // Case: 10a at end of chapter, still catches missing 10b verse, logs missing verse error + // Case: 1 2 7 3 4 5 6 8 out of order and missing + // Case: 1 2 7 3 4 5 6 7 out of order and repeat + // Case: Missing chapter number following a verse bridge + // Note: 1 2 3 5 6 7... 15 3-4 16 ?? throws a duplicate. If bridge, 3-4 go together + + /// ----------------------------------------------------------------------------------- + /// + /// Test the AnyOverlappingVerses method with only one verse in both ranges. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void OverlappingSingleVerse1() + { + object[] retVerses; + Assert.That(Check.AnyOverlappingVerses(5, 5, 5, 5, out retVerses), Is.True); + Assert.That(retVerses.Length, Is.EqualTo(1)); + Assert.That(retVerses[0], Is.EqualTo(5)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the AnyOverlappingVerses method with one verse in one range and a real + /// range in the other. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void OverlappingSingleVerse2() + { + object[] retVerses; + Assert.That(Check.AnyOverlappingVerses(6, 6, 5, 8, out retVerses), Is.True); + Assert.That(retVerses.Length, Is.EqualTo(1)); + Assert.That(retVerses[0], Is.EqualTo(6)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the AnyOverlappingVerses method with one verse in one range and a real + /// range in the other. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void OverlappingSingleVerse3() + { + object[] retVerses; + Assert.That(Check.AnyOverlappingVerses(5, 8, 6, 6, out retVerses), Is.True); + Assert.That(retVerses.Length, Is.EqualTo(1)); + Assert.That(retVerses[0], Is.EqualTo(6)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the AnyOverlappingVerses method with many verses in one range and many in + /// the other. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void OverlappingVerseRange1() + { + object[] retVerses; + Assert.That(Check.AnyOverlappingVerses(5, 8, 3, 6, out retVerses), Is.True); + Assert.That(retVerses.Length, Is.EqualTo(2)); + Assert.That(retVerses[0], Is.EqualTo(5)); + Assert.That(retVerses[1], Is.EqualTo(6)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the AnyOverlappingVerses method with many verses in one range and many in + /// the other. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void OverlappingVerseRange2() + { + object[] retVerses; + Assert.That(Check.AnyOverlappingVerses(5, 20, 10, 100, out retVerses), Is.True); + Assert.That(retVerses.Length, Is.EqualTo(2)); + Assert.That(retVerses[0], Is.EqualTo(10)); + Assert.That(retVerses[1], Is.EqualTo(20)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the CheckForMissingVerses method to make sure detects single + /// (i.e. non ranges) missing verses. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void CheckForMissingVerses_Singles() + { + ITextToken[] versesFound = new ITextToken[7] { + new DummyTextToken("0"), null, new DummyTextToken("2"), + new DummyTextToken("003"), null, new DummyTextToken("05"), null }; + + object[] args = new object[] { versesFound, 2, 5 }; + + BindingFlags flags = BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.InvokeMethod; + + typeof(ChapterVerseCheck).InvokeMember("CheckForMissingVerses", + flags, null, m_check, args); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, versesFound[0].Text, 1, String.Empty, "Missing verse number 1"); + Assert.That(m_errors[0].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005001))); + Assert.That(m_errors[0].Tts.MissingEndRef, Is.EqualTo(null)); + + CheckError(1, versesFound[3].Text, 3, String.Empty, "Missing verse number 4"); + Assert.That(m_errors[1].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005004))); + Assert.That(m_errors[1].Tts.MissingEndRef, Is.EqualTo(null)); + + CheckError(2, versesFound[5].Text, 2, String.Empty, "Missing verse number 6"); + Assert.That(m_errors[2].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005006))); + Assert.That(m_errors[2].Tts.MissingEndRef, Is.EqualTo(null)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test the CheckForMissingVerses method to make sure detects ranges of missing + /// verses. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void CheckForMissingVerses_Ranges() + { + ITextToken[] versesFound = new ITextToken[12] { + new DummyTextToken("0"), null, null, + new DummyTextToken("003"), null, null, null, + new DummyTextToken("7"), new DummyTextToken("8"), + new DummyTextToken("09"), null, null }; + + object[] args = new object[] { versesFound, 2, 5 }; + + BindingFlags flags = BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.InvokeMethod; + + typeof(ChapterVerseCheck).InvokeMember("CheckForMissingVerses", + flags, null, m_check, args); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, versesFound[0].Text, 1, String.Empty, "Missing verse numbers 1-2"); + Assert.That(m_errors[0].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005001))); + Assert.That(m_errors[0].Tts.MissingEndRef, Is.EqualTo(new BCVRef(2005002))); + + CheckError(1, versesFound[3].Text, 3, String.Empty, "Missing verse numbers 4-6"); + Assert.That(m_errors[1].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005004))); + Assert.That(m_errors[1].Tts.MissingEndRef, Is.EqualTo(new BCVRef(2005006))); + + CheckError(2, versesFound[9].Text, 2, String.Empty, "Missing verse numbers 10-11"); + Assert.That(m_errors[2].Tts.MissingStartRef, Is.EqualTo(new BCVRef(2005010))); + Assert.That(m_errors[2].Tts.MissingEndRef, Is.EqualTo(new BCVRef(2005011))); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find no chapter or verse errors + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void NoChapterVerseErrors() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find no chapter or verse errors when script digits are used, + /// selected Arabic-Indic for this test. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void NoChapterVerseErrors_ScriptDigits() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + m_dataSource.SetParameterValue("Script Digit Zero", "\u0660"); + m_dataSource.SetParameterValue("Verse Bridge", "\u200F-\u200f"); + + // \u0660-\u0669 are Arabic-Indic digits, \u200F is the RTL Mark. + m_dataSource.m_tokens.Add(new DummyTextToken("\u0661", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0661", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0662", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0663\u200F-\u200f\u0661\u0665", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0662", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0661\u200F-\u200f\u0662\u0663", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find an error when script digits are used but not expected. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void FormatErrors_UnexpectedScriptDigits() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + m_dataSource.SetParameterValue("Script Digit Zero", "0"); + m_dataSource.SetParameterValue("Verse Bridge", "\u200F-\u200f"); + + // \u0660-\u0669 are Arabic-Indic digits, \u200F is the RTL Mark. + DummyTextToken badToken1 = new DummyTextToken("\u0661", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken1); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3\u200F-\u200f15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + DummyTextToken badToken2 = new DummyTextToken("1\u200f-\u200f2\u0663", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, badToken1.Text, 0, badToken1.Text, "Invalid chapter number"); + CheckError(1, badToken2.Text, 0, badToken2.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find an error when script digits are expected but not used. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void FormatErrors_ExpectedScriptDigits() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + m_dataSource.SetParameterValue("Script Digit Zero", "\u0660"); + m_dataSource.SetParameterValue("Verse Bridge", "\u200F-\u200f"); + + // \u0660-\u0669 are Arabic-Indic digits, \u200F is the RTL Mark. + DummyTextToken badToken1 = new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken1); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0661", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0662", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0663\u200F-\u200f\u0661\u0665", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u0662", + TextType.ChapterNumber, false, false, "Paragraph")); + DummyTextToken badToken2 = new DummyTextToken("\u0661\u200F-\u200f\u06623", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, badToken1.Text, 0, badToken1.Text, "Invalid chapter number"); + CheckError(1, badToken2.Text, 0, badToken2.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that formatting error is reported when verse bridge contains unexpected + /// right-to-left marks. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void FormatErrors_UnexpectedRtoLMarksInVerseBridge() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "JUD"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + m_dataSource.SetParameterValue("Verse Bridge", "-"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + DummyTextToken badToken = new DummyTextToken("3\u200f-\u200f25", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, badToken.Text, 0, badToken.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that formatting error is reported when verse number contains a medial letter. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void FormatErrors_UnexpectedLetterInVerseNumber() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "JUD"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-24", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + DummyTextToken badToken = new DummyTextToken("2a5", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, badToken.Text, 0, badToken.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that formatting error is reported when wrong verse bridge character is used. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void FormatErrors_UnexpectedBridgeCharacter() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "JUD"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + m_dataSource.SetParameterValue("Verse Bridge", "~"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + DummyTextToken badToken = new DummyTextToken("1-25", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(badToken); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, badToken.Text, 0, badToken.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test use of versification info + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void NoChapterVerseErrors_DifferentVersifications() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "NAM"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("1-15", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("1-13", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-19", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + + m_dataSource.SetParameterValue("Versification Scheme", "Septuagint"); + + ((DummyTextToken)TempTok).Text = "1-14"; + ((DummyTextToken)TempTok2).Text = "1-14"; + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find no chapter error when there is no Chapter 1. + /// Chapter number 1 is optional. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void NoErrorWhenMissingChapterOne() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // Missing chapter number 1 + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a chapter error when there is a Chapter 0. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterZeroError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("0", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, m_dataSource.m_tokens[0].Text, "Invalid chapter number"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 1"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find errors when Chapter and verse numbers start with a leading 0. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void LeadingZeroErrors() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("01", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("002", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, m_dataSource.m_tokens[0].Text, "Invalid chapter number"); + CheckError(1, m_dataSource.m_tokens[3].Text, 0, m_dataSource.m_tokens[3].Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find no chapter or verse errors when checking only a single + /// chapter + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void NoChapterVerseErrors_CheckingSingleChapter() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "1"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter number error + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberMissingError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing chapter number 2 + ITextToken TempTok = new DummyTextToken("1-23", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, TempTok.Text, 0, TempTok.Text, "Duplicate verse numbers"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter number error following a verse bridge + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberMissingError_FollowingVerseBridge() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing chapter number 2 + ITextToken TempTok = new DummyTextToken("1-23", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, TempTok.Text, 0, TempTok.Text, "Duplicate verse numbers"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter number error when missing final chapters + /// with no data after the missing chapter number + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberMissingFinalError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing entire chapter 2 + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter number error when previous chapter has + /// no verses + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberMissingNoVerses() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // error sequence - chapter 1 with verse, chapter 2 no verse, chapter 3 with verse + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(1, "1", 1, String.Empty, "Missing chapter number 2"); + Assert.That(m_errors[1].Tts.MissingStartRef.BBCCCVVV, Is.EqualTo(2000)); + Assert.That(m_errors[1].Tts.MissingEndRef, Is.Null); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a duplicated chapter two + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberDuplicated() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // error sequence - chapter 1 & 2 with verse, chapter 2 duplicated + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-15", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + DummyTextToken dupChapter = new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(dupChapter); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, dupChapter.Text, 0, dupChapter.Text, "Duplicate chapter number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter one + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberOneMissing() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // error sequence - chapter 1 skipped, chapter 2 & 3 fully present + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, "2", 0, String.Empty, "Missing chapter number 1"); + Assert.That(m_errors[0].Tts.MissingStartRef.BBCCCVVV, Is.EqualTo(1000)); + Assert.That(m_errors[0].Tts.MissingEndRef, Is.Null); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing verse number error + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumberMissingError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verse 2 + ITextToken TempTok = new DummyTextToken("3-14", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verse 15 + ITextToken TempTok2 = new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + // Missing verse 1 + ITextToken TempTok3 = new DummyTextToken("2-22", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok3); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verse 23 + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(4)); + CheckError(0, m_dataSource.m_tokens[1].Text, 1, String.Empty, "Missing verse number 2"); + CheckError(1, TempTok.Text, 4, String.Empty, "Missing verse number 15"); + CheckError(2, TempTok2.Text, 1, String.Empty, "Missing verse number 1"); + CheckError(3, TempTok3.Text, 4, String.Empty, "Missing verse number 23"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a chapter number outside bounds (too large) error + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void ChapterNumberOutOfRangeError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("5", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, TempTok.Text, 0, TempTok.Text, "Chapter number out of range"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find verse numbers out of range + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumbersOutOfRangeError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("16", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("1-24", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, TempTok.Text, 0, TempTok.Text, "Verse number out of range"); + CheckError(1, TempTok2.Text, 0, TempTok2.Text, "Verse number out of range"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find verse "number" (i.e. text that's marked with the verse + /// style) at beyond last valid verse. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumbersBeyondLastValidInChapter() + { + m_dataSource.SetParameterValue("InvalidAtEnd", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("aa", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, TempTok.Text, 0, TempTok.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find multiple missing verse numbers error + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void MultipleVerseNumbersMissingError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verses 2-5 + m_dataSource.m_tokens.Add(new DummyTextToken("6-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("1-20", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verses 21-23 + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, m_dataSource.m_tokens[1].Text, 1, String.Empty, "Missing verse numbers 2-5"); + CheckError(1, TempTok.Text, 4, String.Empty, "Missing verse numbers 21-23"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find duplicate verse number error + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void DuplicateVerseNumberError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // Chapter 1 + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Duplicate verse number 1 + ITextToken TempTok = new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Chapter 2 + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Duplicate verse number 13 + ITextToken TempTok2 = new DummyTextToken("13", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Duplicate verse numbers 21-23 + ITextToken TempTok3 = new DummyTextToken("21-23", + TextType.VerseNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok3); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Duplicate verse number"); + CheckError(1, TempTok2.Text, 0, TempTok2.Text, "Duplicate verse number"); + CheckError(2, TempTok3.Text, 0, TempTok3.Text, "Unexpected verse numbers"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find verse numbers out of order (there will also be missing verses) + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumbersOutOfOrderError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("4-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-9", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("12-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("10-11", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Verse number out of order; expected verse 4"); + CheckError(1, TempTok2.Text, 0, TempTok2.Text, "Verse numbers out of order"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find verse number out of range for 999 and still find missing + /// verse next + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumberGreaterThan999() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("1-14", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("1515", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, TempTok2.Text, 0, TempTok2.Text, "Verse number out of range"); + CheckError(1, TempTok.Text, 4, String.Empty, "Missing verse numbers 15-16"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find no error for verse parts a and b + /// If there were a 2a with no 2b will throw missing verse for verse 5 + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumberPartsAandB() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2a", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("First part of verse two", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Section Head Text", + TextType.Other, true, false, "Section Head")); + m_dataSource.m_tokens.Add(new DummyTextToken("2b", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Second part of verse two", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-22", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("23a", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("23b", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that errors are flagged when only part a or part b of a verse couplet are + /// missing. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumberPartAOrB() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + + // Currently we don't catch any of these invalid cases... + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1a-3", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("4-6b", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("7-8a", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("9-10", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("11-13", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("14b-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + // These cases are valid... + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2a", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2b", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-4b", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("5", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + // These cases are not valid (should produce errors)... + ITextToken TempTok = new DummyTextToken("6a", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("7", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("8b", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok3 = new DummyTextToken("9b", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok3); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok4 = new DummyTextToken("10a", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok4); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok5 = new DummyTextToken("11b", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok5); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok6 = new DummyTextToken("12-13a", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok6); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("14", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("15-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(6)); + + CheckError(0, TempTok.Text, 2, String.Empty, "Missing verse number 6b"); + CheckError(1, TempTok2.Text, 0, String.Empty, "Missing verse number 8a"); + CheckError(2, TempTok3.Text, 0, String.Empty, "Missing verse number 9a"); + CheckError(3, TempTok4.Text, 3, String.Empty, "Missing verse number 10b"); + CheckError(4, TempTok5.Text, 0, String.Empty, "Missing verse number 11a"); + CheckError(5, TempTok6.Text, 6, String.Empty, "Missing verse number 13b"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find error for verse part c + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseNumberPartCError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, false, false, "Paragraph")); + ITextToken tempTok1 = new DummyTextToken("1-23a", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(tempTok1); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken tempTok2 = new DummyTextToken("23c", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(tempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, tempTok1.Text, 5, string.Empty, "Missing verse number 23b"); + CheckError(1, tempTok2.Text, 0, tempTok2.Text, "Invalid verse number"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Tests the case when chapter 1 and verse 1 of a book is missing at the same time. + /// + /// NOTE: The following comment was written before DavidO & CoreyW made significant + /// changes. I'm not sure exactly what the comment means and is probably no + /// longer relevant. -- DDO + /// + /// (If it has just finished last verse of previous chapter, it will assume new + /// chapter and only throw the two errors, missing chapter number and missing verse + /// number. It will not throw duplicate verse error for all following verses as it + /// would normally. Now it will only do so if verse 1 and 2 are missing after a + /// chapter number missing. Since it could be a more common error to lose chapter + /// number and verse one together, we allow for this unique case.) + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void MissingChapterOneandVerseOneError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, String.Empty, "Missing verse number 1"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Tests the case when chapter 2 and verse 1 of a book is missing at the same time. + /// The assumption for this test is that we'll get the same behavior when any chapter + /// greater than 2 is missing when that chapter's verse one is missing. That is why + /// we only check the case for chapter 2. See MissingChapterTwoandVerseOneError for + /// testing the case when chapter 1, verse 1 is missing at the same time. + /// (If it has just finished last verse of previous chapter, it will assume new + /// chapter and only throw the two errors, missing chapter number and missing verse + /// number. It will not throw duplicate verse error for all following verses as it + /// would normally. Now it will only do so if verse 1 and 2 are missing after a + /// chapter number missing. Since it could be a more common error to lose chapter + /// number and verse one together, we allow for this unique case.) + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void MissingChapterTwoandVerseOneError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("2", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("3-23", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Unexpected verse number"); + CheckError(1, TempTok2.Text, 0, TempTok2.Text, "Unexpected verse numbers"); + CheckError(2, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Space before or after verse number or bridge. Space is not easily visible + /// if it is formatted as a verse style. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void InvalidVerse_SpaceError() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken(" 1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("2-22 ", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, m_dataSource.m_tokens[1].Text, 0, m_dataSource.m_tokens[1].Text, + "Space found in verse number"); + CheckError(1, TempTok.Text, 0, TempTok.Text, "Space found in verse bridge"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Checks for a verse number that only consists of letters. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void InvalidVerse_InvalidCharacters() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("zv", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-13", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("14z7a", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("text", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("more text", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + ITextToken TempTok2 = new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok2); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok3 = new DummyTextToken("u-r-an-idot", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok3); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(7)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Invalid verse number"); + CheckError(1, m_dataSource.m_tokens[5].Text, 0, m_dataSource.m_tokens[5].Text, "Verse number out of range"); + CheckError(2, m_dataSource.m_tokens[5].Text, 0, m_dataSource.m_tokens[5].Text, "Invalid verse number"); + CheckError(3, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing verse number 1"); + CheckError(4, m_dataSource.m_tokens[3].Text, 4, String.Empty, "Missing verse number 14"); + CheckError(5, TempTok3.Text, 0, TempTok3.Text, "Invalid verse number"); + CheckError(6, TempTok2.Text, 1, String.Empty, "Missing verse numbers 2-22"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Checks for a chapter number that only consists of letters. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void InvalidChapter_InvalidCharacters() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("2-15", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + ITextToken TempTok = new DummyTextToken("jfuo", + TextType.ChapterNumber, true, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Invalid chapter number"); + CheckError(1, TempTok.Text, 4, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Checks for a missing verse number at the end of a chapter + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void MissingVerse_AtEndOfChapter() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "HAG"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1-14", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing verse 15, Missing chapter 2 + ITextToken TempTok = new DummyTextToken("1-23", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Duplicate verse numbers"); + CheckError(1, m_dataSource.m_tokens[1].Text, 4, String.Empty, "Missing verse number 15"); + CheckError(2, m_dataSource.m_tokens[0].Text, 1, String.Empty, "Missing chapter number 2"); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Checks for a missing verse number at the end of a chapter + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void MissingChapter_Multiple() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + // TODO: Figure out where to put vrs files for tests. Probably want to just include + // copies here locally with the tests. + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "JAS"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + // Missing chapter 1 (ignored) + m_dataSource.m_tokens.Add(new DummyTextToken("1-27", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing chapter 2 + ITextToken TempTok = new DummyTextToken("1-26", + TextType.VerseNumber, false, false, "Paragraph"); + m_dataSource.m_tokens.Add(TempTok); + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + // Missing chapters 3-5 + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(5)); + + CheckError(0, TempTok.Text, 0, TempTok.Text, "Duplicate verse numbers"); + + for (int i = 2; i < 6; i++) + CheckError(i - 1, m_dataSource.m_tokens[0].Text, 0, String.Empty, string.Format("Missing chapter number {0}", i)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing verse text without white space + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseTextMissingText() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-12", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(4)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing verse text with white space + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseTextMissingTextWithWhiteSpace() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-12", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken(" ", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken(Environment.NewLine, + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("\t", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken(" ", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(4)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that check should assume missing chapter one and verse one present with + /// first text token. + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseTextAssumeChapterOneVerseOne() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + + // no chapter one - assumed + // no verse one - assumed + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-12", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing chapter one + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseTextMissingChapterOne() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + + // no chapter one - assumed + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-12", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("1-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ----------------------------------------------------------------------------------- + /// + /// Test that should find a missing verse one + /// + /// ----------------------------------------------------------------------------------- + [Test] + public void VerseTextMissingVerseOne() + { + m_dataSource.SetParameterValue("OmittedVerses", ""); + m_dataSource.SetParameterValue("Versification Scheme", "English"); + m_dataSource.SetParameterValue("Book ID", "2TH"); + m_dataSource.SetParameterValue("Chapter Number", "0"); + + + // no verse one - assumed + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-12", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-17", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("3", + TextType.ChapterNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("2-18", + TextType.VerseNumber, true, false, "Paragraph")); + + m_dataSource.m_tokens.Add(new DummyTextToken("verse body", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + //Need test for chapter number out of range with verses following it (because valid chapter missing). + //The errors thrown currently are correct,but need a test for that, for those errors to be optimized in the future. + //Currently handles out of range chapter, by incrementing to next valid chapter, assuming that is what's wanted + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/CharactersCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/CharactersCheckUnitTest.cs new file mode 100644 index 0000000000..51268d0fbf --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/CharactersCheckUnitTest.cs @@ -0,0 +1,299 @@ +// --------------------------------------------------------------------------------------------- +// Copyright (c) 2008-2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) +// +// File: RepeatedWordsTests.cs +// Responsibility: TE Team +// --------------------------------------------------------------------------------------------- +using System.Collections.Generic; +using NUnit.Framework; +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Utils; + +namespace SILUBS.ScriptureChecks +{ + /// ------------------------------------------------------------------------------------ + /// + /// Test the Characters Scripture check using a data source that passes tokens similar + /// to those produced by TE. + /// + /// ------------------------------------------------------------------------------------ + [TestFixture] + public class CharactersCheckUnitTest_Fw : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + #region Initialization + /// ------------------------------------------------------------------------------------ + /// + /// Set up that happens before every test runs. + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_check = new CharactersCheck(m_dataSource); + } + #endregion + + #region Tests + ///-------------------------------------------------------------------------------------- + /// + /// Tests the Character check for some simple cases that don't freak your mind out. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void Basic() + { + m_dataSource.SetParameterValue("ValidCharacters", "a b c d e"); + + m_dataSource.m_tokens.Add(new DummyTextToken("gha bcdefi", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(4)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "g", "Invalid or unknown character"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, "h", "Invalid or unknown character"); + CheckError(2, m_dataSource.m_tokens[0].Text, 8, "f", "Invalid or unknown character"); + CheckError(3, m_dataSource.m_tokens[0].Text, 9, "i", "Invalid or unknown character"); + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests the AlwaysValidCharacters check for some simple cases. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void AlwaysValidChars() + { + m_dataSource.SetParameterValue("AlwaysValidCharacters", "12345\u2028"); + m_dataSource.m_tokens.Add(new DummyTextToken("ej53427\u20281fi", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(5)); + CheckError(0, m_dataSource.m_tokens[0].Text, 0, "e", "Invalid or unknown character"); + CheckError(1, m_dataSource.m_tokens[0].Text, 1, "j", "Invalid or unknown character"); + CheckError(2, m_dataSource.m_tokens[0].Text, 6, "7", "Invalid or unknown character"); + CheckError(3, m_dataSource.m_tokens[0].Text, 9, "f", "Invalid or unknown character"); + CheckError(4, m_dataSource.m_tokens[0].Text, 10, "i", "Invalid or unknown character"); + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests the Character check with diacritic characters. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void Diacritics() + { + m_dataSource.SetParameterValue("ValidCharacters", "a b c d e a\u0301 e\u0301"); + + // 02 JUN 2008, Phil Hopper: InvalidCharacters is not currently used. + //m_dataSource.SetParameterValue("InvalidCharacters", "f g h a\u0302 e\u0302"); + + m_dataSource.m_tokens.Add(new DummyTextToken("aa\u0301bcdea\u0302e\u0303", + TextType.Verse, true, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 7, "a\u0302", + "Invalid or unknown character diacritic combination"); // invalid character + CheckError(1, m_dataSource.m_tokens[0].Text, 9, "e\u0303", + "Invalid or unknown character diacritic combination"); // unknown character + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests the Character check with different writing systems. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void DifferentWritingSystems() + { + // Set the valid characters for different writing systems. The vernacular doesn't + // specify a locale. + m_dataSource.SetParameterValue("ValidCharacters", "a b c d e f g"); + m_dataSource.SetParameterValue("ValidCharacters_en", "h i j k l m n"); + m_dataSource.SetParameterValue("ValidCharacters_fr", "o p q r s t u"); + + m_dataSource.m_tokens.Add(new DummyTextToken("abcdefgh", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("hijklmno", + TextType.Verse, true, false, "Paragraph", string.Empty, "en")); + m_dataSource.m_tokens.Add(new DummyTextToken("aopqrstu", + TextType.Verse, true, false, "Paragraph", string.Empty, "fr")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(3)); + CheckError(0, m_dataSource.m_tokens[0].Text, 7, "h", "Invalid or unknown character"); + CheckError(1, m_dataSource.m_tokens[1].Text, 7, "o", "Invalid or unknown character"); + CheckError(2, m_dataSource.m_tokens[2].Text, 0, "a", "Invalid or unknown character"); + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests the Character check doesn't crash if the valid characters list is not set. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void UnsetValidCharactersList() + { + m_dataSource.m_tokens.Add(new DummyTextToken("abcdefgh", + TextType.Verse, true, false, "Paragraph")); + + // This should not crash, even if the valid characters list has not been set. + List refs = + CheckInventory.GetReferences(m_dataSource.TextTokens(), string.Empty); + + Assert.That(refs.Count, Is.EqualTo(8)); + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests the Character check with different writing systems. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void InventoryMode() + { + m_dataSource.m_tokens.Add(new DummyTextToken("Eph. 2:10", + TextType.Verse, true, false, "Paragraph", string.Empty, "en")); + m_dataSource.m_tokens.Add(new DummyTextToken("For we are God's workmanship...", + TextType.Verse, true, false, "Paragraph")); + + List refs = + CheckInventory.GetReferences(m_dataSource.TextTokens(), string.Empty); + + // We requested only the default vernacular. + // Should only get references from the second token. + Assert.That(refs.Count, Is.EqualTo(31)); + } + + ///-------------------------------------------------------------------------------------- + /// + /// Tests that ParseCharacterSequences returns characters with their following diacritics. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void ParseCharacterSequences_Diacritics() + { + // Arabic letter alef with madda above + // Arabic letter yeh with hamza above and immediately followed by a character without + // diacritics (Arabic letter zain) + string charsWithDiacritics = "\u0627\u0653 \u064A\u0654\u0632"; + + // set up for this test. + m_dataSource.m_tokens.Add(new DummyTextToken(charsWithDiacritics, + TextType.Verse, true, false, "Paragraph")); + m_check = new CharactersCheck(m_dataSource); + ReflectionHelper.SetField(m_check, "m_categorizer", m_dataSource.CharacterCategorizer); + + // Get the parsed character sequences. + List parsedChars = new List(); + foreach (string character in ((CharactersCheck)m_check).ParseCharacterSequences(charsWithDiacritics)) + parsedChars.Add(character); + + // Confirm that we have four characters with the expected contents. + Assert.That(parsedChars.Count, Is.EqualTo(4), "We expected four characters"); + Assert.That(parsedChars[0], Is.EqualTo("\u0627\u0653")); + Assert.That(parsedChars[1], Is.EqualTo(" ")); + Assert.That(parsedChars[2], Is.EqualTo("\u064A\u0654")); + Assert.That(parsedChars[3], Is.EqualTo("\u0632")); + } + #endregion + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Characters Scripture check using the USFM-style data source. + /// + /// ------------------------------------------------------------------------------------ + [TestFixture] + public class CharactersCheckUnitTest_Usfm + { + private UnitTestChecksDataSource m_UsfmDataSource = new UnitTestChecksDataSource(); + + void Test(string[] result, string text) + { + Test(result, text, ""); + } + + void Test(string[] result, string text, string desiredKey) + { + m_UsfmDataSource.Text = text; + + CharactersCheck check = new CharactersCheck(m_UsfmDataSource); + List tts = + check.GetReferences(m_UsfmDataSource.TextTokens(), desiredKey); + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0)+1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i]), "Result number: " + i.ToString()); + } + + #region Tests + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void Text() + { + Test(new string[] { "\u201C", "T", "h", "e", " ", "t", "e", "x", "t", ".", "\u201D"}, + "\\p \u201CThe text.\u201D"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void CanBeComposed() + { + Test(new string[] { "\u0210", "a" }, "\\p \u0210a"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void CanBeDeComposed() + { + Test(new string[] { "\u0210", "a" }, "\\p R\u030Fa"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void FindingComposed() + { + Test(new string[] { "\u0210" }, "\\p R\u030Fa \u0210a", "\u0210"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void FindingDeComposed() + { + Test(new string[] { "R\u030F" }, "\\p R\u030Fa \u0210a", "R\u030F"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void MustBeDeComposed() + { + Test(new string[] { "B\u030B", "a" }, "\\p B\u030Ba"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void NonRoman() + { + Test(new string[] { "\u0E01\u0E34", "\u0E02" }, "\\p \u0E01\u0E34\u0E02"); + } + + [Test] + [Ignore("Missing implementation of get_Locale on UnitTestTokenizer causes this to fail")] + public void PUA() + { + Test(new string[] { "\uEE00" }, "\\p \uEE00"); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/DummyTextToken.cs b/Lib/src/ScrChecks/ScrChecksTests/DummyTextToken.cs new file mode 100644 index 0000000000..7b020eba46 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/DummyTextToken.cs @@ -0,0 +1,220 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Core.Scripture; + +namespace SILUBS.ScriptureChecks +{ + /// ------------------------------------------------------------------------------------ + /// + /// See ITextToken for field definitions comments. + /// This is a dummy class used for testing the checks. + /// + /// ------------------------------------------------------------------------------------ + public class DummyTextToken : ITextToken + { + private string m_text; + private TextType m_textType; + private bool m_isParagraphStart; + private bool m_isNoteStart; + private string m_paraStyleName; + private string m_charStyleName; + private string m_iculocale = null; + private BCVRef m_missingStartRef; + private BCVRef m_missingEndRef; + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// ------------------------------------------------------------------------------------ + public DummyTextToken(string text) + { + m_text = text; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// Type of the text. + /// if set to true text token starts a paragraph. + /// + /// if set to true text token starts a note. + /// Name of the paragraph style. + /// ------------------------------------------------------------------------------------ + public DummyTextToken(string text, TextType textType, bool isParagraphStart, + bool isNoteStart, string paraStyleName) : this(text, textType, isParagraphStart, + isNoteStart, paraStyleName, string.Empty, null) + { + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// Type of the text. + /// if set to true text token starts a paragraph. + /// + /// if set to true text token starts a note. + /// Name of the paragraph style. + /// Name of the character style. + /// ------------------------------------------------------------------------------------ + public DummyTextToken(string text, TextType textType, bool isParagraphStart, + bool isNoteStart, string paraStyleName, string charStyleName) + : this(text, textType, isParagraphStart, + isNoteStart, paraStyleName, charStyleName, null) + { + } + + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// Type of the text. + /// if set to true text token starts a paragraph. + /// + /// if set to true text token starts a note. + /// Name of the paragraph style. + /// Name of the character style. + /// The icu locale. + /// ------------------------------------------------------------------------------------ + public DummyTextToken(string text, TextType textType, bool isParagraphStart, + bool isNoteStart, string paraStyleName, string charStyleName, string icuLocale) + { + m_text = text; + m_textType = textType; + m_isParagraphStart = isParagraphStart; + m_isNoteStart = isNoteStart; + m_paraStyleName = paraStyleName; + m_charStyleName = charStyleName; + m_iculocale = icuLocale; + } + + #region ITextToken Members + /// ------------------------------------------------------------------------------------ + /// + /// Gets/sets the locale. + /// + /// ------------------------------------------------------------------------------------ + public string Locale + { + get { return m_iculocale; } + set { m_iculocale = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets the type of the text. + /// + /// ------------------------------------------------------------------------------------ + public TextType TextType + { + get { return m_textType; } + set { m_textType = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets a value indicating whether this instance is note start. + /// + /// ------------------------------------------------------------------------------------ + public bool IsNoteStart + { + get { return m_isNoteStart; } + set { m_isNoteStart = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets a value indicating whether this instance is paragraph start. + /// + /// ------------------------------------------------------------------------------------ + public bool IsParagraphStart + { + get { return m_isParagraphStart; } + set { m_isParagraphStart = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets the name of the paragraph style. + /// + /// ------------------------------------------------------------------------------------ + public string ParaStyleName + { + get { return m_paraStyleName; } + set { m_paraStyleName = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets the name of the character style. + /// + /// ------------------------------------------------------------------------------------ + public string CharStyleName + { + get { return m_charStyleName; } + set { m_charStyleName = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets or sets the text. + /// + /// ------------------------------------------------------------------------------------ + public string Text + { + get { return m_text; } + set { m_text = value; } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Gets the Scripture reference as a string, suitable for displaying in the UI + /// + /// ------------------------------------------------------------------------------------ + public string ScrRefString + { + get { return string.Empty; } + set { ; } + } + + public BCVRef MissingEndRef + { + get { return m_missingEndRef; } + set { m_missingEndRef = value; } + } + + public BCVRef MissingStartRef + { + get { return m_missingStartRef; } + set { m_missingStartRef = value; } + } + + public override string ToString() + { + return Text; + } + + /// ------------------------------------------------------------------------------------ + /// + /// Makes a deep copy of this text token. + /// + /// ------------------------------------------------------------------------------------ + public ITextToken Clone() + { + DummyTextToken copy = new DummyTextToken(Text, TextType, IsParagraphStart, + IsNoteStart, ParaStyleName, CharStyleName, Locale); + copy.m_missingStartRef = m_missingStartRef; + copy.m_missingEndRef = m_missingEndRef; + return copy; + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/MatchedPairsCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/MatchedPairsCheckUnitTest.cs new file mode 100644 index 0000000000..7c175e3753 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/MatchedPairsCheckUnitTest.cs @@ -0,0 +1,409 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ------------------------------------------------------------------------------------ + /// + /// Tests the Matched Pairs check using the USFM-style data source + /// + /// ------------------------------------------------------------------------------------ + [TestFixture] + public class MatchedPairsCheckUnitTest_Usfm : ScrChecksTestBase + { + internal const string kMatchedPairXml1 = + "" + + "" + + "" + + "" + + "" + + ""; + + internal const string kMatchedPairXml2 = + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + UnitTestChecksDataSource m_dataSource = new UnitTestChecksDataSource(); + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource.SetParameterValue("MatchedPairs", kMatchedPairXml1); + m_dataSource.SetParameterValue("IntroductionOutlineStyles", "io"); + m_dataSource.SetParameterValue("PoeticStyles", + "q1" + CheckUtils.kStyleNamesDelimiter.ToString() + "q2"); + m_check = new MatchedPairsCheck(m_dataSource); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + void Test(string[,] result, string text) + { + m_dataSource.Text = text; + + List tts = + CheckInventory.GetReferences(m_dataSource.TextTokens(), string.Empty); + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0) + 1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + { + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i, 0]), "InventoryText number: " + i); + Assert.That(tts[i].Message, Is.EqualTo(result[i, 1]), "Message number: " + i); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Matched() + { + Test(new string[0, 0], @"\p \v 1 [foo]"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void UnMatched() + { + string[,] result = new string[,] + { + { "]", "Unmatched punctuation" }, + { "[", "Unmatched punctuation" }, + }; + + Test(result, @"\p \v 1 ]foo["); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void NestedDifferentMatched() + { + Test(new string[0, 0], @"\p \v 1 (foo [bar] baz)"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void NestedSameMatched() + { + Test(new string[0, 0], @"\p \v 1 (foo (bar) baz)"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void MutipleUnMatched() + { + string[,] result = new string[,] + { + { "(", "Unmatched punctuation" }, + { "]", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" } + }; + + Test(result, @"\p \v 1 (foo] bar)"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Overlapping() + { + string[,] result = new string[,] + { + { "(", "Overlapping pair" }, + { "[", "Overlapping pair" }, + { ")", "Overlapping pair" }, + { "]", "Overlapping pair" } + }; + + Test(result, @"\p \v 1 (foo [bar) baz]"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void BodyFootnote() + { + string[,] result = new string[,] + { + { ")", "Unmatched punctuation" }, + { "(", "Unmatched punctuation" }, + }; + + Test(result, @"\p \v 1 (foo \f + bar)\f* baz"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void FootnoteFootnote() + { + string[,] result = new string[,] + { + { "(", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\p \v 1 \f + (foo\f* bar \f + baz)\f*"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ZMoreMatchedPairs() + { + string[,] result = new string[,] + { + { "\u00A1", "Unmatched punctuation" } + }; + + m_dataSource.SetParameterValue("MatchedPairs", kMatchedPairXml2); + Test(result, "\\p \\v 1 \u00A1hola"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void IntroOutlineValue() + { + string[,] result = new string[,] + { + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\io A.] foo 1) bar 2) baz"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void IntroOutlineValueWrongDirection() + { + string[,] result = new string[,] + { + { "[", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\io A.[ foo 1) bar 2) baz"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void IntroOutlineValueDuplicate() + { + string[,] result = new string[,] + { + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\io A.)] foo 1) bar 2) baz"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void IntroOutline() + { + string[,] result = new string[,] + { + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\io A. B.) foo 1) bar 2) baz"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void TestClosedByParagraphForNormalPara() + { + string[,] result = new string[,] + { + { "(", "Unmatched punctuation" }, + { ")", "Unmatched punctuation" }, + }; + + Test(result, @"\p \v 1 (foo \p \v 2 bar)"); + Test(new string[0, 0], @"\p \v 1 {foo \p \v 2 bar}"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void TestClosedByParagraphForPoetryPara() + { + Test(new string[0, 0], @"\q1 \v 1 [foo \q2 \v 2 bar]"); + Test(new string[0, 0], @"\q1 \v 1 {foo \q2 \v 2 bar}"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void SectionHead() + { + Test(new string[,] {{ ")", "Unmatched punctuation" },}, @"\s text)"); + } + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests the Matched Pairs check using a data source that passes tokens similar + /// to those produced by TE. + /// + /// ------------------------------------------------------------------------------------ + [TestFixture] + public class MatchedPairsCheckUnitTest_Fw : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + #region Initialization + /// ------------------------------------------------------------------------------------ + /// + /// Set up that happens before every test runs. + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_check = new MatchedPairsCheck(m_dataSource); + m_dataSource.SetParameterValue("PoeticStyles", "Citation Line1" + + CheckUtils.kStyleNamesDelimiter.ToString() + "Citation Line2"); + } + #endregion + + #region Tests + /// ------------------------------------------------------------------------------------ + /// + /// Tests that we correctly detect a paragraph start when it begins with a verse number + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void OpenParenFollowedByParaStartingWithVerseNum() + { + m_dataSource.SetParameterValue("MatchedPairs", MatchedPairsCheckUnitTest_Usfm.kMatchedPairXml1); + + m_dataSource.m_tokens.Add(new DummyTextToken("This is nice (and by nice, I mean", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken(" really, super nice). Amen?", + TextType.Verse, false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, m_dataSource.m_tokens[0].Text, 13, "(", "Unmatched punctuation"); + CheckError(1, m_dataSource.m_tokens[2].Text, 19, ")", "Unmatched punctuation"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that a footnote doesn't mess up processing of surrounding body text when a + /// matched pair spans paragraphs. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void OpenFollowedByFootnoteFollowedByParaWithClosing() + { + m_dataSource.SetParameterValue("MatchedPairs", MatchedPairsCheckUnitTest_Usfm.kMatchedPairXml1); + + m_dataSource.m_tokens.Add(new DummyTextToken("This is nice (and by nice, I mean", + TextType.Verse, true, false, "Citation Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("Mean <> cruel", + TextType.Note, true, true, "Note General Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken(" text following footnote.", + TextType.Verse, false, false, "Citation Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("really, super nice). Amen?", + TextType.Verse, true, false, "Citation Line1")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/MixedCapitalizationCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/MixedCapitalizationCheckUnitTest.cs new file mode 100644 index 0000000000..e84e77d24b --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/MixedCapitalizationCheckUnitTest.cs @@ -0,0 +1,317 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Unit tests for the MixedCapitalizationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class MixedCapitalizationCheckUnitTest + { + UnitTestChecksDataSource m_source = new UnitTestChecksDataSource(); + + [SetUp] + public void RunBeforeEachTest() + { + m_source.SetParameterValue("UncapitalizedPrefixes", ""); + m_source.SetParameterValue("CapitalizedSuffixes", ""); + m_source.SetParameterValue("CapitalizedPrefixes", ""); + } + + void Test(string[] result, string text) + { + Test(result, text, ""); + } + + void Test(string[] result, string text, string desiredKey) + { + m_source.Text = text; + + MixedCapitalizationCheck check = new MixedCapitalizationCheck(m_source); + List tts = + check.GetReferences(m_source.TextTokens(), desiredKey); + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0)+1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i]), "Result number: " + i.ToString()); + } + + [Test] + public void WordNoPrefixLower() + { + AWord word = new AWord("bat", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("")); + } + [Test] + public void WordNoSuffixLower() + { + AWord word = new AWord("bat", m_source.CharacterCategorizer); + Assert.That(word.Suffix, Is.EqualTo("")); + } + + [Test] + public void WordNoPrefixUpper() + { + AWord word = new AWord("BAT", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("")); + } + [Test] + public void WordNoSuffixUpper() + { + AWord word = new AWord("BAT", m_source.CharacterCategorizer); + Assert.That(word.Suffix, Is.EqualTo("")); + } + + [Test] + public void WordPrefixLower() + { + AWord word = new AWord("caBat", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("ca")); + } + + [Test] + public void WordPrefixLowerWithTitle() + { + AWord word = new AWord("ca\u01C5at", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("ca")); + } + + [Test] + public void WordPrefixUpper() + { + AWord word = new AWord("CaBat", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("Ca")); + } + + [Test] + public void WordSuffix() + { + AWord word = new AWord("DavidBen", m_source.CharacterCategorizer); + Assert.That(word.Suffix, Is.EqualTo("Ben")); + } + + [Test] + public void WordWithNumberNoPrefix() + { + AWord word = new AWord("1Co", m_source.CharacterCategorizer); + Assert.That(word.Prefix, Is.EqualTo("")); + } + [Test] + public void WordWithNumberNoSuffix() + { + AWord word = new AWord("1Co", m_source.CharacterCategorizer); + Assert.That(word.Suffix, Is.EqualTo("")); + } + + [Test] + public void Regular() + { + Test(new string[] { }, @"\p \v 1 Bat"); + } + + [Test] + public void TwoCapitalLetter() + { + Test(new string[] { "BaT" }, @"\p \v 1 BaT"); + } + + [Test] + public void TwoCapitalLetterDiacritic_MustBeDeComposed() + { + Test(new string[] { "B\u030BaT\u030B" }, "\\p \\v 1 B\u030BaT\u030B"); + } + + [Test] + public void TwoCapitalLetterDiacritic_CanBeComposed() + { + Test(new string[] { "\u0210a\u0210" }, "\\p \\v 1 \u0210a\u0210"); + } + + [Test] + public void TwoCapitalLetterDiacritic_CanBeDeComposed() + { + Test(new string[] { "R\u030FaR\u030F" }, "\\p \\v 1 R\u030FaR\u030F"); + } + + [Test] + public void AllCaps() + { + Test(new string[] { }, @"\p \v 1 BAT"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that diacritics within a lowercase word does not return a mixed capitalization + /// problem. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void AllLowerDiacriticNFC() + { + Test(new string[] { }, "\\p \\v 1 Phile\u0301mon"); + } + + [Test] + public void AllCapsDiacritic_MustBeDeComposed() + { + Test(new string[] { }, "\\p \\v 1 B\030BAT"); + } + + [Test] + public void AllCapsDiacritic_CanBeComposed() + { + Test(new string[] { }, "\\p \\v 1 \0210AT"); + } + + [Test] + public void AllCapsDiacritic_CanBeDeComposed() + { + Test(new string[] { }, "\\p \\v 1 R\u030FAT"); + } + + [Test] + public void UncapitalizedPrefix() + { + Test(new string[] { "aBat" }, @"\p \v 1 aBat"); + } + + [Test] + public void UncapitalizedPrefixDiacritic_MustBeDeComposed() + { + Test(new string[] { "aB\u030Bat" }, "\\p \\v 1 aB\u030Bat"); + } + + [Test] + public void UncapitalizedPrefixDiacritic_CanBeComposed() + { + Test(new string[] { "a\u0210at" }, "\\p \\v 1 a\u0210at"); + } + + [Test] + public void UncapitalizedPrefixDiacritic_CanBeDeComposed() + { + Test(new string[] { "aR\u030Fat" }, "\\p \\v 1 aR\u030Fat"); + } + + [Test] + [Ignore("Text needs to be normalized to NFC (or maybe NFD) before check is run.")] + public void FindingDifferentNormalization() + { + Test(new string[] { "a\u0210at", "a\u0210at" }, + "\\p \\v 1 aR\u030Fat aNd a\u0210at", "a\u0210at"); + } + + [Test] + public void UncapitalizedPrefixTitleCase() + { + Test(new string[] { "a\u01C5at" }, "\\p \\v 1 a\u01C5at"); + } + + [Test] + public void UncapitalizedPrefixSpecificOK() + { + m_source.SetParameterValue("UncapitalizedPrefixes", "a"); + Test(new string[] { }, @"\p \v 1 aBat"); + } + + [Test] + public void UncapitalizedPrefixPatternOK1() + { + m_source.SetParameterValue("UncapitalizedPrefixes", "*a"); + Test(new string[] { }, @"\p \v 1 baBat"); + } + [Test] + public void UncapitalizedPrefixPatternOK2() + { + m_source.SetParameterValue("UncapitalizedPrefixes", "*a"); + Test(new string[] { }, @"\p \v 1 caBat"); + } + + [Test] + public void UncapitalizedPrefixAllOK1() + { + m_source.SetParameterValue("UncapitalizedPrefixes", "*"); + Test(new string[] { }, @"\p \v 1 baBat"); + } + [Test] + public void UncapitalizedPrefixAllOK2() + { + m_source.SetParameterValue("UncapitalizedPrefixes", "*"); + Test(new string[] { }, @"\p \v 1 caBat"); + } + + [Test] + public void CapitalizedSuffixOK() + { + m_source.SetParameterValue("CapitalizedSuffixes", "Bat"); + Test(new string[] { }, @"\p \v 1 CaBat"); + } + + [Test] + public void CapitalizedPrefixOK() + { + m_source.SetParameterValue("CapitalizedPrefixes", "Ca"); + Test(new string[] { }, @"\p \v 1 CaBat"); + } + + [Test] + public void WithNumbers() + { + Test(new string[] { }, @"\p \v 1 1Co"); + } + [Test] + public void WithNumbersPrefix() + { + Test(new string[] { "1CoR" }, @"\p \v 1 1CoR"); + } + + [Test] + public void NonLettersOneCapitalLetter() + { + Test(new string[] { }, @"\p \v 1 Foo-bar"); + } + + [Test] + public void NonLettersTwoCapitalLetter() + { + Test(new string[] { "Foo-Bar" }, @"\p \v 1 Foo-Bar"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests changing the character categorizer after the check is instantiated. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ChangeCharacterCategorizerAfterInstantiationOfCheck() + { + MixedCapitalizationCheck check = new MixedCapitalizationCheck(m_source); + + m_source.Text = @"\p \v 1 w!Forming"; + + List tts = check.GetReferences(m_source.TextTokens(), null); + + Assert.That(tts.Count, Is.EqualTo(0)); + + m_source.m_extraWordFormingCharacters = "!"; + + tts = check.GetReferences(m_source.TextTokens(), null); + + Assert.That(tts.Count, Is.EqualTo(1)); + Assert.That(tts[0].Text, Is.EqualTo("w!Forming")); + } + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/PunctuationCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/PunctuationCheckUnitTest.cs new file mode 100644 index 0000000000..295298db90 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/PunctuationCheckUnitTest.cs @@ -0,0 +1,838 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Unit tests for the PunctuationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class PunctuationCheckUnitTest : ScrChecksTestBase + { + private UnitTestChecksDataSource m_dataSource = new UnitTestChecksDataSource(); + + #region Setup + /// ------------------------------------------------------------------------------------ + /// + /// Test fixture setup (runs once for the whole fixture). + /// + /// ------------------------------------------------------------------------------------ + [OneTimeSetUp] + public void FixtureSetup() + { + QuotationMarksList qmarks = QuotationMarksList.NewList(); + qmarks.QMarksList[0].Opening = "\u201C"; + qmarks.QMarksList[0].Closing = "\u201D"; + qmarks.QMarksList[1].Opening = "\u2018"; + qmarks.QMarksList[1].Closing = "\u2019"; + qmarks.EnsureLevelExists(5); + m_dataSource.SetParameterValue("QuotationMarkInfo", qmarks.XmlString); + m_dataSource.SetParameterValue("PunctWhitespaceChar", "_"); + m_check = new PunctuationCheck(m_dataSource); + } + #endregion + + #region Helper methods + /// ------------------------------------------------------------------------------------ + /// + /// Tests that processing the specified text produces the expected punctuation pattern. + /// Use this version for tests that expect a single pattern. + /// + /// The expected punct pattern. + /// The expected offset. + /// A string marked up with SF codes representing a text to be + /// processed. + /// ------------------------------------------------------------------------------------ + void TestGetReferences(string expectedPunctPattern, int expectedOffset, string text) + { + TestGetReferences(new string[] { expectedPunctPattern }, new int[] { expectedOffset }, text); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that processing the specified text using the GetReferences method produces the + /// expected punctuation patterns. + /// + /// The expected punct patterns. + /// The expected offsets. + /// A string marked up with SF codes representing a text to be + /// processed. + /// ------------------------------------------------------------------------------------ + void TestGetReferences(string[] expectedPunctPatterns, int[] expectedOffsets, string text) + { + Assert.That(expectedOffsets.Length, Is.EqualTo(expectedPunctPatterns.Length), "Poorly defined expected test results."); + m_dataSource.Text = text; + + PunctuationCheck check = new PunctuationCheck(m_dataSource); + List tts = + check.GetReferences(m_dataSource.TextTokens(), String.Empty); + + Assert.That(tts.Count, Is.EqualTo(expectedPunctPatterns.Length), "Unexpected number of punctuation patterns."); + + for (int i = 0; i < expectedPunctPatterns.Length; i++ ) + { + Assert.That(tts[i].InventoryText, Is.EqualTo(expectedPunctPatterns[i]), "Result number: " + i); + Assert.That(tts[i].Offset, Is.EqualTo(expectedOffsets[i]), "Result number: " + i); + } + } + #endregion + + #region GetReferences tests + [Test] + public void GetReferences_BasicMedial() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("-", 3, "\\p \\v 1 pre-word"); + } + + [Test] + public void GetReferences_IntermediateMedial() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("-", 3, "\\p \\v 1 pre-word"); + } + + [Test] + public void GetReferences_AdvancedMedial() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("-", 3, "\\p \\v 1 pre-word"); + } + + [Test] + public void GetReferences_BasicIsolated() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("_\u2014_", 5, "\\p \\v 1 word \u2014 word"); + } + + [Test] + public void GetReferences_IntermediateIsolated() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("_\u2014_", 5, "\\p \\v 1 word \u2014 word"); + } + + [Test] + public void GetReferences_AdvancedIsolated() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("_\u2014_", 5, "\\p \\v 1 word \u2014 word"); + } + + [Test] + public void GetReferences_BasicDoubleStraightQuoteAfterVerseNum() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow.", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"Word", + TextType.Verse, false, false, "Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(2)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(3)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("Wow.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"Word")); + } + + [Test] + public void GetReferences_IntermediateDoubleStraightQuoteAfterVerseNum() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow.", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"Word", + TextType.Verse, false, false, "Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(2)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(3)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("Wow.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"Word")); + } + + [Test] + public void GetReferences_AdvancedDoubleStraightQuoteAfterVerseNum() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow.", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"Word", + TextType.Verse, false, false, "Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(2)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(3)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("Wow.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"Word")); + } + + [Test] + public void GetReferences_BasicVerseNumBetweenNotes() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("I am a note.", + TextType.Note, true, true, "Note General Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"I am a quote note!\"", + TextType.Note, true, true, "Note General Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(4)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(11)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("I am a note.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + + Assert.That(tokens[2].InventoryText, Is.EqualTo("!_")); + Assert.That(tokens[2].Offset, Is.EqualTo(18)); + Assert.That(tokens[2].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + + Assert.That(tokens[3].InventoryText, Is.EqualTo("\"_")); + Assert.That(tokens[3].Offset, Is.EqualTo(19)); + Assert.That(tokens[3].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + } + + [Test] + public void GetReferences_IntermediateVerseNumBetweenNotes() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("I am a note.", + TextType.Note, true, true, "Note General Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"I am a quote note!\"", + TextType.Note, true, true, "Note General Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(3)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(11)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("I am a note.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + + Assert.That(tokens[2].InventoryText, Is.EqualTo("!\"_")); + Assert.That(tokens[2].Offset, Is.EqualTo(18)); + Assert.That(tokens[2].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + } + + [Test] + public void GetReferences_AdvancedVerseNumBetweenNotes() + { + TestChecksDataSource dataSource = new TestChecksDataSource(); + dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + + PunctuationCheck check = new PunctuationCheck(dataSource); + dataSource.m_tokens.Add(new DummyTextToken("Wow", + TextType.Verse, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("I am a note.", + TextType.Note, true, true, "Note General Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("17", + TextType.VerseNumber, true, false, "Paragraph")); + dataSource.m_tokens.Add(new DummyTextToken("\"I am a quote note!\"", + TextType.Note, true, true, "Note General Paragraph")); + List tokens = + check.GetReferences(dataSource.TextTokens(), string.Empty); + Assert.That(tokens.Count, Is.EqualTo(3)); + + Assert.That(tokens[0].InventoryText, Is.EqualTo("._")); + Assert.That(tokens[0].Offset, Is.EqualTo(11)); + Assert.That(tokens[0].FirstToken.Text, Is.EqualTo("I am a note.")); + + Assert.That(tokens[1].InventoryText, Is.EqualTo("_\"")); + Assert.That(tokens[1].Offset, Is.EqualTo(0)); + Assert.That(tokens[1].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + + Assert.That(tokens[2].InventoryText, Is.EqualTo("!\"_")); + Assert.That(tokens[2].Offset, Is.EqualTo(18)); + Assert.That(tokens[2].FirstToken.Text, Is.EqualTo("\"I am a quote note!\"")); + } + + [Test] + public void GetReferences_BasicInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("_\u201C", 5, "\\p \\v 1 word \u201Cword"); + } + + [Test] + public void GetReferences_IntermediateInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("_\u201C", 5, "\\p \\v 1 word \u201Cword"); + } + + [Test] + public void GetReferences_AdvancedInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("_\u201C", 5, "\\p \\v 1 word \u201Cword"); + } + + [Test] + public void GetReferences_BasicParagraphInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("_\u201C", 0, "\\p \\v 1 \u201Cword"); + } + + [Test] + public void GetReferences_IntermediateParagraphInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("_\u201C", 0, "\\p \\v 1 \u201Cword"); + } + + [Test] + public void GetReferences_AdvancedParagraphInitialSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("_\u201C", 0, "\\p \\v 1 \u201Cword"); + } + + [Test] + public void GetReferences_BasicInitialMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "_\u201C", "_\u2018" }, + new int[] { 5, 7 }, + "\\p \\v 1 word \u201C \u2018word"); + } + + [Test] + public void GetReferences_IntermediateInitialMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("_\u201C_\u2018", 5, "\\p \\v 1 word \u201C \u2018word"); + } + + [Test] + public void GetReferences_AdvancedInitialMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("_\u201C_\u2018", 5, "\\p \\v 1 word \u201C \u2018word"); + try + { + m_dataSource.SetParameterValue("PunctWhitespaceChar", " "); + TestGetReferences(" \u201C \u2018", 5, "\\p \\v 1 word \u201C \u2018word"); + } + finally + { + m_dataSource.SetParameterValue("PunctWhitespaceChar", "_"); + } + } + + [Test] + public void GetReferences_BasicFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D word"); + } + + [Test] + public void GetReferences_IntermediateFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D word"); + } + + [Test] + public void GetReferences_AdvancedFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D word"); + } + + [Test] + public void GetReferences_BasicParagraphFinalMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + // REVIEW: This appears to be the intended design, but it doesn't seem to be particularly + // useful since a period followed by a space and a comma followed by a space are both valid, + // but a period followed by a space, followed by a dollar sign is probably not valid. The + // user who runs this check in "basic" mode will never be able to catch this kind of error. + TestGetReferences(new string[] { "._", ",_", "$_" }, new int[] { 4, 5, 6 }, "\\p \\v 1 word.,$"); + } + + [Test] + public void GetReferences_IntermediateParagraphFinalMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(".,$_", 4, "\\p \\v 1 word.,$"); + } + + [Test] + public void GetReferences_AdvancedParagraphFinalMultiple() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences(".,$_", 4, "\\p \\v 1 word.,$"); + } + + [Test] + public void GetReferences_BasicParagraphFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D"); + } + + [Test] + public void GetReferences_IntermediateParagraphFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D"); + } + + [Test] + public void GetReferences_AdvancedParagraphFinalSingle() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u201D_", 4, "\\p \\v 1 word\u201D"); + } + + [Test] + public void GetReferences_BasicFinalMutiplePeriodBefore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "._", "\u2019_", "\u201D_" }, + new int[] {4, 5, 7}, + "\\p \\v 1 word.\u2019 \u201D word"); + } + + [Test] + public void GetReferences_IntermediateFinalMutiplePeriodBefore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(".\u2019_\u201D_", 4, "\\p \\v 1 word.\u2019 \u201D word"); + } + + [Test] + public void GetReferences_AdvancedFinalMutiplePeriodBefore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences(".\u2019_\u201D_", 4, "\\p \\v 1 word.\u2019 \u201D word"); + } + + [Test] + public void GetReferences_BasicFinalMutiplePeriodAfter() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "\u2019_", "\u201D_", "._" }, + new int[] { 4, 6, 7 }, + "\\p \\v 1 word\u2019 \u201D. word"); + } + + [Test] + public void GetReferences_IntermediateFinalMutiplePeriodAfter() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("\u2019_\u201D._", 4, "\\p \\v 1 word\u2019 \u201D. word"); + } + + [Test] + public void GetReferences_AdvancedFinalMutiplePeriodAfter() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u2019_\u201D._", 4, "\\p \\v 1 word\u2019 \u201D. word"); + } + + [Test] + public void GetReferences_BasicFinalMutipleNoSpace() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "\u2019_", "\u201D_" }, + new int[] { 4, 5 }, + "\\p \\v 1 word\u2019\u201D word"); + } + + [Test] + public void GetReferences_IntermediateFinalMutipleNoSpace() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("\u2019\u201D_", 4, "\\p \\v 1 word\u2019\u201D word"); + } + + [Test] + public void GetReferences_AdvancedFinalMutipleNoSpace() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u2019\u201D_", 4, "\\p \\v 1 word\u2019\u201D word"); + } + + [Test] + public void GetReferences_BasicFinalInitialAllQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "\u2019_", "_\u201C" }, + new int[] { 4, 6 }, + "\\p \\v 1 word\u2019 \u201Cword"); + } + + [Test] + public void GetReferences_IntermediateFinalInitialAllQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "\u2019_", "_\u201C" }, + new int[] { 4, 6 }, + "\\p \\v 1 word\u2019 \u201Cword"); + } + + [Test] + public void GetReferences_AdvancedFinalInitialAllQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u2019_\u201C", 4, "\\p \\v 1 word\u2019 \u201Cword"); + } + + [Test] + public void GetReferences_IntermediateFinalInitialSomeQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "\u2019!_", "_\u201C" }, + new int[] { 4, 7 }, + "\\p \\v 1 word\u2019! \u201Cword"); + } + + [Test] + public void GetReferences_IntermediateFinalExclamationBetweenClosingQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "\u2019!_", "_\u201D" }, + new int[] { 4, 7 }, + "\\p \\v 1 word\u2019! \u201Dword"); + } + + [Test] + public void GetReferences_BasicFinalInitialSomeQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "\u2019_", "!_", "_\u201C" }, + new int[] { 4, 5, 7 }, + "\\p \\v 1 word\u2019! \u201Cword"); + } + + [Test] + public void GetReferences_AdvancedFinalInitialSomeQuotes() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("\u2019!_\u201C", 4, "\\p \\v 1 word\u2019! \u201Cword"); + } + + [Test] + public void GetReferences_BasicEllipsis() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("...", 4, "\\p \\v 1 word...word"); + } + + [Test] + public void GetReferences_IntermediateEllipsis() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("...", 4, "\\p \\v 1 word...word"); + } + + [Test] + public void GetReferences_AdvancedEllipsis() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("...", 4, "\\p \\v 1 word...word"); + } + + [Test] + public void GetReferences_BasicNumbersIgnore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "._", "._" }, + new int[] { 4, 14 }, + "\\p \\v 1 word. 3:4 word."); + } + + [Test] + public void GetReferences_IntermediateNumbersIgnore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "._", "._" }, + new int[] { 4, 14 }, + "\\p \\v 1 word. 3:4 word."); + } + + [Test] + public void GetReferences_AdvancedNumbersIgnore() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences(new string[] { "._", "._" }, + new int[] { 4, 14 }, + "\\p \\v 1 word. 3:4 word."); + } + + [Test] + public void GetReferences_BasicNumbersIgnoreNoPunctBeforeNumbers() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("._", 13, "\\p \\v 1 word 3:4 word."); + } + + [Test] + public void GetReferences_IntermediateNumbersIgnoreNoPunctBeforeNumbers() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences("._", 13, "\\p \\v 1 word 3:4 word."); + } + + [Test] + public void GetReferences_AdvancedNumbersIgnoreNoPunctNumbers() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences("._", 13, "\\p \\v 1 word 3:4 word."); + } + + [Test] + public void GetReferences_BasicFootnoteSpanning() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "._", "!_", "\u2019_", "\u201D_" }, + new int[] { 8, 4, 0, 2}, + "\\p \\v 1 text!\\f + \\fr 1:1 Note.\\f*\u2019 \u201D"); + } + + [Test] + public void GetReferences_IntermediateFootnoteSpanning() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "._", "!\u2019_\u201D_" }, + new int[] { 8, 4 }, + "\\p \\v 1 text!\\f + \\fr 1:1 Note.\\f*\u2019 \u201D"); + } + + [Test] + public void GetReferences_AdvancedFootnoteSpanning() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences(new string[] { "._", "!\u2019_\u201D_" }, + new int[] { 8, 4 }, + "\\p \\v 1 text!\\f + \\fr 1:1 Note.\\f*\u2019 \u201D"); + } + + [Test] + public void GetReferences_BasicFootnoteText() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences(new string[] { "_\u2018", "._", "\u2019_" }, + new int[] { 4, 9, 10 }, + "\\p \\v 1 text\\f + \\fr 1:1 \u2018Note.\u2019\\f* text"); + } + + [Test] + public void GetReferences_IntermediateFootnoteText() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + TestGetReferences(new string[] { "_\u2018", ".\u2019_" }, + new int[] { 4, 9 }, + "\\p \\v 1 text\\f + \\fr 1:1 \u2018Note.\u2019\\f* text"); + } + + [Test] + public void GetReferences_AdvancedFootnoteText() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Advanced"); + TestGetReferences(new string[] { "_\u2018", ".\u2019_" }, + new int[] { 4, 9 }, + "\\p \\v 1 text\\f + \\fr 1:1 \u2018Note.\u2019\\f* text"); + } + + [Test] + public void GetReferences_BasicSectionHead() + { + m_dataSource.SetParameterValue("PunctCheckLevel", "Basic"); + TestGetReferences("._", 4, @"\s text."); + } + #endregion + + #region Check tests + /// ------------------------------------------------------------------------------------ + /// + /// Tests that the Check method reports inconsistencies for invalid and unknown + /// patterns, but not for patterns which are valid. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Check_ValidPatternsAreNotReported() + { + PuncPatternsList puncPatterns = new PuncPatternsList(); + PuncPattern pattern = new PuncPattern(); + pattern.Pattern = "._"; + pattern.ContextPos = ContextPosition.WordFinal; + pattern.Status = PuncPatternStatus.Valid; + puncPatterns.Add(pattern); + pattern = new PuncPattern(); + pattern.Pattern = ","; + pattern.ContextPos = ContextPosition.WordBreaking; + pattern.Status = PuncPatternStatus.Invalid; + puncPatterns.Add(pattern); + m_dataSource.SetParameterValue("PunctuationPatterns", puncPatterns.XmlString); + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(m_dataSource); + + m_dataSource.Text = "\\p This is nice. By nice,I mean really nice!"; + + check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, "This is nice. By nice,I mean really nice!", 21, ",", "Invalid punctuation pattern"); + CheckError(1, "This is nice. By nice,I mean really nice!", 40, "!", "Unspecified use of punctuation pattern"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that the Check method reports the whole pattern for a punctuation pattern that + /// consists of more than one character (not counting spaces). + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Check_MultiCharPatterns() + { + m_dataSource.SetParameterValue("PunctuationPatterns", String.Empty); + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(m_dataSource); + + m_dataSource.Text = "\\p This _> is!?."; + + check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, "This _> is!?.", 5, "_>", "Unspecified use of punctuation pattern"); + CheckError(1, "This _> is!?.", 10, "!?.", "Unspecified use of punctuation pattern"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that the Check method reports the whole pattern for a punctuation pattern that + /// consists of nested quotation marks, where the opening and closing marks are + /// separated by a (thin no-break) space. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Check_PatternsWithSpaceSeparatedQuoteMarks() + { + PuncPatternsList puncPatterns = new PuncPatternsList(); + PuncPattern pattern = new PuncPattern(); + pattern.Pattern = ",_"; + pattern.ContextPos = ContextPosition.WordFinal; + pattern.Status = PuncPatternStatus.Valid; + puncPatterns.Add(pattern); + pattern = new PuncPattern(); + pattern.Pattern = "_\u201C"; + pattern.ContextPos = ContextPosition.WordInitial; + pattern.Status = PuncPatternStatus.Valid; + puncPatterns.Add(pattern); + pattern = new PuncPattern(); + pattern.Pattern = "_\u2018"; + pattern.ContextPos = ContextPosition.WordInitial; + pattern.Status = PuncPatternStatus.Valid; + puncPatterns.Add(pattern); + m_dataSource.SetParameterValue("PunctuationPatterns", puncPatterns.XmlString); + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(m_dataSource); + + m_dataSource.Text = "\\p Tom replied, \u201CBill said, \u2018Yes!\u2019\u202F\u201D"; + + check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, "Tom replied, \u201CBill said, \u2018Yes!\u2019\u202F\u201D", 29, "!\u2019\u202F\u201D", "Unspecified use of punctuation pattern"); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests that the Check method reports an error for a paragraph whose only character + /// is a quotation mark. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Check_ParaWithSingleQuotationMark() + { + PuncPatternsList puncPatterns = new PuncPatternsList(); + PuncPattern pattern = new PuncPattern(); + pattern.Pattern = "._"; + pattern.ContextPos = ContextPosition.WordFinal; + pattern.Status = PuncPatternStatus.Valid; + puncPatterns.Add(pattern); + m_dataSource.SetParameterValue("PunctuationPatterns", puncPatterns.XmlString); + m_dataSource.SetParameterValue("PunctCheckLevel", "Intermediate"); + + PunctuationCheck check = new PunctuationCheck(m_dataSource); + m_dataSource.Text = "\\p wow\u201D\\p \u2019"; + + check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(2)); + CheckError(0, "wow\u201D", 3, "\u201D", "Unspecified use of punctuation pattern"); + CheckError(1, "\u2019", 0, "\u2019", "Unspecified use of punctuation pattern"); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckSilUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckSilUnitTest.cs new file mode 100644 index 0000000000..667878ac9b --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckSilUnitTest.cs @@ -0,0 +1,1013 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// TE-style Unit tests for the QuotationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class QuotationCheckSilUnitTest : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + // A subset of serialized style information for seven different classes of styles + // that require capitalization: + // sentence intial styles, proper nouns, tables, lists, special, headings and titles. + // Only "Paragraph" style has the UseType set. If the UseType is not set, it will + // be set to Other by default. + string stylesInfo = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "<StyleInfo StyleName=\"Title Main\" StyleType=\"paragraph\" />" + + "
"; + + #region Initialization + /// ------------------------------------------------------------------------------------ + /// + /// Set up that happens before every test runs. + /// + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_dataSource.SetParameterValue("StylesInfo", stylesInfo); + m_check = new QuotationCheck(m_dataSource); + } + #endregion + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Quotation check when continuing through an empty paragraph followed by a + /// paragraph that only has a verse number in it and then some text. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueEmptyParaAfterEmptyVerse() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.RemoveLastLevel(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse one <>", TextType.Verse, + true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[2])); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Quotation check when the continuation quote occurs after a verse number, + /// but at the start of a paragraph. TE-8092. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueAfterVerseNumber() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.RemoveLastLevel(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks.ContinuationMark = ParagraphContinuationMark.Closing; + qMarks.ContinuationType = ParagraphContinuationType.RequireOutermost; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse <>verse two>>", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Quotation check when the continuation quote occurs after a verse number + /// and a footnote ORC, but at the start of a paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueAfterVerseNumberAndFootnote() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.RemoveLastLevel(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("verse <>", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Quotation check when we have one level and have repeat closing quotation + /// with a present continuer. TE-8093 + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatClosingWhenContinuerPresent() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.RemoveLastLevel(); + qMarks[0].Opening = "<"; + qMarks[0].Closing = ">"; + qMarks.ContinuationMark = ParagraphContinuationMark.Closing; + qMarks.ContinuationType = ParagraphContinuationType.RequireOutermost; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Para1 Para2 two>", TextType.Verse, + true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Para3 three", TextType.Verse, + true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test the Quotation check when we have one level and have repeat closing quotation + /// with a missing continuation. TE-8093 + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatClosingWhenContinuerMissing() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.RemoveLastLevel(); + qMarks[0].Opening = "<"; + qMarks[0].Closing = ">"; + qMarks.ContinuationMark = ParagraphContinuationMark.Closing; + qMarks.ContinuationType = ParagraphContinuationType.RequireOutermost; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Para1 ", TextType.Verse, + true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Para3 three", TextType.Verse, + true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[2])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("Para2")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Continues at quotation. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueAtQuotation() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks[1].Opening = "<"; + qMarks[1].Closing = ">"; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("< >>", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[5])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("qux")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Continues at quotation after paragraph. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueAtQuotationAfterParagraph() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks[1].Opening = "<"; + qMarks[1].Closing = ">"; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("< >>", TextType.Verse, + false, false, "Line1")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests when the first level quotes have identical opening and closing marks + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level1OpenAndClosingAreSameChar() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "!"; + qMarks[0].Closing = "!"; + qMarks[1].Opening = "<"; + qMarks[1].Closing = ">"; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("Intro !Paragraph very! short.", + TextType.Other, true, false, "Intro Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken(string.Empty, TextType.Other, + true, false, "Section Head")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level1_ContinuationFromLinesIntoProse is based on MAT 5:1-14 (NIV) but the marks differ + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level1_ContinuationFromLinesIntoProse_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + qMarks[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("5", TextType.ChapterNumber, + true, false, "Line1", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("3", TextType.VerseNumber, + false, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Level one, ", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("line two. ", TextType.Verse, + true, false, "Line2")); + m_dataSource.m_tokens.Add(new DummyTextToken("11", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continue ", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("14", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continue and close level one.»", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level1_ContinuationInProseEvenSpanningSectionHead is based on MAT 5:11-13 (NIV) but + /// the marks differ + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level1_ContinuationInProseEvenSpanningSectionHead_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + qMarks[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("5", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("11", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Level one begins", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("really it continues. ", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("", TextType.Other, + true, false, "Section Head")); + m_dataSource.m_tokens.Add(new DummyTextToken("13", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continue level one.»", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level1_ContinuationIntoFourLines2 is based on MAT 2:5-7 (NIV) but the marks differ + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level1_ContinuationIntoLines2_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + qMarks[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("5", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("5", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Level one,» they replied, «level one:", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("6", TextType.VerseNumber, + true, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("« \u2039Level two,", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("line two, ", TextType.Verse, + true, false, "Line2")); + m_dataSource.m_tokens.Add(new DummyTextToken("line one", TextType.Verse, + true, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("both levels end.\u203A »", TextType.Verse, + true, false, "Line2")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Tests the case when there is only one continuer when there should be two (i.e. one + /// for each open level) and that continuer is closing when it should be opening. + /// See TE-8173. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level2_IncorrectContinuation() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + qMarks[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("«Level one, they replied, \u2039level two", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("»Paragraph two\u203A, the end»", + TextType.Verse, true, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(3)); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("»")); + Assert.That(m_errors[1].Tts.Text, Is.EqualTo("\u203A")); + Assert.That(m_errors[2].Tts.Text, Is.EqualTo("»")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level2_ContinuationContainsLevel3_Recycled is based on EZK 27:1-12 (NIV) + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level2_ContinuationContainsLevel3_Recycled_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + qMarks[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("27", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CLevel one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Say, \u2018Level two says: ", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("4", TextType.VerseNumber, + true, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201C \u2018Continuation says,", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CLevel three.\u201D ", TextType.Verse, + true, false, "Line2")); + m_dataSource.m_tokens.Add(new DummyTextToken(string.Empty, TextType.Other, + true, false, "Stanza Break")); + m_dataSource.m_tokens.Add(new DummyTextToken("10", TextType.VerseNumber, + true, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201C \u2018Continuation.", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201C \u2018Continuation into prose, and then into lines again.\u2019 \u201D", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level2_ContinuationContainsLevel3_Distinct is based on EZK 27:1-12 (NIV) but + /// the marks differ + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level2_ContinuationContainsLevel3_Distinct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u201C"; // Left double quotation mark + qMarks[1].Closing = "\u201D"; // Right double quotation mark + qMarks[2].Opening = "\u2018"; // Left single quotation mark + qMarks[2].Closing = "\u2019"; // Right single quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("27", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("2", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Level one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("3", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Say, \u201CLevel two says:", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("4", TextType.VerseNumber, + true, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("« \u201CContinuation says,", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u2018Level three.\u2019", TextType.Verse, + true, false, "Line2")); + m_dataSource.m_tokens.Add(new DummyTextToken(string.Empty, TextType.Other, + true, false, "Stanza Break")); + m_dataSource.m_tokens.Add(new DummyTextToken("10", TextType.VerseNumber, + true, false, "Line1", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("« \u201CContinuation.", TextType.Verse, + false, false, "Line1")); + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("« \u201CContinuation into prose, and then into lines again.\u201D »", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Test when the 2nd level quotes have identical opening and closing marks + /// (TE-8104) + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level2OpenAndClosingAreSameChar() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "<"; + qMarks[0].Closing = ">"; + qMarks[1].Opening = "!"; + qMarks[1].Closing = "!"; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("Intro + /// Level3_Distinct is based on LUK 12:16-21 (NIV) but the marks differ + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Distinct_Continuation_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u201C"; // Left double quotation mark + qMarks[1].Closing = "\u201D"; // Right double quotation mark + qMarks[2].Opening = "\u2018"; // Left single quotation mark + qMarks[2].Closing = "\u2019"; // Right single quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: «Level one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u201Clevel two.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, \u2018Level three.\u2019 \u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation, and then close level one.»", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Distinct_Continuation_UnmatchedOpeningMark() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u201C"; // Left double quotation mark + qMarks[1].Closing = "\u201D"; // Right double quotation mark + qMarks[2].Opening = "\u2018"; // Left single quotation mark + qMarks[2].Closing = "\u2019"; // Right single quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: «Level one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u201Clevel two.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, \u2018Level three.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation, and then close level one.»", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[8])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("\u2018")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Distinct_Continuation_UnmatchedClosingMark() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "«"; // Left-pointing double angle quotation mark + qMarks[0].Closing = "»"; // Right-pointing double angle quotation mark + qMarks[1].Opening = "\u201C"; // Left double quotation mark + qMarks[1].Closing = "\u201D"; // Right double quotation mark + qMarks[2].Opening = "\u2018"; // Left single quotation mark + qMarks[2].Closing = "\u2019"; // Right single quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: «Level one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u201Clevel two.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, Level three.\u2019 \u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation \u201CTwo.\u201D", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("«Continuation, and then close level one.»", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[8])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("\u2019")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level3_Recycled is based on LUK 12:16-21 (NIV) + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Recycled_Continuation_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "\u201C"; // Left double quotation mark + qMarks[0].Closing = "\u201D"; // Right double quotation mark + qMarks[1].Opening = "\u2018"; // Left single quotation mark + qMarks[1].Closing = "\u2019"; // Right single quotation mark + qMarks[2].Opening = "\u201C"; // Left double quotation mark + qMarks[2].Closing = "\u201D"; // Right double quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: \u201CLevel one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u2018Level two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation \u2018Two.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, \u201Clevel three.\u201D \u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation \u2018Two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation, and then close level one.\u201D", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Recycled_Continuation_UnmatchedOpeningMark() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "\u201C"; // Left double quotation mark + qMarks[0].Closing = "\u201D"; // Right double quotation mark + qMarks[1].Opening = "\u2018"; // Left single quotation mark + qMarks[1].Closing = "\u2019"; // Right single quotation mark + qMarks[2].Opening = "\u201C"; // Left double quotation mark + qMarks[2].Closing = "\u201D"; // Right double quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: \u201CLevel one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u2018Level two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation \u2018Two.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, \u201Clevel three.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation \u2018Two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CContinuation, and then close level one.\u201D", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[8])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("\u201C")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level3_Recycled_UnmatchedOpeningMark() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(3); + qMarks[0].Opening = "\u201C"; // Left double quotation mark + qMarks[0].Closing = "\u201D"; // Right double quotation mark + qMarks[1].Opening = "\u2018"; // Left single quotation mark + qMarks[1].Closing = "\u2019"; // Right single quotation mark + qMarks[2].Opening = "\u201C"; // Left double quotation mark + qMarks[2].Closing = "\u201D"; // Right double quotation mark + qMarks.ContinuationType = ParagraphContinuationType.None; + qMarks.ContinuationMark = ParagraphContinuationMark.None; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("12", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("16", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He told: \u201CLevel one.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("17", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("He thought, \u2018Level two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("18", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u2018Two.", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("19", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To myself, \u201Clevel three.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("20", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u2018Two.\u2019", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("21", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("Close level one.\u201D", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[8])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("\u201C")); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Level4_Recycled is based on JER 29:24-28 (NASB) + /// Inserted \p in verse 28 to account for continuation, but that does not seem + /// quite right, because NASB has verse structure, instead of paragraph structure. + /// The main paragraphs are at verses 24 and 29. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void Level4_Recycled_Continuation_Correct() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks.EnsureLevelExists(4); + qMarks[0].Opening = "\u201C"; // Left double quotation mark + qMarks[0].Closing = "\u201D"; // Right double quotation mark + qMarks[1].Opening = "\u2018"; // Left single quotation mark + qMarks[1].Closing = "\u2019"; // Right single quotation mark + qMarks[2].Opening = "\u201C"; // Left double quotation mark + qMarks[2].Closing = "\u201D"; // Right double quotation mark + qMarks[3].Opening = "\u2018"; // Left single quotation mark + qMarks[3].Closing = "\u2019"; // Right single quotation mark + qMarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + + m_dataSource.m_tokens.Add(new DummyTextToken("29", TextType.ChapterNumber, + true, false, "Paragraph", "Chapter Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("24", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("To Shemiah speak, saying,", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("25", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201Clevel one, \u2018Level two,", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("26", TextType.VerseNumber, + false, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201Clevel three", TextType.Verse, + false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("28", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("\u201CHe has sent saying, \u2018Level four.\u2019\u201D\u2019\u201D", TextType.Verse, + false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// + /// + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void VerboseOptionContinuers() + { + QuotationMarksList qMarks = QuotationMarksList.NewList(); + qMarks[0].Opening = "<<"; + qMarks[0].Closing = ">>"; + qMarks[1].Opening = "<"; + qMarks[1].Closing = ">"; + qMarks.ContinuationType = ParagraphContinuationType.RequireAll; + qMarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_dataSource.SetParameterValue("QuotationMarkInfo", qMarks.XmlString); + m_dataSource.SetParameterValue("VerboseQuotes", "Yes"); + + m_dataSource.m_tokens.Add(new DummyTextToken("1", TextType.VerseNumber, + true, false, "Paragraph", "Verse Number")); + m_dataSource.m_tokens.Add(new DummyTextToken("< >>", TextType.Verse, + false, false, "Line1")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(6)); + Assert.That(m_errors[0].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[1])); + Assert.That(m_errors[0].Tts.Text, Is.EqualTo("<<")); + Assert.That(m_errors[1].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[1])); + Assert.That(m_errors[1].Tts.Text, Is.EqualTo("<")); + Assert.That(m_errors[2].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[3])); + Assert.That(m_errors[2].Tts.Text, Is.EqualTo("<<")); + Assert.That(m_errors[3].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[3])); + Assert.That(m_errors[3].Tts.Text, Is.EqualTo("<")); + Assert.That(m_errors[4].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[5])); + Assert.That(m_errors[4].Tts.Text, Is.EqualTo(">")); + Assert.That(m_errors[5].Tts.FirstToken, Is.EqualTo(m_dataSource.m_tokens[5])); + Assert.That(m_errors[5].Tts.Text, Is.EqualTo(">>")); + } + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckUnitTest.cs new file mode 100644 index 0000000000..61351a4dfe --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/QuotationCheckUnitTest.cs @@ -0,0 +1,1301 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; +using SILUBS.ScriptureChecks; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// + /// Unit tests for the QuotationCheck class + /// + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class QuotationCheckUnitTest + { + public const string kUnmatchedOpeningMark = "Unmatched opening mark: level {0}"; + public const string kUnmatchedClosingMark = "Unmatched closing mark: level {0}"; + public const string kMissingContinuationMark = "Missing continuation mark: level {0}"; + public const string kMissingContinuationMarks = "Missing continuation marks: levels 1-{0}"; + public const string kUnexpectedOpeningMark = "Unexpected opening mark: level {0}"; + + public const string kVerboseQuoteOpened = "Level {0} quote opened"; + public const string kVerboseQuoteClosed = "Level {0} quote closed"; + public const string kVerboseQuoteContinuer = "Level {0} quote continuer"; + + UnitTestChecksDataSource m_source; + + private QuotationMarksList m_qmarks; + + #region Test setup + [OneTimeSetUp] + public void TestSetup() + { + m_source = new UnitTestChecksDataSource(); + + string stylesInfo = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "</StylePropsInfo>"; + + m_source.SetParameterValue("StylesInfo", stylesInfo); + } + + [SetUp] + public void RunBeforeEachTest() + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.QMarksList[0].Opening = "<<"; + m_qmarks.QMarksList[0].Closing = ">>"; + m_qmarks.QMarksList[1].Opening = "<"; + m_qmarks.QMarksList[1].Closing = ">"; + m_qmarks.EnsureLevelExists(5); + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + #endregion + + #region Helper methods + public static string FormatMessage(string format, int level) + { + return string.Format(format, level); + } + + void Test(string[,] result, string text) + { + m_source.Text = text; + + QuotationCheck check = new QuotationCheck(m_source); + List<TextTokenSubstring> tts = + check.GetReferences(m_source.TextTokens(), ""); + + for (int i = 0; i < tts.Count; i++) + { + Console.WriteLine(tts[i].Text); + Console.WriteLine(tts[i].Message); + Debug.WriteLine(tts[i].Text); + Debug.WriteLine(tts[i].Message); + } + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0) + 1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + { + // Verify the Reference, Message, and Details columns of the results pane. + // Verifies empty string, but not null, for the reference (for original tests). + if (result.GetUpperBound(1) == 2) + Assert.That(tts[i].FirstToken.ScrRefString, Is.EqualTo(result[i, 2]), "Reference number: " + i); + + Assert.That(tts[i].Text, Is.EqualTo(result[i, 0]), "Text number: " + i.ToString()); + Assert.That(tts[i].Message, Is.EqualTo(result[i, 1]), "Message number: " + i.ToString()); + } + } + + #endregion + + #region Top-level quotes + [Test] + public void TopLevelNoCloser() + { + m_qmarks.QMarksList[0].Closing = string.Empty; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + Test(new string[0,0], @"\p \v 1 <<foo \v 2 <<foo"); + } + + [Test] + public void TopLevel() + { + Test(new string[,] { + { ">>", FormatMessage(kUnmatchedClosingMark, 1) }, + { "<<", FormatMessage(kUnmatchedOpeningMark, 1) }, + }, @"\p \v 1 <<foo bar>> qux>> <<quux"); + } + + [Test] + public void TopLevelMissingClosing() + { + Test(new string[,] { + { "<<", FormatMessage(kUnmatchedOpeningMark, 1) }, + }, @"\p \v 1 <<foo"); + } + + [Test] + public void TopLevelMissingOpening() + { + Test(new string[,] { + { ">>", FormatMessage(kUnmatchedClosingMark, 1) }, + }, @"\p \v 1 foo>>"); + } + + [Test] + public void OtherQuotesAndParameters() + { + m_qmarks.QMarksList[0].Opening = "\u201C"; + m_qmarks.QMarksList[0].Closing = "\u201D"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + Test(new string[,] { + { "\u201C", FormatMessage(kUnmatchedOpeningMark, 1) }, + }, "\\p \\v 1 \u201Cfoo"); + } + #endregion + + #region Inner level quotes + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Test this scenario: + /// When {< >} are inner quotes and not followed by anything... The inner open is used + /// and not followed by other quotes. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void InnerLevel_ClosingAndTopLevelMissing() + { + Test(new string[,] { + { "<", FormatMessage(kUnexpectedOpeningMark, 2) }, + { "<", FormatMessage(kUnmatchedOpeningMark, 2) }, + }, @"\p \v 1 <foo"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Test this scenario: + /// When {< >} are inner quotes... The inner open is used and followed by valid outer quotes. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void InnerLevel_ClosingAndTopLevelAfter() + { + Test(new string[,] { + { "<", FormatMessage(kUnexpectedOpeningMark, 2) }, + { "<", FormatMessage(kUnmatchedOpeningMark, 2) }, + }, @"\p \v 1 <foo <<foofoo>>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Test this scenario: + /// When {< >} are inner quotes... The inner open is used and followed by valid outer + /// left and right double turned quotes. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void InnerLevel_ClosingAndTopLevelAfter2() + { + m_qmarks.QMarksList[0].Opening = "\u201C"; + m_qmarks.QMarksList[0].Closing = "\u201D"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + Test(new string[,] { + { "<", FormatMessage(kUnexpectedOpeningMark, 2) }, + { "<", FormatMessage(kUnmatchedOpeningMark, 2) } + }, @"\p \v 1 <foo \u201Cfoofoo\u201D"); + } + + [Test] + public void Inner() + { + Test(new string[0,0], + @"\p \v 1 <<foo <bar> baz>>"); + } + + [Test] + public void InnerMissingClosing() + { + Test(new string[,] { + { "<", FormatMessage(kUnmatchedOpeningMark, 2) }, + }, @"\p \v 1 <<foo <bar baz>>"); + } + + [Test] + public void InnerMissingOpening() + { + Test(new string[,] { + { ">", FormatMessage(kUnmatchedClosingMark, 2) }, + }, @"\p \v 1 <<foo bar> baz>>"); + } + #endregion + + #region Third level quotes - repeated level 1 as level 3 + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Thirds the level embedding. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ThirdLevelEmbedding() + { + Test(new string[0,0], + @"\p \v 1 <<foo <bar <<baz>> qux> quux>>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Inners the inner missing closing. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void InnerInnerMissingClosing() + { + Test(new string[,] { + { "<<", FormatMessage(kUnmatchedOpeningMark, 3) }, + }, @"\p \v 1 <<foo <bar <<baz qux> quux>>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Inners the inner missing opening. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void InnerInnerMissingOpening() + { + m_qmarks.Clear(); + m_qmarks.EnsureLevelExists(3); + m_qmarks[0].Opening = "<<<"; + m_qmarks[0].Closing = ">>>"; + m_qmarks[1].Opening = "<<"; + m_qmarks[1].Closing = ">>"; + m_qmarks[2].Opening = "<"; + m_qmarks[2].Closing = ">"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[,] { + { ">", FormatMessage(kUnmatchedClosingMark, 3) }, + }, @"\p \v 1 <<<foo <<bar baz> qux>> quux>>>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Others the quotes. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void OtherQuotes() + { + Test(new string[0, 0], @"<<foo <bar <<baz>> qux> quux>>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Threes the distinct levels. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ThreeDistinctLevels() + { + m_qmarks.AddLevel(); + m_qmarks.QMarksList[2].Opening = "["; + m_qmarks.QMarksList[2].Closing = "]"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + Test(new string[0, 0], @"<<foo <bar [baz] qux> quux>>"); + } + + #endregion + + #region Quote continuation + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueNoMarkAllLevels() + { + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 baz bazs\p qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatInnerMostOpening() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 <baz bazs qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatAllOpening() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 << <baz bazs qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatInnerMostClosing() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Closing; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 >baz bazs qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatAllClosing() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Closing; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 >> >baz bazs qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatAllClosing_3Levels() + { + m_qmarks.EnsureLevelExists(3); + m_qmarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Closing; + m_qmarks[2].Opening = "["; + m_qmarks[2].Closing = "]"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar [bars \q1 >> > ]baz bazs] qux quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatInnerMostOpening_3Levels() + { + m_qmarks.EnsureLevelExists(3); + m_qmarks[2].Opening = "["; + m_qmarks[2].Closing = "]"; + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], + @"\p <<foo foos <bar bars \q1 <baz [bazs qux] quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatInnerMostOpeningMissing() + { + m_qmarks.EnsureLevelExists(3); + m_qmarks[2].Opening = "["; + m_qmarks[2].Closing = "]"; + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[,] { + { "baz", FormatMessage(kMissingContinuationMark, 2) }, + }, @"\p <<foo foos <bar bars \q1 baz [bazs qux] quxs> >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ContinueRepeatInnerMostClosingMissing() + { + m_qmarks.EnsureLevelExists(3); + m_qmarks[2].Opening = "["; + m_qmarks[2].Closing = "]"; + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Closing; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[,] { + { "baz", FormatMessage(kMissingContinuationMark, 2) }, + }, @"\p <<foo foos <bar bars \q1 baz [bazs qux] quxs> >>"); + } + + #endregion + + #region Deeply Nested Quotes + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Deeplies the nested quotes. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void DeeplyNestedQuotes() + { + m_qmarks.EnsureLevelExists(5); + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[0, 0], @"\p \v 1 << < <<foo < <<bar baz qux>> > quux>> > >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Deeplies the nested quotes missing closer. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void DeeplyNestedQuotesMissingCloser() + { + m_qmarks.EnsureLevelExists(5); + m_qmarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[,] { + { "<<", FormatMessage(kUnmatchedOpeningMark, 5) }, + }, @"\p \v 1 << < <<foo < <<bar baz qux > quux>> > >>"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Deeplies the nested quotes no nesting. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void ToManyLevels() + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.QMarksList[0].Opening = "<<"; + m_qmarks.QMarksList[0].Closing = ">>"; + m_qmarks.QMarksList[1].Opening = "<"; + m_qmarks.QMarksList[1].Closing = ">"; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + + Test(new string[,] { + { "<", FormatMessage(kUnmatchedOpeningMark, 2) }, + { "<<", FormatMessage(kUnmatchedOpeningMark, 1) }, + { ">", FormatMessage(kUnmatchedClosingMark, 2) }, + { ">>", FormatMessage(kUnmatchedClosingMark, 1) }, + }, @"\p \v 1 << <foo <<bar baz>> qux> quux>>"); + + Test(new string[,] { + { "<", FormatMessage(kUnmatchedOpeningMark, 2) }, + { ">", FormatMessage(kUnmatchedClosingMark, 2) }, + }, @"\p \v 1 << <foo <bar baz> qux> quux>>"); + } + + #endregion + + #region Tests for verbose option + [Test] + public void VerboseOptionNormal() + { + m_source.SetParameterValue("VerboseQuotes", "Yes"); + Test(new string[,] { + { "<<", FormatMessage(kVerboseQuoteOpened, 1) }, + { "<", FormatMessage(kVerboseQuoteOpened, 2) }, + { ">", FormatMessage(kVerboseQuoteClosed, 2) }, + { ">>", FormatMessage(kVerboseQuoteClosed, 1) }, + }, @"\p \v 1 <<foo <foo> >>"); + } + #endregion + + // The tests verify the inconsistencies found in various combinations of: + // Correct or incorrect quotation marks + // Appropriate or inappropriate writing system properties + + #region New test setup of quotation marks for levels + + void SetupEnglish1() + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.RemoveLastLevel(); + m_qmarks.QMarksList[0].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[0].Closing = "\u201D"; // Right double quotation mark + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupEnglish2() + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.EnsureLevelExists(2); + m_qmarks.QMarksList[0].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[0].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[1].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[1].Closing = "\u2019"; // Right single quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupEnglish3() // TO DO: Is there a way to limit it? + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.EnsureLevelExists(3); + m_qmarks.QMarksList[0].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[0].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[1].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[1].Closing = "\u2019"; // Right single quotation mark + m_qmarks.QMarksList[2].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[2].Closing = "\u201D"; // Right double quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupEnglish4() + { + m_qmarks = QuotationMarksList.NewList(); + m_qmarks.EnsureLevelExists(4); + m_qmarks.QMarksList[0].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[0].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[1].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[1].Closing = "\u2019"; // Right single quotation mark + m_qmarks.QMarksList[2].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[2].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[3].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[3].Closing = "\u2019"; // Right single quotation mark + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupEuropean1() + { + m_qmarks.RemoveLastLevel(); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupPortuguese2() + { + m_qmarks.EnsureLevelExists(2); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[1].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[1].Closing = "»"; // Right-pointing double angle quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupPortuguese3() + { + // A Boa Nova, Good News for Modern Man in Portuguese: + // * Uses the same marks for levels one, two, and three (which is rare). + // * Instead of continuing marks, closes, and then reopens quotations + // at section heads (for example, in MAT 5-7). + m_qmarks.EnsureLevelExists(3); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[1].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[1].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[2].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[2].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupSpanish2() + { + // Spanish has a distinct mark for level 3. + m_qmarks.EnsureLevelExists(2); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[1].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[1].Closing = "\u201D"; // Right double quotation mark + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + //m_source.SetParameterValue("ContinueQuotes", "Yes"); + //m_source.SetParameterValue("ContinueInnerQuotes", "Yes"); + //m_source.SetParameterValue("NestingAlternates", "No"); + } + + void SetupSpanish2_QuotationDash() + { + // Spanish has a distinct mark for level 3. + m_qmarks.EnsureLevelExists(2); + m_qmarks.QMarksList[0].Opening = "\u2014"; // Em dash + m_qmarks.QMarksList[0].Closing = string.Empty; + m_qmarks.QMarksList[1].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[1].Closing = "\u201D"; // Right double quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupSpanish3() + { + // Spanish has a distinct mark for level 3. + m_qmarks.EnsureLevelExists(3); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[1].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[1].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[2].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[2].Closing = "\u2019"; // Right single quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupSpanish3_QuotationDash() + { + // Spanish has a distinct mark for level 3. + m_qmarks.EnsureLevelExists(3); + m_qmarks.QMarksList[0].Opening = "\u2014"; // Em dash + m_qmarks.QMarksList[0].Closing = string.Empty; + m_qmarks.QMarksList[1].Opening = "\u201C"; // Left double quotation mark + m_qmarks.QMarksList[1].Closing = "\u201D"; // Right double quotation mark + m_qmarks.QMarksList[2].Opening = "\u2018"; // Left single quotation mark + m_qmarks.QMarksList[2].Closing = "\u2019"; // Right single quotation mark + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupSwiss2() // for French, German, or Italian + { + m_qmarks.EnsureLevelExists(2); + m_qmarks.QMarksList[0].Opening = "«"; // Left-pointing double angle quotation mark + m_qmarks.QMarksList[0].Closing = "»"; // Right-pointing double angle quotation mark + m_qmarks.QMarksList[1].Opening = "\u2039"; // Single left-pointing angle quotation mark + m_qmarks.QMarksList[1].Closing = "\u203A"; // Single right-pointing angle quotation mark + m_qmarks.ContinuationType = ParagraphContinuationType.None; + m_qmarks.ContinuationMark = ParagraphContinuationMark.None; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + #endregion + + #region New test setup for quotation continuation across paragraphs + + void SetupContinuationAllOpening() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireAll; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + + void SetupContinuationInnermost() + { + m_qmarks.ContinuationType = ParagraphContinuationType.RequireInnermost; + m_qmarks.ContinuationMark = ParagraphContinuationMark.Opening; + m_source.SetParameterValue("QuotationMarkInfo", m_qmarks.XmlString); + } + #endregion + + #region Level 1 quotation marks + + // Level1_OnePair is based on MAT 2:2 (NIV and especially NLT) but the marks differ + + [Test] + public void Level1_OnePair_European_Correct() + { + SetupEuropean1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 «Level one.»"); + } + + [Test] + public void Level1_OnePair_European_UnmatchedOpeningMark() + { + SetupEuropean1(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "2:2" } + }, "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 «Level one."); + } + + [Test] + public void Level1_OnePair_European_UnmatchedClosingMark() + { + SetupEuropean1(); + Test(new string[,] { + { "»", FormatMessage(kUnmatchedClosingMark, 1), "2:2" } + }, "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 Level one.»"); + } + + // The check might not find inconsistencies if checking properties are inappropriate + // or if the character is not a quotation mark for the writing system. + + [Test] + public void Level1_OnePair_Inappropriate_Correct() + { + SetupEnglish1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 «Level one.»"); + } + + [Test] + public void Level1_OnePair_Inappropriate_UnmatchedOpeningMark() + { + SetupEnglish1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 «Level one."); + } + + [Test] + public void Level1_OnePair_Inappropriate_UnmatchedClosingMark() + { + SetupEnglish1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 Level one.»"); + } + + // Same marks as NIV and NLT + + [Test] + public void Level1_OnePair_English_Correct() + { + SetupEnglish1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 \u201CLevel one.\u201D"); + } + + [Test] + public void Level1_OnePair_English_UnmatchedOpeningMark() + { + SetupEnglish1(); + Test(new string[,] { + { "\u201C", FormatMessage(kUnmatchedOpeningMark, 1), "2:2" } + }, "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 \u201CLevel one."); + } + + [Test] + public void Level1_OnePair_English_UnmatchedClosingMark() + { + SetupEnglish1(); + Test(new string[,] { + { "\u201D", FormatMessage(kUnmatchedClosingMark, 1), "2:2" } + }, "\\id MAT \\c 2 \\p \\v 1 asking, \\v 2 Level one.\u201D"); + } + + // Level1_TwoPairs is based on MAT 2:13 (NIV and especially NLT) but the marks differ + + [Test] + public void Level1_TwoPairs_European_Correct() + { + SetupEuropean1(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 13 in a dream. «Level one,» the angel said, «level one.»"); + } + + [Test] + public void Level1_TwoPairs_European_UnmatchedOpeningMark() + { + SetupEuropean1(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "2:13" } + }, "\\id MAT \\c 2 \\p \\v 13 in a dream. «Level one, the angel said, «level one.»"); + } + + [Test] + public void Level1_TwoPairs_European_UnmatchedClosingMark() + { + SetupEuropean1(); + Test(new string[,] { + { "»", FormatMessage(kUnmatchedClosingMark, 1), "2:13" } + }, "\\id MAT \\c 2 \\p \\v 13 in a dream. «Level one,» the angel said, level one.»"); + } + + // Level1_ThreePairs is based on MAT 8:3-4 (NIV and NLT) but the marks differ + + [Test] + public void Level1_ThreePairs_European_Correct() + { + SetupEuropean1(); + Test(new string[0, 0], "\\id MAT \\c 8 \\p \\v 3 «Level one,» he said. «level one.» \\v 4 Then said to him, «level one.»"); + } + + [Test] + public void Level1_ThreePairs_European_UnmatchedClosingAndOpeningMarks() + { + SetupEuropean1(); + Test(new string[,] { + { "»", FormatMessage(kUnmatchedClosingMark, 1), "8:3" }, + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "8:4" } + }, "\\id MAT \\c 8 \\p \\v 3 «Level one,» he said. level one.» \\v 4 Then said to him, «level one."); + } + #endregion + + #region Level 1 quotation marks in section headings + + // Level1_HebrewTitle is based on PSA 9:1 (NIV and NLT) but the marks differ + // Does the Test code recognize \d Hebrew Title? + + [Test] + public void Level1_HebrewTitle_European_Correct() + { + SetupEuropean1(); + Test(new string[0, 0], "\\id PSA \\c 9 \\d to the tune of «Level one.» \\q1 \\v 1 Line one; \\q2 Line two."); + } + + [Test] + public void Level1_HebrewTitle_European_UnmatchedOpeningMark() + { + SetupEuropean1(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "9:0" } // Does the check know that the reference is verse 1? + }, "\\id PSA \\c 9 \\d to the tune of «Level one. \\q1 \\v 1 Line one; \\q2 Line two."); + } + + [Test] + public void Level1_HebrewTitle_European_UnmatchedClosingMark() + { + SetupEuropean1(); + Test(new string[,] { + { "»", FormatMessage(kUnmatchedClosingMark, 1), "9:0" } // Does the check know that the reference is verse 1? + }, "\\id PSA \\c 9 \\d to the tune of Level one.» \\q1 \\v 1 Line one; \\q2 Line two."); + } + + // Level1_SectionHead is based on 1CO 1:10 (J.B. Phillips) but the marks differ + // Does the Test code recognize \s Section Head? + + [Test] + public void Level1_SectionHead_European_Correct() + { + SetupEuropean1(); + Test(new string[0, 0], "\\id 1CO \\c 1 \\v 9 \\s But I am anxious over your «divisions» \\p \\v 10 Now I do beg you."); + } + + [Test] + public void Level1_SectionHead_European_UnmatchedOpeningMark() + { + SetupEuropean1(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "1:9" } // Does the check know that the reference is verse 10? + }, "\\id 1CO \\c 1 \\v 9 \\s But I am anxious over your «divisions \\p \\v 10 Now I do beg you."); + } + + [Test] + public void Level1_SectionHead_European_UnmatchedClosingMark() + { + SetupEuropean1(); + Test(new string[,] { + { "»", FormatMessage(kUnmatchedClosingMark, 1), "1:9" } // Does the check know that the reference is verse 10? + }, "\\id 1CO \\c 1 \\v 9 \\s But I am anxious over your divisions» \\p \\v 10 Now I do beg you."); + } + #endregion + + #region Level 2 quotation marks + + // Level2_OnePair is based on MAT 5:38-39 (NIV) but the marks differ + + [Test] + public void Level2_OnePair_Correct() + { + // In the actual context, the level 1 quotation continues preceding and following. + SetupSwiss2(); + Test(new string[0, 0], "\\id MAT \\c 5 \\p \\v 38 «Level one says, \u2039Level two, and level two.\u203A \\v 39 But, Level one.»"); + } + + [Test] + public void Level2_OnePair_UnmatchedOpeningMark() + { + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnmatchedOpeningMark, 2), "5:38" } + }, "\\id MAT \\c 5 \\p \\v 38 «Level one says, \u2039Level two, and level two. \\v 39 But, Level one.»"); + } + + [Test] + public void Level2_OnePair_UnmatchedClosingMark() + { + SetupSwiss2(); + Test(new string[,] { + { "\u203A", FormatMessage(kUnmatchedClosingMark, 2), "5:38" } + }, "\\id MAT \\c 5 \\p \\v 38 «Level one says, Level two, and level two.\u203A \\v 39 But, Level one.»"); + } + + [Test] + public void Level2_OnePair_UnexpectedAndUnmatchedOpeningMark() + { + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnexpectedOpeningMark, 2), "5:38" }, + { "\u2039", FormatMessage(kUnmatchedOpeningMark, 2), "5:38" } + }, "\\id MAT \\c 5 \\p \\v 38 Level one says, \u2039Level two, and level two. \\v 39 But, Level one."); + } + + // Level2_OnePairFollowedByLevel1 is based on MAT 9:12-14 (NLT) but the marks differ + + [Test] + public void Level2_OnePairFollowedByLevel1_UnexpectedOpeningMark() + { + // The first first-level quotation is marked with characters that are not quotation marks for the writing system. + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnexpectedOpeningMark, 2), "9:13" } + }, "\\id MAT \\c 9 \\p \\v 12 \\v 13 He added, \u201CLevel one \u2039Level two.\u203A Level one.\u201D \\p \\v 14 Asked him, «Level one.»"); + } + + // Level2_ThreePairs is based on MAT 8:8-9 (NLT) but the marks differ + + [Test] + public void Level2_ThreePairs_UnexpectedOpeningMark() + { + // Might happen if you do not notice that first-level marks were missing + // when you inserted second-level marks. + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnexpectedOpeningMark, 2), "8:9" }, + }, "\\id MAT \\c 8 \\p \\v 8 Said, Level one. \\v 9 I say \u2039Two,\u203A and they go, or \u2039Two,\u203A and they come. If I say, \u2039Two,\u203A they do it.»"); + } + + [Test] + public void Level2_ThreePairs_UnexpectedOpeningMark_InappropriateProperties() + { + // Might happen after you imported from source with different first-level marks, + // which the check does not notice until you insert second-level marks + // where they did not occur in the source. + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnexpectedOpeningMark, 2), "8:9" } + }, "\\id MAT \\c 8 \\p \\v 8 Said, \u201CLevel one. \\v 9 I say \u2039Two,\u203A and they go, or \u2039Two,\u203A and they come. If I say, \u2039Two,\u203A they do it.\u201D"); + } + + // Level2_TwoPairs is based on MAT 5:33-37 (A Boa Nova, Portuguese Good News) + + [Test] + public void Level2_TwoPairs_Correct() + { + // In the actual context, the level 1 quotation continues preceding and following. + SetupPortuguese2(); + Test(new string[0, 0], "\\id MAT \\c 5 \\p \\v 33 «Level one says: In italics not quotation marks. \\v 37 Let your «Yes» and «No» level one.»"); + } + + // Level2_FivePairs is based on MAT 5:33-37 (NIV) but the marks differ + + [Test] + public void Level2_FivePairs_Correct() + { + // In the actual context, the level 1 quotation continues preceding and following. + SetupSwiss2(); + Test(new string[0, 0], "\\id MAT \\c 5 \\p \\v 33 «Level one says, \u2039Level two.\u203A \\v 37 Let your \u2039Yes\u203A be \u2039Yes\u203A and your \u2039No,\u203A \u2039No\u203A; level one.»"); + } + + [Test] + public void Level2_FivePairs_UnmatchedOpeningMark() + { + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnmatchedOpeningMark, 2), "5:37" } + }, "\\id MAT \\c 5 \\p \\v 33 «Level one says, \u2039Level two.\u203A \\v 37 Let your \u2039Yes be \u2039Yes\u203A and your \u2039No,\u203A \u2039No\u203A; level one.»"); + } + + [Test] + public void Level2_FivePairs_UnmatchedClosingMark() + { + SetupSwiss2(); + Test(new string[,] { + { "\u203A", FormatMessage(kUnmatchedClosingMark, 2), "5:37" } + }, "\\id MAT \\c 5 \\p \\v 33 «Level one says, \u2039Level two.\u203A \\v 37 Let your \u2039Yes\u203A be Yes\u203A and your \u2039No,\u203A \u2039No\u203A; level one.»"); + } + + [Test] + public void Level2_FivePairs_UnmatchedOpeningAndClosingMarks() + { + SetupSwiss2(); + Test(new string[,] { + { "\u2039", FormatMessage(kUnmatchedOpeningMark, 2), "5:37" }, + { "\u203A", FormatMessage(kUnmatchedClosingMark, 2), "5:37" } + }, "\\id MAT \\c 5 \\p \\v 33 «Level one says, \u2039Level two.\u203A \\v 37 Let your \u2039Yes be \u2039Yes\u203A and your \u2039No,\u203A No\u203A; level one.»"); + } + + [Test] + public void Level2_FivePairs_UnmatchedClosingAndOpeningMarks() + { + SetupSwiss2(); + Test(new string[,] { + { "\u203A", FormatMessage(kUnmatchedClosingMark, 2), "5:37" }, + { "\u2039", FormatMessage(kUnmatchedOpeningMark, 2), "5:37" } + }, "\\id MAT \\c 5 \\p \\v 33 «Level one says, \u2039Level two.\u203A \\v 37 Let your \u2039Yes\u203A be Yes\u203A and your \u2039No, \u2039No\u203A; level one.»"); + } + #endregion + + #region Same marks for multiple levels + + // Level2_QuotedText is based on MAT 5:33-37 (A Boa Nova) + + [Test] + public void Level2_QuotedText_Correct() + { + SetupPortuguese3(); + Test(new string[0, 0], "\\id MAT \\c 5 \\p \\v 33 «Level one was said: \\qt Quoted text instead of level two. \\qt* But I say: What I say. \\v 37 Let your «yes» be yes, and your «no» no.»"); + } + + // Level2_SameMarks is based on MAT 7:1-6 (A Boa Nova) + + [Test] + public void Level2_SameMarks_Correct() + { + SetupPortuguese3(); + Test(new string[0, 0], "\\id MAT \\c 7 \\p \\v 1 «Level one. \\v 4 How can you say: «Level two», level \\v 5 one. \\p \\v 6 No continuation mark.»"); + } + + // Level2_MissingClosingMark is based on MAT 6:25-7:6 (A Boa Nova) + + [Test] + public void Level2_MissingClosingMark_UnmatchedOpeningMark() + { + // In MAT 5-7, each section consists of a separate quotation. + // If you omit a closing mark at the end of a section, + // and if the following section contains a level 2 quotation, it seems to be a level 3 quotation. + // Can the check limit the number of levels? + SetupPortuguese3(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "6:25" } + }, "\\id MAT \\c 6 \\v 25 \\p «Section with a missing closing mark. \\c 7 \\p \\v 1 «Level one. \\v 4 How can you say: «Level two», level \\v 5 one. \\p \\v 6 No continuation mark.»"); + } + + // Level2_OnlyOneClosingMark is based on MAT 7:21-23 (A Boa Nova) + + [Test] + public void Level2_OnlyOneClosingMark_UnmatchedOpeningMark() + { + // If levels one and two have the same marks, and if both levels close at the same place, + // the check displays an inconsistency that translators need to ignore. + SetupPortuguese3(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "7:21" } + }, "\\id MAT \\c 7 \\p \\v 21 «Not everyone who says: «Level two!», level one. \\v 22 Many will say: «Level two?» \\v 23 Then I will tell them: «Levels two and one end here!»"); + } + + // Level3_OnlyOneClosingMark is based on LUK 12:16-21 (A Boa Nova) + + [Test] + public void Level3_OnlyOneClosingMark_UnmatchedOpeningMark() + { + SetupPortuguese3(); + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 2), "12:17" }, + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "12:16" } // Does the check return this message first? + }, "\\id LUK \\c 12 \\p \\v 16 He told them: «Level one. \\v 17 So he said: «Level two \\v 18 \\v 19 and say to myself: «Level three and level two ends here too.», \\v 20 But God said to him: «Level two?» \\p \\v 21 Jesus said: «Level two and level one ends here too.»"); + } + #endregion + + #region Level 3 with same marks as level 1 + + // For level 3 with same marks as level 1, + // the check displays misleading inconsistencies instead of unmatched closing mark: level 3. + + // The following tests omit paragraph marks so that continuation does not matter. + + [Test] + public void Level3_Recycled_Correct() + { + SetupEnglish3(); + Test(new string[0, 0], "\\id LUK \\c 12 \\p \\v 16 He told: \u201CLevel one. \\v 17 He thought, \u2018Level two.\u2019 \\v 18 \u2018Two. \\v 19 To myself, \u201Clevel three.\u201D \u2019 \\v 20 \u2018Two.\u2019 \\v 21 Close level one.\u201D"); + } + #endregion + + #region Level 3 with distinct marks + + // The following tests omit paragraph marks so that continuation does not matter. + + [Test] + public void Level3_Distinct_Correct() + { + SetupSpanish3(); + Test(new string[0, 0], "\\id LUK \\c 12 \\p \\v 16 He told: «Level one. \\v 17 He thought, \u201Clevel two.\u201D \\v 18 \u201CTwo. \\v 19 To myself, \u2018Level three.\u2019 \u201D \\v 20 \u201CTwo.\u201D \\v 21 Close level one.»"); + } + + [Test] + public void Level3_Distinct_UnmatchedOpeningMark() + { + SetupSpanish3(); + Test(new string[,] { + { "\u2018", FormatMessage(kUnmatchedOpeningMark, 3), "12:19" } + }, "\\id LUK \\c 12 \\p \\v 16 He told: «Level one. \\v 17 He thought, \u201Clevel two.\u201D \\v 18 \u201CTwo. \\v 19 To myself, \u2018Level three.\u201D \\v 20 \u201CTwo.\u201D \\v 21 Close level one.»"); + } + + [Test] + public void Level3_Distinct_UnmatchedClosingMark() + { + SetupSpanish3(); + Test(new string[,] { + { "\u2019", FormatMessage(kUnmatchedClosingMark, 3), "12:19" } + }, "\\id LUK \\c 12 \\p \\v 16 He told: «Level one. \\v 17 He thought, \u201Clevel two.\u201D \\v 18 \u201CTwo. \\v 19 To myself, Level three.\u2019 \u201D \\v 20 \u201CTwo.\u201D \\v 21 Close level one.»"); + } + #endregion + + #region Levels 3-4 with same marks as levels 1-2 + // We have not yet found four levels in a translation that has paragraph structure. + + // Level4_Recycled is based on JER 29:24-28 (NASB) + // Inserted \p in verse 28 to account for continuation, but that does not seem quite right, + // because NASB has verse structure, instead of paragraph structure. + // The main paragraphs are at verses 24 and 29. + + public void Level4_Recycled_Continuation_InappropriateProperties_UnmatchedOpeningMark3() + { + SetupEnglish4(); + // If there is a continuation mark, but the properties indicate no mark needed, + // the recycling makes it seem to be an unmatched opening mark for level 3 + // instead of a missing continuation mark for level 1. + Test(new string[,] { + { "\u201C", FormatMessage(kUnmatchedOpeningMark, 3), "29:26" } + }, "\\id JER \\c 29 \\p \\v 24 To Shemiah speak, saying, \\v 25 \u201Clevel one, \u2018Level two, \\v 26 \u201Clevel three \\v 27 \\p \\v 28 \u201CHe has sent saying, \u2018Level four.\u2019\u201D\u2019\u201D"); + } + + [Test] + public void Level4_Recycled_Correct() // Omit continuation in verse 28. + { + SetupEnglish4(); + Test(new string[0, 0], "\\id JER \\c 29 \\p \\v 24 To Shemiah speak, saying, \\v 25 \u201Clevel one, \u2018Level two, \\v 26 \u201Clevel three \\v 27 \\v 28 He has sent saying, \u2018Level four.\u2019\u201D\u2019\u201D"); + } + #endregion + + #region Level 1 to continue in prose + + [Test] + public void Level1_ContinuationFromLinesIntoProse_InappropriateProperties_UnmatchedOpeningMark() + { + SetupSwiss2(); + // If there is a continuation mark, but the properties indicate no mark needed: + // The opening mark is unmatched. + // The continuation mark immediately preceding the closing mark is matched. + // Any other continuation marks are unmatched. + Test(new string[,] { + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "5:3" }, + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "5:11" }, + { "«", FormatMessage(kUnmatchedOpeningMark, 1), "5:13" } + }, "\\id MAT \\c 5 \\p \\v 1 \\v 2 He taught them: \\q1 \\v 3 «Level one, \\q2 line two. \\q1 \\v 10 Line one, \\q2 line two. \\p \\v 11 «Continue \\v 12 \\p \\v 13 «Continue \\p \\v 14 «Continue and close level one.»"); + } + #endregion + + #region Level 1 to continue into lines + + [Test] + public void Level1_ContinuationIntoLines2_MissingContinuationMark() + { + SetupSwiss2(); + SetupContinuationAllOpening(); + Test(new string[,] { + { "\u2039", FormatMessage(kMissingContinuationMark, 1), "2:6" }, + }, "\\id MAT \\c 2 \\p \\v 5 «Level one,» they said, «level one: \\q1 \\v 6 \u2039Level two, \\q2 level two, \\q1 level two \\q2 level two.\u203A » \\p \\v 7 Following."); + } + + // Level1_ContinuationIntoLines23 is based on MRK 12:35-37 (NIV) + + [Test] + public void Level1_ContinuationIntoLines23_Correct() + { + SetupEnglish3(); + SetupContinuationAllOpening(); + Test(new string[0, 0], "\\id MRK \\c 12 \\p \\v 35 He asked, \u201CLevel one? \\v 36 declared, \\q1 \u201C \u2018Level two, \\q2 \u201CLevel three, \\q1 line one \\q2 line two.\u201D \u2019 \\m \\v 37 No continuation \u2018Two.\u2019 Close level one.\u201D"); + } + #endregion + + #region Level 1 not to continue into lines + + // Level1_NoContinuationIntoLines2 is based on MAT 2:5-7 (NLT) but the marks differ + + [Test] + public void Level1_NoContinuationIntoLines2_Correct() + { + SetupSwiss2(); + Test(new string[0, 0], "\\id MAT \\c 2 \\p \\v 5 «Level one,» they said, «level one: \\q1 \\v 6 \u2039Level two, \\q2 line two, \\q1 line one \\q2 end of levels two and one.\u203A » \\p \\v 7 Following."); + } + #endregion + + #region Level 1 to continue within lines + // Continuation at PSA 81:8 and 11 in NIV and NLT; also at 13 in NIV but not NLT. + // Continuation occurs in PSA 82 in NIV and NLT (not at same verses). + // Continuation occurs in PSA 39:4,12; 50:22; 89:30; 132:17 in NIV, but not NLT. + // No continuation following interlude without stanza break at PSA 39:6 in NIV. + // Interlude withoug stanza break might not occur in NLT. + + // Level1_LinesContinuationFollowingStanzaBreak is based on PSA 81:6-16 (NLT and especially NIV) but the marks differ + + [Test] + [Ignore("This is better tested (because of the other used styles) in QuotationCheckSilUnitTest.cs")] + public void Level1_LinesContinuationFollowingStanzaBreak_Correct() + { + SetupEuropean1(); + SetupContinuationAllOpening(); + Test(new string[0, 0], "\\id PSA \\c 81 \\q1 \\v 6 He says, «Level one. \\q2 line two \\q1 \\v 7 line 1 \\qs \\b \\q1 \\v 8 «Continuation \\v 9 \\v 10 \\b \\q1 \\v 11 «Continuation \\v 12 to \\v 13 the \\v 14 end \\v 15 of \\v 16 the quotation and psalm.»"); + } + #endregion + + #region Not to continue within lines + + // Level12_NoContinuationWithinLines_Correct is based on MAT 3:3-4 (NIV) but the marks differ + + [Test] + public void Level12_NoContinuationWithinLines_Correct() + { + SetupSwiss2(); + Test(new string[0, 0], "\\id MAT \\c 3 \\p \\v 3 He said, \\q1 «Level one: \\q1 \\v6 \u2039Level two. \\q2 Both levels end.\u203A » \\p \\v 4 Following."); + } + #endregion + + #region Level 1 quotation dash has no closing mark + + // QuotationDash_Level2 is based on JDG 11:13-19 (RVE95 for the first level, but NIV the second level) + + [Test] + [Ignore("We currently don't correctly support this functionality (quotation dashes)")] + public void QuotationDash_Level2_Correct() + { + SetupSpanish2_QuotationDash(); + Test(new string[0, 0], "\\id JDG \\c 11 \\p \\v 13 Ammon answered. \\p \u2014Answer. \\p \\v 14 Jepthah sent messages \\v 15 saying: \\p \u2014Jepthah says: \\v 17 to Edom: \u201Clevel two.\u201D \\v 19 To Amorites: \u201Clevel two.\u201D End of message."); + } + + [Test] + [Ignore("We currently don't correctly support this functionality (quotation dashes)")] + public void QuotationDash_Level2_InappropriateProperties_UnexpectedOpeningMark() + { + SetupSpanish2(); + Test(new string[,] { + { "\u201C", FormatMessage(kUnexpectedOpeningMark, 2), "11:15" } + // The check does not find a second inconsistency for 11:17, should it? + }, "\\id JDG \\c 11 \\p \\v 13 Ammon answered. \\p \u2014Answer. \\p \\v 14 Jepthah sent messages \\v 15 saying: \\p \u2014Jepthah says: \\v 17 to Edom: \u201Clevel two.\u201D \\v 19 To Amorites: \u201Clevel two.\u201D End of message."); + } + + // QuotationDash_Level3 is based on JDG 11:13-19 (RVE95) + + [Test] + [Ignore("We currently don't correctly support this functionality (quotation dashes)")] + public void QuotationDash_Level3_Correct() + { + SetupSpanish3_QuotationDash(); + Test(new string[0, 0], "\\id JDG \\c 11 \\p \\v 13 Ammon answered. \\p \u2014Answer. \\p \\v 14 Jepthah sent messages \\v 15 saying: \\p \u2014Jepthah says: \u201Clevel two \\v 17 to Edom: \u2018Level three.\u2019 \\v 19 To Amorites: \u2018Level three.\u2019 End of message.\u201D"); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckTests.cs b/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckTests.cs new file mode 100644 index 0000000000..07c92b8d4f --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckTests.cs @@ -0,0 +1,221 @@ +// --------------------------------------------------------------------------------------------- +// Copyright (c) 2008-2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) +// +// File: RepeatedWordsTests.cs +// Responsibility: TE Team +// +// <remarks> +// </remarks> +// --------------------------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// + /// </summary> + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public class RepeatedWordsCheckTests : ScrChecksTestBase + { + private TestChecksDataSource m_dataSource; + + #region Initialization + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Set up that happens before every test runs. + /// </summary> + /// ------------------------------------------------------------------------------------ + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_dataSource = new TestChecksDataSource(); + m_check = new RepeatedWordsCheck(m_dataSource); + } + #endregion + + #region Tests + ///-------------------------------------------------------------------------------------- + /// <summary> + /// Tests the Repeated Words check for some simple cases that don't freak your mind out. + /// </summary> + ///-------------------------------------------------------------------------------------- + [Test] + public void Basic() + { + m_dataSource.m_tokens.Add(new DummyTextToken("This this is is nice, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("friend monkey ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("monkey friend.", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 5, "this", "Repeated word"); + CheckError(1, m_dataSource.m_tokens[0].Text, 13, "is", "Repeated word"); + CheckError(2, m_dataSource.m_tokens[2].Text, 0, "monkey", "Repeated word"); + } + + ///-------------------------------------------------------------------------------------- + /// <summary> + /// Tests the Repeated Words check when the repeated words differ in case (TE-6311). + /// </summary> + ///-------------------------------------------------------------------------------------- + [Test] + public void DiffCapitalization() + { + m_dataSource.m_tokens.Add(new DummyTextToken("This thiS is IS nice, my friend! ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("friend monkey ", + TextType.Verse, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("moNkEY friend.", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(3)); + + CheckError(0, m_dataSource.m_tokens[0].Text, 5, "thiS", "Repeated word"); + CheckError(1, m_dataSource.m_tokens[0].Text, 13, "IS", "Repeated word"); + CheckError(2, m_dataSource.m_tokens[2].Text, 0, "moNkEY", "Repeated word"); + } + + ///-------------------------------------------------------------------------------------- + /// <summary> + /// Tests the Repeated Words check when chapter 1 is followed by verse 1. + /// Jira issue is TE-6255 + /// </summary> + ///-------------------------------------------------------------------------------------- + [Test] + public void Chapter1Verse1() + { + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Some verse text.", + TextType.Verse, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Tests the Repeated Words check when the last word in a section head is the same + /// as the first word in the following content. A repeated word should not be reported. + /// Jira issue is TE-6634. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void SameWordAfterSectionHead() + { + m_dataSource.m_tokens.Add(new DummyTextToken("Same Words", TextType.Other, true, + false, "Section Head")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("Words that begin the paragraph", + TextType.Verse, false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0), "Word at para start is not a repeated word when the section head ends with the same word."); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Tests the Repeated Words check when the same word occurs after a verse break. We + /// do want this to be reported as a repeated word. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void SameWordAfterVerseNumber() + { + m_dataSource.m_tokens.Add(new DummyTextToken("In love", TextType.Verse, true, + false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("5", + TextType.VerseNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("love he predestined us", + TextType.Verse, false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[2].Text, 0, "love", "Repeated word"); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Tests the Repeated Words check when the repeated words have punctuation between them. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void SameWordAfterPunctuation() + { + m_dataSource.m_tokens.Add(new DummyTextToken("I am, am I not?", TextType.Verse, true, + false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0)); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Tests the Repeated Words check when the last word in a picture is the same + /// as the word following the picture ORC. A repeated word should not be reported. + /// Jira issue is TE-6402. + /// </summary> + /// ------------------------------------------------------------------------------------ + [Test] + public void SameWordAfterPictureCaption() + { + m_dataSource.m_tokens.Add(new DummyTextToken("words ending a caption", + TextType.PictureCaption, true, false, "Caption")); + m_dataSource.m_tokens.Add(new DummyTextToken("Caption also begins this paragraph", + TextType.Verse, false, false, "Paragraph")); + + m_check.Check(m_dataSource.TextTokens(), RecordError); + Assert.That(m_errors.Count, Is.EqualTo(0), "Word after caption marker is not a repeated word when the caption ends with the same word."); + } + + ///-------------------------------------------------------------------------------------- + /// <summary> + /// Tests the Repeated Words check when there are a bunch of ones mingled together, some + /// of which are chapter numbers, some verse numbers, and some plain text. + /// Jira issue is TE-6255, sort of. + /// </summary> + ///-------------------------------------------------------------------------------------- + [Test] + public void Mixed1s() + { + // \c 1 + // \v 1 + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, true, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + + // \p 1 1 + m_dataSource.m_tokens.Add(new DummyTextToken(" 1 1 ", + TextType.Verse, false, false, "Paragraph")); + + // \c 1 + // \p 1 + // \v 1 + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.ChapterNumber, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken(" 1 ", + TextType.Verse, false, false, "Paragraph")); + m_dataSource.m_tokens.Add(new DummyTextToken("1", + TextType.VerseNumber, false, false, "Paragraph")); + m_check.Check(m_dataSource.TextTokens(), RecordError); + + Assert.That(m_errors.Count, Is.EqualTo(1)); + CheckError(0, m_dataSource.m_tokens[2].Text, 3, "1", "Repeated word"); + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckUnitTest.cs new file mode 100644 index 0000000000..cfd95a3e78 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/RepeatedWordsCheckUnitTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; +using SILUBS.ScriptureChecks; + +namespace SILUBS.ScriptureChecks +{ +#if DEBUG + [TestFixture] + public class RepeatedWordsCheckUnitTest + { + UnitTestChecksDataSource source = new UnitTestChecksDataSource(); + + [SetUp] + public void RunBeforeEachTest() + { + } + + void Test(string[] result, string text) + { + Test(result, text, ""); + } + + void Test(string[] result, string text, string desiredKey) + { + source.Text = text; + + RepeatedWordsCheck check = new RepeatedWordsCheck(source); + List<TextTokenSubstring> tts = + check.GetReferences(source.TextTokens(), desiredKey); + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0)+1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i]), "Result number: " + i.ToString()); + } + + [Test] + public void Repeated() + { + Test(new string[] { "Bar" }, @"\p \v 1 Bar Bar"); + } + + [Test] + public void Quotes() + { + Test(new string[] { }, "\\p \\v 1 Bar\u201D \u201CBar"); + } + + [Test] + public void DifferentCase() + { + Test(new string[] { "bar" }, @"\p \v 1 Bar bar"); + } + + [Test] + [Ignore("Text needs to be normalized to NFC (or maybe NFD) before check is run.")] + public void DifferentNormalization() + { + Test(new string[] { "B\u00E3r", "B\u00E3r" }, + "\\p \\v 1 B\u00E3r Ba\u0303r and Ba\u0303r B\u00E3r "); + } + + [Test] + [Ignore("Text needs to be normalized to NFC (or maybe NFD) before check is run.")] + public void FindingDifferentNormalization() + { + Test(new string[] { "B\u00E3r", "B\u00E3r" }, + "\\p \\v 1 B\u00E3r Ba\u0303r and and Ba\u0303r B\u00E3r ", "B\u00E3r"); + } + + [Test] + public void CharacterStyle() + { + Test(new string[] { "Bar" }, @"\p \v 1 Bar \nd Bar\nd*"); + } + + [Test] + public void Footnote() + { + Test(new string[] { "foo", "Bar" }, @"\p \v 1 Bar \f + foo foo\f* Bar"); + } + + [Test] + public void Footnotes() + { + Test(new string[] { "Bar" }, @"\p \v 1 Bar \f + foo\f* Bar \f + foo\f*"); + } + + [Test] + public void NewParagraph() + { + Test(new string[] { }, @"\p \v 1 Bar \p Bar"); + } + + [Test] + public void NewVerse() + { + Test(new string[] { "Bar" }, @"\p \v 1 Bar \v 2 Bar"); + } + + [Test] + public void NewChapter() + { + Test(new string[] { }, @"\c 1 \p \v 1 Bar \c 2 \nd Bar\nd*"); + } + + [Test] + public void EndOfLine() + { + Test(new string[] { "bar" }, "\\p \\v 1 bar\\f + foo\\f* \r\nbar"); + } + } +#endif +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTestBase.cs b/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTestBase.cs new file mode 100644 index 0000000000..6778b4f8e5 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTestBase.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Unit tests for the PunctuationCheck class + /// </summary> + /// ---------------------------------------------------------------------------------------- + [TestFixture] + public abstract class ScrChecksTestBase + { + protected List<RecordErrorEventArgs> m_errors; + protected IScriptureCheck m_check; + + #region Setup + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Test setup (runs before each test). + /// </summary> + /// ------------------------------------------------------------------------------------ + [SetUp] + public virtual void TestSetup() + { + m_errors = new List<RecordErrorEventArgs>(); + // This is worthless, so we won't do it: dataSource.GetText(0, 0); + } + #endregion + + #region Helper methods + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Record errors returned by a check. The check calls this delegate whenever an error + /// is found. + /// </summary> + /// <param name="args">Information about the potential inconsistency being reported</param> + /// ------------------------------------------------------------------------------------ + public void RecordError(RecordErrorEventArgs args) + { + m_errors.Add(args); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Checks the error. + /// </summary> + /// <param name="iError">The index of the error.</param> + /// <param name="tokenText">The text of the token containing the error.</param> + /// <param name="offset">The offset to the start of the problem data.</param> + /// <param name="problemData">The invalid punctuation pattern, character, improperly + /// capitalized word, repeated word, etc.</param> + /// <param name="errorMessage">The error message.</param> + /// ------------------------------------------------------------------------------------ + protected void CheckError(int iError, string tokenText, int offset, string problemData, + string errorMessage) + { + //int length = problemData.Length; + //StringBuilder bldr = new StringBuilder(); + //for (int iTok = 0; iTok < m_errors[iError].toks.Count; iTok++) + //{ + // ITextToken tok = m_errors[iError].toks[iTok]; + // Assert.That(tok.Text, Is.EqualTo(tokenText[iTok])); + // if (iTok > 0 && (tok.TextType == TextType.VerseNumber || + // tok.TextType == TextType.ChapterNumber)) + // { + // continue; + // } + // if (offset + length > tok.Text.Length) + // { + // string substring = tok.Text.Substring(offset); + // bldr.Append(substring); + // offset = 0; + // length -= substring.Length; + // } + // else + // { + // bldr.Append(tok.Text.Substring(offset, length)); + // Assert.That(iTok, Is.EqualTo(m_errors[iError].toks.Count -1), "We've now found enough characters, so there should be no more tokens"); + // } + //} + //Assert.That(bldr.ToString(), Is.EqualTo(problemData)); + + Assert.That(m_errors[iError].Tts.FirstToken.Text, Is.EqualTo(tokenText)); + Assert.That(m_errors[iError].Tts.Text, Is.EqualTo(problemData)); + Assert.That(m_errors[iError].Tts.Offset, Is.EqualTo(offset)); + Assert.That(m_errors[iError].CheckId, Is.EqualTo(m_check.CheckId)); + Assert.That(m_errors[iError].Tts.Message, Is.EqualTo(errorMessage)); + } + #endregion + + #region Properties + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the IScrCheckInventory interface for the check (or null if the check does not + /// implement this interface). + /// </summary> + /// ------------------------------------------------------------------------------------ + protected IScrCheckInventory CheckInventory + { + get { return m_check as IScrCheckInventory; } + } + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTests.csproj b/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTests.csproj new file mode 100644 index 0000000000..762cfe161c --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/ScrChecksTests.csproj @@ -0,0 +1,41 @@ +<?xml version='1.0' encoding='utf-8'?> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <AssemblyName>ScrChecksTests</AssemblyName> + <RootNamespace>SILUBS.ScriptureChecks</RootNamespace> + <TargetFramework>net48</TargetFramework> + <OutputType>Library</OutputType> + <IsTestProject>true</IsTestProject> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <NoWarn>168,169,219,414,649,1635,1702,1701</NoWarn> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <Prefer32Bit>false</Prefer32Bit> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>portable</DebugType> + <Optimize>false</Optimize> + <DefineConstants>DEBUG;TRACE</DefineConstants> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' "> + <DebugType>portable</DebugType> + <Optimize>true</Optimize> + <DefineConstants>TRACE</DefineConstants> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="SIL.LCModel.Core" GeneratePathProperty="true" /> + <PackageReference Include="SIL.LCModel.Core.Tests" PrivateAssets="All" /> + <PackageReference Include="SIL.LCModel.Utils" /> + <PackageReference Include="SIL.LCModel.Utils.Tests" PrivateAssets="All" /> + <PackageReference Include="SIL.TestUtilities" /> + </ItemGroup> + <ItemGroup> + <Reference Include="netstandard" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="../../../../Src/Common/FwUtils/FwUtils.csproj" /> + <ProjectReference Include="../../../../Src/Common/FwUtils/FwUtilsTests/FwUtilsTests.csproj" /> + <ProjectReference Include="../../../../Src/Common/ScriptureUtils/ScriptureUtils.csproj" /> + <ProjectReference Include="../ScrChecks.csproj" /> + </ItemGroup> +</Project> \ No newline at end of file diff --git a/Lib/src/ScrChecks/ScrChecksTests/SentenceFinalPunctCapitalizationCheckUnitTest.cs b/Lib/src/ScrChecks/ScrChecksTests/SentenceFinalPunctCapitalizationCheckUnitTest.cs new file mode 100644 index 0000000000..ea9f89fab0 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/SentenceFinalPunctCapitalizationCheckUnitTest.cs @@ -0,0 +1,146 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) +// DISABLED: SentenceFinalPunctCapitalizationCheck class no longer exists in ScrChecks + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using NUnit.Framework; + +namespace SILUBS.ScriptureChecks +{ + [TestFixture] + [Ignore("Obsolete: SentenceFinalPunctCapitalizationCheck no longer exists in ScrChecks.")] + public class SentenceFinalPunctCapitalizationCheckUnitTest + { + [Test] + public void ObsoleteCheck_Disabled() + { + Assert.Ignore( + "Obsolete: SentenceFinalPunctCapitalizationCheck was removed/refactored; " + + "legacy implementation tests remain behind RUN_LW_LEGACY_TESTS." + ); + } + } + +} + +#if RUN_LW_LEGACY_TESTS + +namespace SILUBS.ScriptureChecks +{ + [TestFixture] + public class SentenceFinalPunctCapitalizationCheckUnitTest_Legacy + { + UnitTestChecksDataSource source = new UnitTestChecksDataSource(); + + [SetUp] + public void RunBeforeEachTest() + { + source.SetParameterValue("ValidPunctuation", "._ !_ ?_"); + } + + void Test(string[] result, string text) + { + source.Text = text; + + SentenceFinalPunctCapitalizationCheck check = new SentenceFinalPunctCapitalizationCheck(source); + List<TextTokenSubstring> tts = + check.GetReferences(source.TextTokens(), ""); + + Assert.That(tts.Count, Is.EqualTo(result.GetUpperBound(0)+1), "A different number of results was returned than what was expected."); + + for (int i = 0; i <= result.GetUpperBound(0); ++i) + Assert.That(tts[i].InventoryText, Is.EqualTo(result[i]), "Result number: " + i.ToString()); + } + + [Test] + public void UpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo. Bar"); + } + + [Test] + public void LowerCase() + { + Test(new string[] { "." }, @"\p \v 1 Foo. bar"); + } + + [Test] + public void NoCaseNonRoman() + { + Test(new string[] { }, "\\p \\v 1 Foo. \u0E01"); + } + + [Test] + public void NoCasePUA() + { + Test(new string[] { }, "\\p \\v 1 Foo. \uEE00"); + } + + [Test] + public void TitleCase() + { + Test(new string[] { }, "\\p \\v 1 Foo. \u01C5"); + } + + [Test] + public void MultipleUpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo. Bar! Baz"); + } + + [Test] + public void MultipleLowerCase() + { + Test(new string[] { ".", "!" }, @"\p \v 1 Foo. bar! baz"); + } + + [Test] + public void MultipleMixedCase() + { + Test(new string[] { "!" }, @"\p \v 1 Foo. Bar! baz"); + } + + [Test] + public void MultiplePunctUpperCase() + { + Test(new string[] { }, @"\p \v 1 Foo!? Bar"); + } + + [Test] + public void MultiplePunctLowerCase() + { + Test(new string[] { "!", "?" }, @"\p \v 1 Foo!? bar"); + } + + [Test] + public void Quotes() + { + Test(new string[] { "!" }, "\\p \\v 1 \u201CFoo!\u201D bar"); + } + + [Test] + public void Digits() + { + Test(new string[] { }, @"\p \v 1 Foo 1.2 bar"); + } + + [Test] + public void AbbreviationError() + { + Test(new string[] { "." }, @"\p \v 1 The E.U. headquarters."); + } + + [Test] + public void AbbreviationOK() + { + source.SetParameterValue("Abbreviations", "E.U."); + Test(new string[] { }, @"\p \v 1 The E.U. headquarters."); + } + } +} +#endif diff --git a/Lib/src/ScrChecks/ScrChecksTests/TestChecksDataSource.cs b/Lib/src/ScrChecks/ScrChecksTests/TestChecksDataSource.cs new file mode 100644 index 0000000000..a4865706cd --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/TestChecksDataSource.cs @@ -0,0 +1,144 @@ +// --------------------------------------------------------------------------------------------- +// Copyright (c) 2008-2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) +// +// File: TestChecksDataSource.cs +// Responsibility: TE Team +// --------------------------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Text; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Test Checks data source + /// </summary> + /// ---------------------------------------------------------------------------------------- + public class TestChecksDataSource : IChecksDataSource + { + private Dictionary<string, string> m_parameters = new Dictionary<string, string>(); + internal List<ITextToken> m_tokens = new List<ITextToken>(); + + #region IChecksDataSource Members + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the books present. + /// </summary> + /// ------------------------------------------------------------------------------------ + public List<int> BooksPresent + { + get { return new List<int>(); } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the character categorizer. + /// </summary> + /// ------------------------------------------------------------------------------------ + public CharacterCategorizer CharacterCategorizer + { + get { return new CharacterCategorizer(); } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the parameter value. + /// </summary> + /// <param name="key">The key.</param> + /// ------------------------------------------------------------------------------------ + public string GetParameterValue(string key) + { + string value; + + if (m_parameters.TryGetValue(key, out value)) + return value; + + if (key.Contains("ValidCharacters")) + return value; + + return string.Empty; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the text. + /// </summary> + /// <param name="bookNum">The book num.</param> + /// <param name="chapterNum">The chapter num.</param> + /// ------------------------------------------------------------------------------------ + public bool GetText(int bookNum, int chapterNum) + { + return true; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the localized name of the check. + /// </summary> + /// <param name="scrCheckName">Name of the check.</param> + /// ------------------------------------------------------------------------------------ + public string GetUiCheckName(string scrCheckName) + { + return scrCheckName; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the localized name of the inventory column header. + /// </summary> + /// <param name="scrCheckUnit">The check unit.</param> + /// ------------------------------------------------------------------------------------ + public string GetUiInventoryColumnHeader(string scrCheckUnit) + { + return scrCheckUnit; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// No-op. + /// </summary> + /// ------------------------------------------------------------------------------------ + public void Save() + { + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Sets the parameter value. + /// </summary> + /// <param name="key">The key.</param> + /// <param name="value">The value.</param> + /// ------------------------------------------------------------------------------------ + public void SetParameterValue(string key, string value) + { + m_parameters[key] = value; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets an enumarable thingy to enumerate the tokens. + /// </summary> + /// ------------------------------------------------------------------------------------ + public IEnumerable<ITextToken> TextTokens() + { + return m_tokens; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Returns a localized version of the specified string. + /// </summary> + /// ------------------------------------------------------------------------------------ + public string GetLocalizedString(string strToLocalize) + { + return strToLocalize; + } + + #endregion + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/UnitTestChecksDataSource.cs b/Lib/src/ScrChecks/ScrChecksTests/UnitTestChecksDataSource.cs new file mode 100644 index 0000000000..33f30fdda5 --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/UnitTestChecksDataSource.cs @@ -0,0 +1,103 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; +using SIL.FieldWorks.Common.FwUtils; + +namespace SILUBS.ScriptureChecks +{ + /// <summary> + /// SEE ICheckDataSource for documentation of these functions !!!! + /// </summary> + public class UnitTestChecksDataSource : IChecksDataSource + { + List<UnitTestUSFMTextToken> tokens2 = null; + internal string m_extraWordFormingCharacters = String.Empty; + + Dictionary<string, string> parameterValues = new Dictionary<string, string>(); + string text; + + public UnitTestChecksDataSource() + { + } + + public string GetParameterValue(string key) + { + string value; + if (!parameterValues.TryGetValue(key, out value)) + value = ""; + + return value; + } + + public void SetParameterValue(string key, string value) + { + parameterValues[key] = value; + } + + public void Save() + { + //scrText.Save(scrText.Name); + } + + public string Text + { + set + { + text = value; + UnitTestTokenizer tokenizer = new UnitTestTokenizer(); + tokens2 = tokenizer.Tokenize(text); + } + get + { + return text; + } + } + + public IEnumerable<ITextToken> TextTokens() + { + foreach (UnitTestUSFMTextToken tok in tokens2) + { + yield return (ITextToken)tok; + } + } + + public CharacterCategorizer CharacterCategorizer + { + get { return new CharacterCategorizer(m_extraWordFormingCharacters, "-", + String.Empty); + } + } + + public List<int> BooksPresent + { + get + { + List<int> present = new List<int>(); + present.Add(1); + return present; + } + } + + public bool GetText(int bookNum, int chapterNum) + { + return true; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Returns a localized version of the specified string. + /// </summary> + /// ------------------------------------------------------------------------------------ + public string GetLocalizedString(string strToLocalize) + { + return strToLocalize; + } + } +} diff --git a/Lib/src/ScrChecks/ScrChecksTests/UnitTestTokenizer.cs b/Lib/src/ScrChecks/ScrChecksTests/UnitTestTokenizer.cs new file mode 100644 index 0000000000..7d297c7fcb --- /dev/null +++ b/Lib/src/ScrChecks/ScrChecksTests/UnitTestTokenizer.cs @@ -0,0 +1,451 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Collections.Generic; +using System.Diagnostics; +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Core.Scripture; + +namespace SILUBS.ScriptureChecks +{ + /// <summary> + /// This class allows tokenizing usfm text snippets without using any Paratext specific + /// code. + /// Markers supported: id, rem, s, p, q1, q2, io, f, f*, f?, x, x*, x?, nd, nd* + /// </summary> + public class UnitTestUSFMTextToken : ITextToken + { + // Set by NextToken(...) + string paraStyleName; + public string ParaStyleName + { + set { paraStyleName = value; } + get { return paraStyleName; } + } + + string charStyleName = string.Empty; + public string CharStyleName + { + set { charStyleName = value; } + get { return charStyleName; } + } + + public string BookText; + public int Offset; + + // Set by DivideText(...) + public int Length; + public string Chapter; + public string Verse; + + // Set by CategorizeToken(...) + public bool IsPublishableText; + public bool IsNoteText; + public bool IsVerseText; + + // Virtual + public bool IsPublishable + { + get + { + if (ParaStyleName == "id") return false; + if (ParaStyleName == "rem") return false; + return true; + } + } + + public bool IsVerse { get { return CharStyleName == "v"; } } + public bool IsChapter { get { return CharStyleName == "c"; } } + + public bool IsParagraphStart + { + get + { + if (ParaStyleName == "id") return true; + if (ParaStyleName == "rem") return true; + if (ParaStyleName == "p") return true; + if (paraStyleName == "b") return true; + if (paraStyleName == "d") return true; + if (ParaStyleName == "q1") return true; + if (ParaStyleName == "q2") return true; + if (ParaStyleName == "io") return true; + if (ParaStyleName == "s") return true; + return false; + } + } + + public bool IsNoteStart + { + get + { + if (CharStyleName == "f") return true; + if (CharStyleName == "x") return true; + return false; + } + } + + public bool IsCharacterStyle + { + get + { + //if (CharStyleName == "nd") return true; + //if (ParaStyleName.StartsWith("x") && ParaStyleName != "x") return true; + //if (ParaStyleName.StartsWith("f") && ParaStyleName != "f") return true; + //return false; + + //return string.IsNullOrEmpty(CharStyleName); + + return CharStyleName != null; + } + } + + public bool IsVerseTextStyle + { + get + { + if (ParaStyleName == "p") return true; + if (paraStyleName == "b") return true; + if (ParaStyleName == "q1") return true; + if (ParaStyleName == "q2") return true; + if (ParaStyleName == "nd") return true; + return false; + } + } + + public bool IsEndStyle + { + get + { + Debug.Assert(ParaStyleName == null || !ParaStyleName.EndsWith("*"), + "Paragraph styles should never end with an asterisk."); + + return CharStyleName != null && CharStyleName.EndsWith("*"); + } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Clones this instance. + /// </summary> + /// ------------------------------------------------------------------------------------ + public ITextToken Clone() + { + UnitTestUSFMTextToken tok = new UnitTestUSFMTextToken(); + tok.ParaStyleName = this.ParaStyleName; + tok.CharStyleName = this.CharStyleName; + tok.BookText = this.BookText; + tok.Offset = this.Offset; + tok.Length = this.Length; + tok.Chapter = this.Chapter; + tok.Verse = this.Verse; + tok.IsPublishableText = this.IsPublishableText; + tok.IsNoteText = this.IsNoteText; + tok.IsVerseText = this.IsVerseText; + + return tok; + } + + public override string ToString() + { + return "ParaStyle: " + ((!string.IsNullOrEmpty(ParaStyleName)) ? ParaStyleName : "-") + + ", CharStyle: " + ((!string.IsNullOrEmpty(CharStyleName)) ? CharStyleName : "-") + + ": " + Text + "(length=" + Text.Length.ToString() + ")"; + } + + public string Locale + { + get { return string.Empty; } + } + + public TextType TextType + { + get { + if (IsVerse) return TextType.VerseNumber; + if (IsChapter) return TextType.ChapterNumber; + if (IsNoteText) return TextType.Note; + if (IsVerseText) return TextType.Verse; + return TextType.Other; + } + } + + public string Text + { + get { return BookText.Substring(Offset, Length); } + } + + public string ScrRefString + { + get { return Chapter + ":" + Verse; } + set { ; } + } + + public BCVRef MissingEndRef + { + get { return null; } + set { ; } + } + + public BCVRef MissingStartRef + { + get { return null; } + set { ; } + } + } + + class UnitTestTokenizer + { + /// <summary> + /// Split text for book into TextTokens. Populate Tokens and chapters. + /// </summary> + public List<UnitTestUSFMTextToken> Tokenize(string text) + { + List<UnitTestUSFMTextToken> tokens = DivideText(text); + CategorizeTokens(tokens); + return tokens; + } + + /// <summary> + /// Divide text for book into TextTokens. + /// Set Offset, Length, BookText, AnnotationOffset, Chapter, Verse + /// Tricky things needing done: + /// 1) Split \v N abc... into two tokens, first containing just verse number + /// 2) \f X abc... don't return caller as part of the token + /// </summary> + private List<UnitTestUSFMTextToken> DivideText(string text) + { + UnitTestUSFMTextToken tok = null; + List<UnitTestUSFMTextToken> tokens = new List<UnitTestUSFMTextToken>(); + string chapter = "1"; + string verse = "0"; + bool inPublishable = false; + + for (int i = 0; i < text.Length; ) + { + int ind = text.IndexOf("\\", i); + if (tok != null) // if token in progress, set its length + { + int last = (ind == -1) ? text.Length : ind; + tok.Length = last - tok.Offset; + } + + if (ind == -1) break; // quit if not more markers + + tok = NextToken(text, ind); // start new token + + if (tok.IsParagraphStart) + inPublishable = tok.IsPublishable || + tok.IsChapter; + + if (inPublishable) + tokens.Add(tok); + + if (tok.IsChapter) + { + chapter = GetCVNumber(text, tok.Offset); + // Everything after \c is verse '0'. + // This allows the title of Psalms (\d) which are present in the Hebrew + // text to be considered verse text. + verse = "0"; + } + else if (tok.IsVerse) + { + // Add a token with just the verse number + verse = GetCVNumber(text, tok.Offset); + tok.Length = verse.Length; + + // Make another token to contain the verse text + tok = tok.Clone() as UnitTestUSFMTextToken; + tok.CharStyleName = ""; + tok.Offset += verse.Length; + tokens.Add(tok); + + // If number followed by a space, skip this + if (char.IsWhiteSpace(text[tok.Offset])) + tok.Offset += 1; + } + + tok.Chapter = chapter; + tok.Verse = verse; + + if (tok.IsNoteStart) + { + // Skip over the footnote caller + while (tok.Offset < text.Length) + { + char cc = text[tok.Offset]; + if (cc == '\\') + break; + if (char.IsWhiteSpace(cc)) + { + ++tok.Offset; + break; + } + + ++tok.Offset; + } + } + + i = tok.Offset; + } + + return tokens; + } + + // Scan tokens. Return publishable tokens. + // Set StyleName, IsPublishableText, IsVerseText, IsNoteText. + // Character style can override the IsVerseText feature of the pargraph styles. + private void CategorizeTokens(List<UnitTestUSFMTextToken> tokens) + { + List<string> noteEndMarkers = new List<string>(); + + bool inNote = false; + bool inPublishable = false; + bool paragraphStyleIsVerseText = false; + bool characterStyleIsVerseText = false; + + foreach (UnitTestUSFMTextToken tok in tokens) + { + if (tok.ParaStyleName == "") + { + // This is the second token created form splitting the verse number from \v N abc... + characterStyleIsVerseText = true; + paragraphStyleIsVerseText = true; + inPublishable = true; + inNote = false; + } + + else if (tok.IsChapter || tok.IsVerse) + { + if (tok.IsChapter) + paragraphStyleIsVerseText = false; + characterStyleIsVerseText = false; + inPublishable = true; + inNote = false; + } + + else if (tok.IsParagraphStart) + { + inPublishable = tok.IsPublishable; + paragraphStyleIsVerseText = tok.IsVerseTextStyle; + characterStyleIsVerseText = paragraphStyleIsVerseText; + inNote = false; + } + + else if (tok.IsNoteStart) + { + inNote = true; + + // We have to build a list of note end markers. Otherwise there + // is no way to distinguish between character end markers and + // note ending markers. Sigh. + if (!noteEndMarkers.Contains("f*")) + noteEndMarkers.Add("f*"); + if (!noteEndMarkers.Contains("x*")) + noteEndMarkers.Add("x*"); + } + + else if (tok.IsEndStyle) + { + if (noteEndMarkers.Contains(tok.CharStyleName)) + inNote = false; + + characterStyleIsVerseText = paragraphStyleIsVerseText; + tok.ParaStyleName = ""; + } + + else if (tok.IsCharacterStyle) + { + characterStyleIsVerseText = tok.IsVerseTextStyle; + } + + else { Debug.Assert(false); } + + tok.IsNoteText = inNote; + tok.IsPublishableText = inPublishable; + tok.IsVerseText = !inNote && characterStyleIsVerseText && paragraphStyleIsVerseText; + } + } + + // Create a new token. Set its Offset, BookText, StyleName. + private UnitTestUSFMTextToken NextToken(string text, int ind) + { + // When this loop is done j points to the first character that is not + // part of the marker. Note that the space in '\\p ' is considered part + // of the marker (it terminates the marker). The space in '\\nd* ' is not + // considered part of the marker. + int j; + string marker = ""; + for (j = ind + 1; j < text.Length; ++j) + { + if (text[j] <= 32) + { + marker = text.Substring(ind + 1, j - (ind + 1)); + j = j + 1; + if (j < text.Length && text[j] == '\n') + j = j + 1; + break; + } + if (text[j] == '*') + { + j = j + 1; + marker = text.Substring(ind + 1, j - (ind + 1)); + break; + } + } + + UnitTestUSFMTextToken tok = new UnitTestUSFMTextToken(); + tok.Offset = j; + tok.BookText = text; + if (IsParagraphStart(marker)) + tok.ParaStyleName = marker; + else + tok.CharStyleName = marker; + + return tok; + } + + /// <summary> + /// Determines whether the specified marker is a paragraph start marker. + /// </summary> + /// <param name="marker">The specified marker.</param> + /// <returns> + /// <c>true</c> if the specified marker is a paragraph start marker; otherwise, <c>false</c>. + /// </returns> + private bool IsParagraphStart(string marker) + { + if (marker == "id") return true; + if (marker == "rem") return true; + if (marker == "p") return true; + if (marker == "b") return true; + if (marker == "d") return true; + if (marker == "q1") return true; + if (marker == "q2") return true; + if (marker == "io") return true; + if (marker == "s") return true; + return false; + } + + /// <summary> + /// Return the text of a chapter or verse number starting at the + /// specified offset. + /// </summary> + /// <param name="text"></param> + /// <param name="offset"></param> + /// <returns></returns> + private string GetCVNumber(string text, int offset) + { + while (offset < text.Length && char.IsWhiteSpace(text[offset])) + ++offset; + + int start = offset; + + while (offset < text.Length && !char.IsWhiteSpace(text[offset]) && + text[offset] != '\\') + ++offset; + + string num = text.Substring(start, offset - start); + return num; + } + } +} diff --git a/Lib/src/ScrChecks/TextInventory.cs b/Lib/src/ScrChecks/TextInventory.cs new file mode 100644 index 0000000000..bdcdf38ac6 --- /dev/null +++ b/Lib/src/ScrChecks/TextInventory.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace SILUBS.ScriptureChecks +{ + /// <summary> + /// We keep inventories of various kinds of items in a text, e.g. characters, + /// repeated words, etc. For each item we store its textual form, the books + /// it occurs in (so that we can reasonably quickly go refind it), and the + /// number of times it occurs. + /// </summary> + public class TextInventoryItem + { + public enum ItemStatus { unknown, good, bad }; + + int count = 0; + + // This is a list of book numbers in ascending order with no duplicates allowed. + // We keep a list of book numbers in order to make it faster to go back and search + // for occurrences of this item in the project. + List<int> references = new List<int>(); + + public string Text; + + public ItemStatus Status = ItemStatus.unknown; // 'y', 'n', '?' + + public void AddReference(int bookNum) + { + count = count + 1; + if (references.Count == 0 || references[references.Count - 1] != bookNum) + { + // make sure the list is in ascending order + Debug.Assert(references.Count == 0 || + bookNum > references[references.Count - 1]); + references.Add(bookNum); + } + } + + public void AddReference(int bookNum, int Count) + { + int i = references.BinarySearch(bookNum); + if (i < 0) + references.Insert(~i, bookNum); + count = count + Count; + } + + public int Count { get { return count; } } + + public List<int> References + { + get { return references; } + } + + public string Books + { + get + { + string books = ""; + + foreach (int bookNum in references) + { + while (books.Length < bookNum - 1) + books += "0"; + books += "1"; + } + + return books; + } + } + } + + /// <summary> + /// A dictionary containing instances of tetual items indexed by their + /// text. Example: the repeated key "the". + /// </summary> + public class TextInventory : Dictionary<string, TextInventoryItem> + { + public TextInventoryItem GetValue(string key) + { + TextInventoryItem item; + + if (!this.TryGetValue(key, out item)) + { + item = new TextInventoryItem(); + item.Text = key; + this[key] = item; + } + + return item; + } + } + + //void InventoryBook(TextAnalyzer tkb) + //{ + // GetErrors(tkb); + + // foreach (TextToken tok in repeatedWords) + // { + // inventory.AddReference(tok.ToString(), tkb.BookNumber); + // } + //} + +} diff --git a/Lib/src/ScrChecks/VerseTextToken.cs b/Lib/src/ScrChecks/VerseTextToken.cs new file mode 100644 index 0000000000..6be545d07a --- /dev/null +++ b/Lib/src/ScrChecks/VerseTextToken.cs @@ -0,0 +1,155 @@ +// Copyright (c) 2009-2017 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using SIL.FieldWorks.Common.FwUtils; +using SIL.LCModel.Core.Scripture; + +namespace SILUBS.ScriptureChecks +{ + /// ------------------------------------------------------------------------------------------- + /// <summary> + /// The purpose of this class is to wrap the ITextTokens for the checks that are concerned + /// with verse text only in order to be able to deal with situations in which the + /// IsParagraphStart should be true but the tokenizer has set it to false. That happens for + /// tokens containing normal text that appears at the beginning of a paragraph but is + /// preceded in the paragraph by a chapter or verse number (or a combination of these). + /// If either of these token types exist at the beginning of a paragraph before the actual + /// body text, then we need to override the IsParagraphStart property to return true for + /// the first body text token. Where this was needed, it would have been great to just set + /// that property to true for the base token, but ITextToken doesn't define a setting for + /// that property and someone with whom I consulted on this, thought this solution may be a + /// better idea than to modify the interface to require one. --DDO (May 29, 2009 - TE-8050). + /// </summary> + /// ------------------------------------------------------------------------------------------- + public class VerseTextToken : ITextToken + { + private ITextToken m_token; + private bool m_fIsParagraphStart; + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Sets the base token. + /// </summary> + /// ------------------------------------------------------------------------------------ + internal ITextToken Token + { + get { return m_token; } + set + { + ITextToken prevToken = m_token; + m_token = value; + + if (prevToken != null) + { + if (prevToken.TextType == TextType.ChapterNumber || + prevToken.TextType == TextType.VerseNumber) + { + m_fIsParagraphStart = (prevToken.IsParagraphStart || m_fIsParagraphStart); + } + else + m_fIsParagraphStart = false; + } + } + } + + #region ITextToken Members + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public bool IsParagraphStart + { + get + { + return (m_fIsParagraphStart || m_token.IsParagraphStart); + } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public string CharStyleName + { + get { return m_token.CharStyleName; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public bool IsNoteStart + { + get { return m_token.IsNoteStart; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public string Locale + { + get { return m_token.Locale; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public BCVRef MissingEndRef + { + get { return m_token.MissingEndRef; } + set { m_token.MissingEndRef = value; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public BCVRef MissingStartRef + { + get { return m_token.MissingStartRef; } + set { m_token.MissingStartRef = value; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public string ParaStyleName + { + get { return m_token.ParaStyleName; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public string ScrRefString + { + get { return m_token.ScrRefString; } + set { m_token.ScrRefString = value; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public string Text + { + get { return m_token.Text; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public TextType TextType + { + get { return m_token.TextType; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary></summary> + /// ------------------------------------------------------------------------------------ + public ITextToken Clone() + { + VerseTextToken verseToken = new VerseTextToken(); + verseToken.m_fIsParagraphStart = m_fIsParagraphStart; + verseToken.m_token = m_token; + return verseToken; + } + #endregion + } +} diff --git a/SDK_MIGRATION.md b/SDK_MIGRATION.md index e0a7b25139..e4254ebcb5 100644 --- a/SDK_MIGRATION.md +++ b/SDK_MIGRATION.md @@ -311,7 +311,7 @@ Build commands: see `.github/instructions/build.instructions.md` or `build.ps1 - **3. Native C++ Projects**: 8 VCXPROJ files — Win32 configurations removed, MIDL updated for 64-bit -**4. CI Enforcement**: `./build.ps1 -Configuration Debug -Platform x64` in `.github/workflows/CI.yml` +**4. CI Enforcement**: `./build.ps1 -Configuration Debug` in `.github/workflows/CI.yml` ### Registration-Free COM Implementation diff --git a/Setup-Developer-Machine.ps1 b/Setup-Developer-Machine.ps1 index e2526a7520..1271b4b869 100644 --- a/Setup-Developer-Machine.ps1 +++ b/Setup-Developer-Machine.ps1 @@ -13,6 +13,7 @@ # - .NET desktop development workload # - Desktop development with C++ workload (including ATL/MFC) # - Git for Windows +# - This script installs .NET SDK 8.x if it is missing (for compiling liblcm) # # Note: Serena MCP language servers (Microsoft's Roslyn C# server and clangd for C++) # auto-download on first use. No manual installation needed for Serena support. @@ -93,6 +94,43 @@ if (-not (Test-Path $toolsBase)) { # Check what's already installed +# .NET SDK 8.x +$sdkLines = @() +$dotnetCommand = Get-Command dotnet.exe -ErrorAction SilentlyContinue +if ($dotnetCommand) { + $sdkLines = @(& dotnet.exe --list-sdks 2>$null) +} + +$dotnet8Installed = $sdkLines | Where-Object { $_ -match '^8\.' } | Select-Object -First 1 +if ($dotnet8Installed) { + Write-Host "[OK] .NET SDK 8.x: $($dotnet8Installed.Trim())" -ForegroundColor Green +} else { + $winget = Get-Command winget.exe -ErrorAction SilentlyContinue + if (-not $winget) { + Write-Host "[MISSING] .NET SDK 8.x - install manually with WinGet or the .NET installer" -ForegroundColor Red + Write-Host " WinGet: winget install --id Microsoft.DotNet.SDK.8 --exact" -ForegroundColor Red + exit 1 + } + + if ($PSCmdlet.ShouldProcess('.NET SDK 8.x', 'Install via WinGet')) { + Write-Host "Installing .NET SDK 8.x..." -ForegroundColor Cyan + & $winget.Source install --id Microsoft.DotNet.SDK.8 --exact --accept-package-agreements --accept-source-agreements + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to install .NET SDK 8.x via WinGet" -ForegroundColor Red + exit 1 + } + + $sdkLines = @(& dotnet.exe --list-sdks 2>$null) + $dotnet8Installed = $sdkLines | Where-Object { $_ -match '^8\.' } | Select-Object -First 1 + if (-not $dotnet8Installed) { + Write-Host "[ERROR] .NET SDK 8.x still not detected after install" -ForegroundColor Red + exit 1 + } + + Write-Host "[OK] .NET SDK 8.x: $($dotnet8Installed.Trim())" -ForegroundColor Green + } +} + # WiX Toolset # This worktree builds installers with WiX v6 via NuGet PackageReference (restored during build). # No separate WiX 3.x installation (candle/light) is required. @@ -129,9 +167,9 @@ if ($InstallerDeps) { # Helper repo definitions: name, git URL, target subdirectory in FW repo $helperRepos = @( - @{ Name = "FwHelps"; Url = "https://github.com/sillsdev/FwHelps.git"; SubDir = "DistFiles/Helps" }, - @{ Name = "FwLocalizations"; Url = "https://github.com/sillsdev/FwLocalizations.git"; SubDir = "Localizations" }, - @{ Name = "genericinstaller"; Url = "https://github.com/sillsdev/genericinstaller.git"; SubDir = "PatchableInstaller" } + @{ Name = "FwHelps"; Url = "https://github.com/sillsdev/FwHelps.git"; SubDir = "DistFiles/Helps"; UseSharedCloneInWorktree = $true }, + @{ Name = "FwLocalizations"; Url = "https://github.com/sillsdev/FwLocalizations.git"; SubDir = "Localizations"; UseSharedCloneInWorktree = $false }, + @{ Name = "genericinstaller"; Url = "https://github.com/sillsdev/genericinstaller.git"; SubDir = "PatchableInstaller"; UseSharedCloneInWorktree = $true } ) foreach ($repo in $helperRepos) { @@ -156,7 +194,7 @@ if ($InstallerDeps) { Remove-Item $targetPath -Recurse -Force } - if ($isWorktree) { + if ($isWorktree -and $repo.UseSharedCloneInWorktree) { # Clone to shared location and create junction $sharedPath = Join-Path $repoRoot $repo.Name @@ -204,31 +242,15 @@ if ($InstallerDeps) { } } - # Special case: liblcm goes inside Localizations + # Special case: liblcm goes inside Localizations and should remain per-worktree. $lcmTarget = Join-Path $scriptDir "Localizations/LCM" $localizationsPath = Join-Path $scriptDir "Localizations" if ((Test-Path $localizationsPath) -and -not (Test-Path $lcmTarget)) { - if ($isWorktree) { - $sharedLcm = Join-Path $repoRoot "liblcm" - if (-not (Test-Path $sharedLcm)) { - if ($PSCmdlet.ShouldProcess("liblcm", "Clone to $sharedLcm")) { - Write-Host "Cloning liblcm to shared location..." -ForegroundColor Cyan - git clone https://github.com/sillsdev/liblcm.git $sharedLcm 2>&1 | Out-Null - } - } - if (Test-Path $sharedLcm) { - if ($PSCmdlet.ShouldProcess("Localizations/LCM", "Create junction to $sharedLcm")) { - New-Item -ItemType Junction -Path $lcmTarget -Target $sharedLcm -Force | Out-Null - Write-Host "[OK] Created junction: Localizations/LCM -> $sharedLcm" -ForegroundColor Green - } - } - } else { - if ($PSCmdlet.ShouldProcess("liblcm", "Clone to $lcmTarget")) { - Write-Host "Cloning liblcm..." -ForegroundColor Cyan - git clone https://github.com/sillsdev/liblcm.git $lcmTarget 2>&1 | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Host "[OK] Cloned liblcm to $lcmTarget" -ForegroundColor Green - } + if ($PSCmdlet.ShouldProcess("liblcm", "Clone to $lcmTarget")) { + Write-Host "Cloning liblcm..." -ForegroundColor Cyan + git clone https://github.com/sillsdev/liblcm.git $lcmTarget 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host "[OK] Cloned liblcm to $lcmTarget" -ForegroundColor Green } } } elseif (Test-Path $lcmTarget) { diff --git a/Src/Common/Controls/Widgets/FontHeightAdjuster.cs b/Src/Common/Controls/Widgets/FontHeightAdjuster.cs index 9cd5662fed..efa32e65b2 100644 --- a/Src/Common/Controls/Widgets/FontHeightAdjuster.cs +++ b/Src/Common/Controls/Widgets/FontHeightAdjuster.cs @@ -165,19 +165,21 @@ public static LgCharRenderProps GetChrpForStyle(string styleName, IVwStylesheet } } - VwPropertyStoreManaged vwps = new VwPropertyStoreManaged(); - vwps.Stylesheet = styleSheet; - vwps.WritingSystemFactory = writingSystemFactory; + using (VwPropertyStoreManaged vwps = new VwPropertyStoreManaged()) + { + vwps.Stylesheet = styleSheet; + vwps.WritingSystemFactory = writingSystemFactory; - ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); - ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, styleName); - ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoWs); - ITsTextProps ttp = ttpBldr.GetTextProps(); + ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); + ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, styleName); + ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoWs); + ITsTextProps ttp = ttpBldr.GetTextProps(); - LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); - ILgWritingSystem ws = writingSystemFactory.get_EngineOrNull(hvoWs); - ws.InterpretChrp(ref chrps); - return chrps; + LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); + ILgWritingSystem ws = writingSystemFactory.get_EngineOrNull(hvoWs); + ws.InterpretChrp(ref chrps); + return chrps; + } } /// ------------------------------------------------------------------------------------ diff --git a/Src/Common/FieldWorks/FieldWorks.csproj b/Src/Common/FieldWorks/FieldWorks.csproj index 2387042a30..9837677fb7 100644 --- a/Src/Common/FieldWorks/FieldWorks.csproj +++ b/Src/Common/FieldWorks/FieldWorks.csproj @@ -26,6 +26,7 @@ <DebugType>portable</DebugType> <Optimize>false</Optimize> <DefineConstants>DEBUG;TRACE</DefineConstants> + <EnableUnmanagedDebugging>true</EnableUnmanagedDebugging> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <DebugType>portable</DebugType> diff --git a/Src/Common/FwUtils/FwDirectoryFinder.cs b/Src/Common/FwUtils/FwDirectoryFinder.cs index 3af3aff31b..db7935d4df 100644 --- a/Src/Common/FwUtils/FwDirectoryFinder.cs +++ b/Src/Common/FwUtils/FwDirectoryFinder.cs @@ -470,6 +470,43 @@ public static string EditorialChecksDirectory } } + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the basic editorial checks DLL. Note that this is currently the ScrChecks DLL, + /// but if we ever split this DLL to separate Scripture-specific checks from more + /// generic checks that are really based on the WS and could be used to check any text, + /// then this property should be made to return the DLL containing the punctuation + /// patterns and characters checks. + /// </summary> + /// ------------------------------------------------------------------------------------ + public static string BasicEditorialChecksDll + { + get + { +#if RELEASE + try + { +#endif + string directory = EditorialChecksDirectory; + string checksDll = Path.Combine(directory, "ScrChecks.dll"); + if (!File.Exists(checksDll)) + { + string msg = ResourceHelper.GetResourceString( + "kstidUnableToFindEditorialChecks" + ); + throw new ApplicationException(string.Format(msg, directory)); + } + return checksDll; +#if RELEASE + } + catch (ApplicationException e) + { + throw new InstallationException(e); + } +#endif + } + } + /// ------------------------------------------------------------------------------------ /// <summary> /// Gets the dir where templates are installed diff --git a/Src/Common/FwUtils/FwUtilsTests/ParseCharacterSequencesTests.cs b/Src/Common/FwUtils/FwUtilsTests/ParseCharacterSequencesTests.cs deleted file mode 100644 index 60511788e4..0000000000 --- a/Src/Common/FwUtils/FwUtilsTests/ParseCharacterSequencesTests.cs +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) 2026 SIL International -// This software is licensed under the LGPL, version 2.1 or later -// (http://www.gnu.org/licenses/lgpl-2.1.html) - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; - -namespace SIL.FieldWorks.Common.FwUtils -{ - /// <summary> - /// A categorizer where diacritics precede their base characters (for hacked fonts). - /// </summary> - internal class DiacriticsPrecedeCategorizer : CharacterCategorizer - { - public override bool DiacriticsFollowBaseCharacters() - { - return false; - } - } - - [TestFixture] - public class ParseCharacterSequencesTests - { - private CharacterCategorizer m_defaultCategorizer; - private DiacriticsPrecedeCategorizer m_precedeCategorizer; - - [SetUp] - public void SetUp() - { - m_defaultCategorizer = new CharacterCategorizer(); - m_precedeCategorizer = new DiacriticsPrecedeCategorizer(); - } - - private static List<string> Parse(string text, CharacterCategorizer categorizer) - { - return TextFileDataSource.ParseCharacterSequences(text, categorizer).ToList(); - } - - #region Boundary cases - [Test] - public void EmptyString_ReturnsEmpty() - { - var result = Parse("", m_defaultCategorizer); - Assert.That(result, Is.Empty); - } - - [Test] - public void NullInput_ReturnsEmpty() - { - var result = Parse(null, m_defaultCategorizer); - Assert.That(result, Is.Empty); - } - #endregion - - #region Basic character parsing - [Test] - public void BasicAscii_EachCharSeparate() - { - var result = Parse("abc", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a", "b", "c" })); - } - - [Test] - public void SingleCharacter_ReturnsSingleElement() - { - var result = Parse("x", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "x" })); - } - - [Test] - public void Whitespace_EachSeparate() - { - var result = Parse(" \n", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { " ", "\n" })); - } - - [Test] - public void PunctuationMixedWithText_EachSeparate() - { - var result = Parse("a.b", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a", ".", "b" })); - } - - [Test] - public void Digits_EachSeparate() - { - var result = Parse("123", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "1", "2", "3" })); - } - #endregion - - #region Diacritics follow base (default Unicode ordering) - [Test] - public void CombiningDiacriticFollowsBase_GroupedWithBase() - { - // a + combining acute accent + b - var result = Parse("a\u0301b", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a\u0301", "b" })); - } - - [Test] - public void MultipleDiacriticsOnOneBase_AllGrouped() - { - // a + combining acute + combining circumflex - var result = Parse("a\u0301\u0302", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a\u0301\u0302" })); - } - - [Test] - public void TwoBasesEachWithDiacritics_SeparateGroups() - { - // a + combining acute + b + combining tilde - var result = Parse("a\u0301b\u0303", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a\u0301", "b\u0303" })); - } - - [Test] - public void OnlyDiacriticsFollowMode_GroupedTogether() - { - // combining acute + combining circumflex (no base) - // First diacritic becomes the key, subsequent diacritics append - var result = Parse("\u0301\u0302", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0301\u0302" })); - } - - [Test] - public void SpacingCombiningMark_TreatedAsDiacritic() - { - // Devanagari vowel sign AA (U+093E) is a SpacingCombiningMark - var result = Parse("\u0915\u093E", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0915\u093E" })); - } - #endregion - - #region Diacritics precede base (hacked font ordering) - [Test] - public void DiacriticsPrecedeBase_DiacriticSeparateFromFollowingBase() - { - // combining acute + a (with diacritics-precede mode) - var result = Parse("\u0301a", m_precedeCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0301", "a" })); - } - - [Test] - public void DiacriticsPrecedeBase_MultipleDiacritics_EachSeparate() - { - // combining acute + combining circumflex (with diacritics-precede mode) - var result = Parse("\u0301\u0302", m_precedeCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0301", "\u0302" })); - } - - [Test] - public void DiacriticsPrecedeBase_DiacriticBeforeBase_ThenAnotherBase() - { - // combining acute + a + b - var result = Parse("\u0301ab", m_precedeCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0301", "a", "b" })); - } - #endregion - - #region Supplementary plane characters (surrogate pairs) - [Test] - public void SupplementaryPlaneCharacter_NotSplit() - { - // U+10400 DESERET CAPITAL LETTER LONG I (surrogate pair in UTF-16) - var result = Parse("\U00010400", m_defaultCategorizer); - Assert.That(result.Count, Is.EqualTo(1)); - Assert.That(result[0], Is.EqualTo("\U00010400")); - Assert.That(result[0].Length, Is.EqualTo(2)); // surrogate pair = 2 chars - } - - [Test] - public void SupplementaryPlaneCharacterFollowedByCombiningMark_Grouped() - { - // U+10000 LINEAR B SYLLABLE B008 A + combining acute - var result = Parse("\U00010000\u0301", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\U00010000\u0301" })); - } - - [Test] - public void MultipleSupplementaryPlaneCharacters_EachSeparate() - { - // Two supplementary characters - var result = Parse("\U00010400\U00010401", m_defaultCategorizer); - Assert.That(result.Count, Is.EqualTo(2)); - Assert.That(result[0], Is.EqualTo("\U00010400")); - Assert.That(result[1], Is.EqualTo("\U00010401")); - } - #endregion - - #region Malformed surrogate input - [Test] - public void LoneHighSurrogate_DoesNotCrash() - { - // A lone high surrogate - malformed but should not throw - var result = Parse("\uD800", m_defaultCategorizer); - Assert.That(result.Count, Is.EqualTo(1)); - } - - [Test] - public void LoneLowSurrogate_DoesNotCrash() - { - // A lone low surrogate - malformed but should not throw - var result = Parse("\uDC00", m_defaultCategorizer); - Assert.That(result.Count, Is.EqualTo(1)); - } - #endregion - - #region Real-world minority language text - [Test] - public void VietnameseToneMarks_GroupedCorrectly() - { - // Vietnamese: a + combining horn + combining acute (NFD form) - var result = Parse("a\u031B\u0301", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "a\u031B\u0301" })); - } - - [Test] - public void HebrewWithMultipleDiacritics_GroupedCorrectly() - { - // Shin + shin dot + hiriq + tipeha - var result = Parse("\u05E9\u05C1\u05B4\u0596", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u05E9\u05C1\u05B4\u0596" })); - } - - [Test] - public void ThaiWithCombiningMarks_GroupedCorrectly() - { - // Thai KO KAI + MAI EK (tone mark, combining) - var result = Parse("\u0E01\u0E48", m_defaultCategorizer); - Assert.That(result, Is.EqualTo(new[] { "\u0E01\u0E48" })); - } - #endregion - - #region TextFileDataSource.GetReferences integration - [Test] - public void GetReferences_ReturnsCorrectOffsetsAndLengths() - { - var categorizer = new CharacterCategorizer(); - var data = new TextFileDataSource( - new[] { "a\u0301b", "cd" }, - "Line {0}", - categorizer); - - var refs = data.GetReferences(); - Assert.That(refs, Is.Not.Null); - // Line 1: "a\u0301" (offset 0, length 2) + "b" (offset 2, length 1) - // Line 2: "c" (offset 0, length 1) + "d" (offset 1, length 1) - Assert.That(refs.Count, Is.EqualTo(4)); - - Assert.That(refs[0].Offset, Is.EqualTo(0)); - Assert.That(refs[0].Length, Is.EqualTo(2)); - Assert.That(refs[0].Text, Is.EqualTo("a\u0301")); - - Assert.That(refs[1].Offset, Is.EqualTo(2)); - Assert.That(refs[1].Length, Is.EqualTo(1)); - Assert.That(refs[1].Text, Is.EqualTo("b")); - - Assert.That(refs[2].Offset, Is.EqualTo(0)); - Assert.That(refs[2].Length, Is.EqualTo(1)); - Assert.That(refs[2].Text, Is.EqualTo("c")); - - Assert.That(refs[3].Offset, Is.EqualTo(1)); - Assert.That(refs[3].Length, Is.EqualTo(1)); - Assert.That(refs[3].Text, Is.EqualTo("d")); - } - - [Test] - public void GetReferences_EmptyLines_Skipped() - { - var categorizer = new CharacterCategorizer(); - var data = new TextFileDataSource( - new[] { "", "a", "" }, - "Line {0}", - categorizer); - - var refs = data.GetReferences(); - Assert.That(refs.Count, Is.EqualTo(1)); - Assert.That(refs[0].Text, Is.EqualTo("a")); - } - - [Test] - public void GetReferences_SupplementaryCharacters_CorrectOffsets() - { - var categorizer = new CharacterCategorizer(); - // U+10400 is a surrogate pair (2 chars in UTF-16) - var data = new TextFileDataSource( - new[] { "\U00010400a" }, - "Line {0}", - categorizer); - - var refs = data.GetReferences(); - Assert.That(refs.Count, Is.EqualTo(2)); - Assert.That(refs[0].Offset, Is.EqualTo(0)); - Assert.That(refs[0].Length, Is.EqualTo(2)); // surrogate pair - Assert.That(refs[1].Offset, Is.EqualTo(2)); - Assert.That(refs[1].Length, Is.EqualTo(1)); - Assert.That(refs[1].Text, Is.EqualTo("a")); - } - #endregion - } -} diff --git a/Src/Common/FwUtils/FwUtilsTests/TestFwStylesheetTests.cs b/Src/Common/FwUtils/FwUtilsTests/TestFwStylesheetTests.cs index 8fd3daa834..2ada8312de 100644 --- a/Src/Common/FwUtils/FwUtilsTests/TestFwStylesheetTests.cs +++ b/Src/Common/FwUtils/FwUtilsTests/TestFwStylesheetTests.cs @@ -127,19 +127,21 @@ public void TestOverrideFontForWritingSystem_ForStyleWithNullProps() stylesheet.OverrideFontsForWritingSystems("FirstStyle", fontOverrides); //check results - VwPropertyStoreManaged vwps = new VwPropertyStoreManaged(); - vwps.Stylesheet = stylesheet; - vwps.WritingSystemFactory = wsf; + using (VwPropertyStoreManaged vwps = new VwPropertyStoreManaged()) + { + vwps.Stylesheet = stylesheet; + vwps.WritingSystemFactory = wsf; - ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); - ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, "FirstStyle"); - ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoGermanWs); - ITsTextProps ttp = ttpBldr.GetTextProps(); + ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); + ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, "FirstStyle"); + ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoGermanWs); + ITsTextProps ttp = ttpBldr.GetTextProps(); - LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); - ws.InterpretChrp(ref chrps); + LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); + ws.InterpretChrp(ref chrps); - Assert.That(chrps.dympHeight / 1000, Is.EqualTo(48)); + Assert.That(chrps.dympHeight / 1000, Is.EqualTo(48)); + } } /// ------------------------------------------------------------------------------------ @@ -190,29 +192,31 @@ public void TestOverrideFontsForWritingSystems_ForStyleWithProps() stylesheet.OverrideFontsForWritingSystems("FirstStyle", fontOverrides); //check results - VwPropertyStoreManaged vwps = new VwPropertyStoreManaged(); - vwps.Stylesheet = stylesheet; - vwps.WritingSystemFactory = wsf; - - ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); - ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, "FirstStyle"); - ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoFrenchWs); - ITsTextProps ttpFrench = ttpBldr.GetTextProps(); - ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoGermanWs); - ITsTextProps ttpGerman = ttpBldr.GetTextProps(); - ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoInglesWs); - ITsTextProps ttpIngles = ttpBldr.GetTextProps(); - - LgCharRenderProps chrpsFrench = vwps.get_ChrpFor(ttpFrench); - LgCharRenderProps chrpsGerman = vwps.get_ChrpFor(ttpGerman); - LgCharRenderProps chrpsIngles = vwps.get_ChrpFor(ttpIngles); - wsFrench.InterpretChrp(ref chrpsFrench); - wsGerman.InterpretChrp(ref chrpsGerman); - wsIngles.InterpretChrp(ref chrpsIngles); - - Assert.That(chrpsFrench.dympHeight / 1000, Is.EqualTo(23)); - Assert.That(chrpsIngles.dympHeight / 1000, Is.EqualTo(34)); - Assert.That(chrpsGerman.dympHeight / 1000, Is.EqualTo(48)); + using (VwPropertyStoreManaged vwps = new VwPropertyStoreManaged()) + { + vwps.Stylesheet = stylesheet; + vwps.WritingSystemFactory = wsf; + + ITsPropsBldr ttpBldr = TsStringUtils.MakePropsBldr(); + ttpBldr.SetStrPropValue((int)FwTextPropType.ktptNamedStyle, "FirstStyle"); + ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoFrenchWs); + ITsTextProps ttpFrench = ttpBldr.GetTextProps(); + ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoGermanWs); + ITsTextProps ttpGerman = ttpBldr.GetTextProps(); + ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoInglesWs); + ITsTextProps ttpIngles = ttpBldr.GetTextProps(); + + LgCharRenderProps chrpsFrench = vwps.get_ChrpFor(ttpFrench); + LgCharRenderProps chrpsGerman = vwps.get_ChrpFor(ttpGerman); + LgCharRenderProps chrpsIngles = vwps.get_ChrpFor(ttpIngles); + wsFrench.InterpretChrp(ref chrpsFrench); + wsGerman.InterpretChrp(ref chrpsGerman); + wsIngles.InterpretChrp(ref chrpsIngles); + + Assert.That(chrpsFrench.dympHeight / 1000, Is.EqualTo(23)); + Assert.That(chrpsIngles.dympHeight / 1000, Is.EqualTo(34)); + Assert.That(chrpsGerman.dympHeight / 1000, Is.EqualTo(48)); + } } } } diff --git a/Src/Common/FwUtils/IChecksDataSource.cs b/Src/Common/FwUtils/IChecksDataSource.cs new file mode 100644 index 0000000000..f1c59d36e6 --- /dev/null +++ b/Src/Common/FwUtils/IChecksDataSource.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Collections.Generic; + +namespace SIL.FieldWorks.Common.FwUtils +{ + public static class CheckUtils + { + public const char kStyleNamesDelimiter = '\uFFFD'; + } + + public interface IChecksDataSource + { + /// <summary> + /// Retrieve named checking parameter value. + /// Checks use this to get their setup information. + /// If the key isn't found, the implementation should return an empty string. + /// </summary> + /// <param name="key">Parameter name</param> + /// <returns>Parameter value</returns> + string GetParameterValue(string key); + + /// <summary> + /// Set the named checking parameter value. This is normally done by + /// the inventory display associated with the check. + /// </summary> + /// <param name="key">Parmameter name</param> + /// <param name="value">Parameter value</param> + void SetParameterValue(string key, string value); + + /// <summary> + /// Save all changes to parameter values + /// </summary> + void Save(); + + /// <summary> + /// Read the text for the specified book number. Parse into Tokens. + /// The tokens are accessed via the TextTokens() method. + /// We split this operation into two parts since we often want to create + /// the tokens list once and then present them to several different checks. + /// REVIEW: Rather than splitting this in this way, would it make more sense + /// to leave the onus on the implementor to cache the list if desired? + /// </summary> + /// <param name="bookNum">Number of book. This may be different in different + /// applications. This number comes from BooksPresent below.</param> + /// <param name="chapterNum">0=read whole book, else specified chapter number</param> + /// <returns><c>true</c> for success, <c>false</c> if no tokens are found for the + /// requested book and chapter</returns> + bool GetText(int bookNum, int chapterNum); + + /// <summary> + /// Enumerate all the ITextToken's from the most recent GetText call. + /// </summary> + /// <returns></returns> + // REVIEW: Why isn't this a property instead of a method? + IEnumerable<ITextToken> TextTokens(); + + /// <summary> + /// Return a list of the book numbers present. + /// This list is used as an argument to GetText above when retrieving the + /// data for each book. + /// The book number is arbitrary as long as BooksPresent/GetText agree. + /// This list should be in ascending order (because TextInventory.AddReference + /// can be implemented more efficiently if this is so --- and it gets called + /// a zillion times when building a character inventory). + /// </summary> + List<int> BooksPresent { get; } + + /// <summary> + /// Get information about characters and words. + /// This is needed for platform such as Paratext which allow the user to + /// overide some chracter info. Platform which don't allow this can mostly + /// likely just use the distributed version of CharacterCategorizer with + /// an empty conctructor. + /// ISSUE: what to do about word medial punctuation, I think every application + /// will have to have a way to specify this list. + /// </summary> + /// <returns>A character categorizer</returns> + CharacterCategorizer CharacterCategorizer { get; } + + /// <summary> + /// Returns a localized version of the specified string. + /// </summary> + /// <param name="strToLocalize"></param> + /// <returns>A Localized version of strToLocalize</returns> + string GetLocalizedString(string strToLocalize); + } +} diff --git a/Src/Common/FwUtils/IScriptureCheck.cs b/Src/Common/FwUtils/IScriptureCheck.cs new file mode 100644 index 0000000000..158179b994 --- /dev/null +++ b/Src/Common/FwUtils/IScriptureCheck.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; +using System.Collections.Generic; + +namespace SIL.FieldWorks.Common.FwUtils +{ + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Interface for checks that don't need to implement an inventory mode. + /// </summary> + /// ---------------------------------------------------------------------------------------- + public interface IScriptureCheck + { + /// <summary> + /// Execute the check. + /// Call 'RecordError' for every error found. + /// </summary> + /// <param name="toks">ITextToken's corresponding to the text to be checked. + /// Typically this is one books worth.</param> + /// <param name="record">Call this delegate to report each error found.</param> + void Check(IEnumerable<ITextToken> toks, RecordErrorHandler record); + + /// <summary> + /// The full name of the check, e.g. "Repeated Words". After replacing any spaces + /// with underscores, this can also be used as a key for looking up a localized + /// string if the application supports localization. If this is ever changed, + /// DO NOT change the CheckId! + /// </summary> + string CheckName { get; } + + /// <summary> + /// The unique identifier of the check. This should never be changed! + /// </summary> + Guid CheckId { get; } + + /// <summary> + /// The group which the check belongs to, e.g. "Basic". After replacing any spaces + /// with underscores, this can also be used as a key for looking up a localized + /// string if the application supports localization. + /// </summary> + string CheckGroup { get; } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets a number that can be used to order this check relative to other checks in the + /// same group when displaying checks in the UI. + /// </summary> + /// ------------------------------------------------------------------------------------ + float RelativeOrder { get; } + + /// <summary> + /// A description for the check, e.g. "Searches for all occurrences of repeated words". + /// After replacing any spaces with underscores, this can also be used as a key for + /// looking up a localized string if the application supports localization. + /// </summary> + string Description { get; } + } + + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Interface for scripture checks that provide an inventory mode. + /// </summary> + /// ---------------------------------------------------------------------------------------- + public interface IScrCheckInventory : IScriptureCheck + { + /// <summary> + /// The name of the basic unit that this check covers as it occurs in the + /// inventory for this check (e.g. "Words"). Empty string if none. After + /// replacing any spaces with underscores, this can also be used as a key + /// for looking up a localized string if the application supports localization. + /// </summary> + string InventoryColumnHeader { get; } + + /// <summary> + /// THIS REALLY OUGHT TO BE List + /// Valid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// </summary> + string ValidItems { get; set; } + + /// <summary> + /// THIS REALLY OUGHT TO BE List + /// Invalid items, separated by spaces. + /// Inventory form queries this to know how what status to give each item + /// in the inventory. Inventory form updates this if user has changed the status + /// of any item. + /// </summary> + string InvalidItems { get; set; } + + /// <summary> + /// Get all instances of the item being checked in the token list passed. + /// This includes both valid and invalid instances. + /// This is used 1) to create an inventory of these items. + /// To show the user all instance of an item with a specified key. + /// 2) With a "desiredKey" in order to fetch instance of a specific + /// item (e.g. all the places where "the" is a repeated word. + /// </summary> + /// <param name="tokens">Tokens for text to be scanned</param> + /// <param name="desiredKey">If you only want instance of a specific key (e.g. one word, one punctuation pattern, + /// one character, etc.) place it here. Empty string returns all items.</param> + /// <returns>List of token substrings</returns> + List<TextTokenSubstring> GetReferences(IEnumerable<ITextToken> tokens, string desiredKey); + + /// <summary> + /// Update the parameter values for storing the valid and invalid lists in CheckDataSource + /// and then save them. This is here because the inventory form does not know the names of + /// the parameters that need to be saved for a given check, only the check knows this. + /// </summary> + void Save(); + } +} diff --git a/Src/Common/FwUtils/ITextToken.cs b/Src/Common/FwUtils/ITextToken.cs index 4ee7de79fa..ad7aa631cd 100644 --- a/Src/Common/FwUtils/ITextToken.cs +++ b/Src/Common/FwUtils/ITextToken.cs @@ -81,6 +81,73 @@ public interface ITextToken /// ------------------------------------------------------------------------------------ TextType TextType { get; } + /// ------------------------------------------------------------------------------------ + /// <summary> + /// This is the style name for the paragraph containing this text. It is needed by + /// * Matched Pair check + /// * Uncapitalized Styles check + /// * Quotations check + /// + /// The quotation check which must have a list of paragraph types which + /// require continuation quotes. + /// + /// Quotation checking requires being able to say things like: + /// (these rules assume USFM markup, not sure how this would translate + /// into TE markup model, maybe just different style names?) + /// * If there is an open quote all \p paragraphs should start with + /// a continuer + /// * If there is an open quote all \q1 paragraphs should start with + /// a continuer if they follow a \p or \b but not if they follow + /// \q2 or \q1. + /// + /// ALTERNATIVE + /// have two properties just for Quotations check + /// bool ParagraphRequiresQuoteContinuer + /// bool ParagraphRequiresQuoteInQuoteContinuer + /// have two properties just for Matched Pair check + /// bool IsPoeticStyle + /// bool IsIntroductionOutlineStyle + /// no alternative for the Uncapitalized Styles check + /// + /// Checks will use the ParaStyleName or CharStyleName below, and not any of the alternatives. + /// </summary> + /// ------------------------------------------------------------------------------------ + string ParaStyleName { get; } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the name of the character style (if any). + /// </summary> + /// ------------------------------------------------------------------------------------ + string CharStyleName { get; } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets or sets the Scripture reference as a string, + /// suitable for displaying in the UI + /// </summary> + /// ------------------------------------------------------------------------------------ + string ScrRefString { get; set; } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// This property is primarily for the chapter/verse check and allows the + /// check to set the beginning reference of a missing chapter or verse range. + /// If the missing verse is not part of a range, then this just contains + /// the missing verse reference and the MissingEndRef is empty. + /// </summary> + /// ------------------------------------------------------------------------------------ + BCVRef MissingStartRef { get; set; } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// This property is primarily for the chapter/verse check and allows the + /// check to set the ending reference of a missing chapter or verse range. + /// If the missing verse is not part of a range, then this is empty. + /// </summary> + /// ------------------------------------------------------------------------------------ + BCVRef MissingEndRef { get; set; } + /// ------------------------------------------------------------------------------------ /// <summary> /// Creates a deep copy of this text token. diff --git a/Src/Common/FwUtils/RecordErrorEventArgs.cs b/Src/Common/FwUtils/RecordErrorEventArgs.cs new file mode 100644 index 0000000000..6dcd7bf5a8 --- /dev/null +++ b/Src/Common/FwUtils/RecordErrorEventArgs.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2009-2015 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System; + +namespace SIL.FieldWorks.Common.FwUtils +{ + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Class of object to be passed to the RecodError delegate, containing information about + /// the location and nature of the checking inconsistency. + /// </summary> + /// ---------------------------------------------------------------------------------------- + public class RecordErrorEventArgs : EventArgs + { + private TextTokenSubstring m_tts; + private Guid m_checkId; + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Initializes a new instance of the <see cref="RecordErrorEventArgs"/> class. + /// </summary> + /// <param name="tts">The TextTokenSubstring.</param> + /// <param name="checkId">The GUID identifying the check.</param> + /// ------------------------------------------------------------------------------------ + public RecordErrorEventArgs(TextTokenSubstring tts, Guid checkId) + { + m_tts = tts; + m_checkId = checkId; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the TextTokenSubstring. + /// </summary> + /// ------------------------------------------------------------------------------------ + public TextTokenSubstring Tts + { + get { return m_tts; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the GUID identifying the check. + /// </summary> + /// ------------------------------------------------------------------------------------ + public Guid CheckId + { + get { return m_checkId; } + } + } + + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Callback to record an error occuring at the specified location within + /// the token. The message will have already been localized by calling the + /// IChecksDataSource.GetLocalizedString() method. + /// </summary> + /// ---------------------------------------------------------------------------------------- + public delegate void RecordErrorHandler(RecordErrorEventArgs args); +} diff --git a/Src/Common/FwUtils/TextFileDataSource.cs b/Src/Common/FwUtils/TextFileDataSource.cs index 15838ad6e4..56152202c5 100644 --- a/Src/Common/FwUtils/TextFileDataSource.cs +++ b/Src/Common/FwUtils/TextFileDataSource.cs @@ -2,8 +2,9 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) +using System; using System.Collections.Generic; -using System.Globalization; +using System.Reflection; using SIL.LCModel.Core.Scripture; namespace SIL.FieldWorks.Common.FwUtils @@ -14,92 +15,174 @@ namespace SIL.FieldWorks.Common.FwUtils /// A class representing a file that can be parsed to find characters /// </summary> /// ---------------------------------------------------------------------------------------- - public class TextFileDataSource + public class TextFileDataSource : IChecksDataSource { + private string m_scrChecksDllFile; + private string m_scrCheck; private CharacterCategorizer m_characterCategorizer; private List<ITextToken> m_tftList; + private Dictionary<string, string> m_params; /// ------------------------------------------------------------------------------------ /// <summary> /// Initializes a new instance of the <see cref="TextFileDataSource"/> class. /// </summary> + /// <param name="scrChecksDllFile">The DLL that contains the CharactersCheck class + /// </param> + /// <param name="scrCheck">Name of the scripture check to use</param> /// <param name="fileData">An array of strings with the lines of data from the file. /// </param> /// <param name="scrRefFormatString">Format string used to format scripture references. /// </param> - /// <param name="categorizer">The character categorizer.</param> /// ------------------------------------------------------------------------------------ - public TextFileDataSource(string[] fileData, string scrRefFormatString, + public TextFileDataSource(string scrChecksDllFile, string scrCheck, string[] fileData, + string scrRefFormatString) : + this(scrChecksDllFile, scrCheck, fileData, scrRefFormatString, null, null) + { + } + + /// -------------------------------------------------------------------------------- + /// <summary> + /// Initializes a new instance of the <see cref="TextFileDataSource"/> class. + /// </summary> + /// <param name="scrChecksDllFile">The DLL that contains the CharactersCheck class</param> + /// <param name="scrCheck">Name of the scripture check to use</param> + /// <param name="fileData">An array of strings with the lines of data from the file.</param> + /// <param name="scrRefFormatString">Format string used to format scripture references.</param> + /// <param name="parameters">Checking parameters to send the check.</param> + /// <param name="categorizer">The character categorizer.</param> + /// -------------------------------------------------------------------------------- + public TextFileDataSource(string scrChecksDllFile, string scrCheck, string[] fileData, + string scrRefFormatString, Dictionary<string, string> parameters, CharacterCategorizer categorizer) { - m_characterCategorizer = categorizer ?? new CharacterCategorizer(); + m_scrChecksDllFile = scrChecksDllFile; + m_scrCheck = scrCheck; + m_characterCategorizer = (categorizer != null) ? categorizer : new CharacterCategorizer(); + m_params = parameters; m_tftList = new List<ITextToken>(); int i = 1; foreach (string line in fileData) m_tftList.Add(new TextFileToken(line, i++, scrRefFormatString)); } + #region IChecksDataSource Members /// ------------------------------------------------------------------------------------ /// <summary> - /// Gets character sequence references from all tokens. + /// Gets the books present (not supported). /// </summary> /// ------------------------------------------------------------------------------------ - public List<TextTokenSubstring> GetReferences() + public List<int> BooksPresent { - var results = new List<TextTokenSubstring>(); - foreach (ITextToken tok in m_tftList) - { - if (tok.Text == null) - continue; - int offset = 0; - foreach (string key in ParseCharacterSequences(tok.Text, m_characterCategorizer)) - { - results.Add(new TextTokenSubstring(tok, offset, key.Length)); - offset += key.Length; - } - } - return results; + get { throw new NotSupportedException(); } } /// ------------------------------------------------------------------------------------ /// <summary> - /// Parses a string into character sequences, grouping base characters with their - /// combining diacritics. Handles surrogate pairs correctly. + /// Gets the character categorizer. /// </summary> - /// <param name="text">The text to parse.</param> - /// <param name="categorizer">The character categorizer.</param> - /// <returns>An enumeration of character sequences.</returns> /// ------------------------------------------------------------------------------------ - internal static IEnumerable<string> ParseCharacterSequences(string text, - CharacterCategorizer categorizer) + public CharacterCategorizer CharacterCategorizer + { + get { return m_characterCategorizer; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the parameter value. + /// </summary> + /// <param name="key">The key.</param> + /// <returns>An empty string</returns> + /// ------------------------------------------------------------------------------------ + public string GetParameterValue(string key) + { + string param; + if (m_params != null && m_params.TryGetValue(key, out param)) + return param; + + return string.Empty; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the text (not supported). + /// </summary> + /// <param name="bookNum">The book num.</param> + /// <param name="chapterNum">The chapter num.</param> + /// ------------------------------------------------------------------------------------ + public bool GetText(int bookNum, int chapterNum) + { + throw new NotSupportedException(); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Saves this instance (not supported). + /// </summary> + /// ------------------------------------------------------------------------------------ + public void Save() + { + throw new NotSupportedException(); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Sets the parameter value (not supported). + /// </summary> + /// <param name="key">The key.</param> + /// <param name="value">The value.</param> + /// ------------------------------------------------------------------------------------ + public void SetParameterValue(string key, string value) { - if (string.IsNullOrEmpty(text)) - yield break; + throw new NotSupportedException(); + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the text tokens. + /// </summary> + /// ------------------------------------------------------------------------------------ + public IEnumerable<ITextToken> TextTokens() + { + return m_tftList; + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + public string GetLocalizedString(string strToLocalize) + { + return strToLocalize; + } - var enumerator = StringInfo.GetTextElementEnumerator(text); - string key = ""; - bool diacriticsFollow = categorizer.DiacriticsFollowBaseCharacters(); + #endregion - while (enumerator.MoveNext()) + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the references. + /// </summary> + /// <returns></returns> + /// ------------------------------------------------------------------------------------ + public List<TextTokenSubstring> GetReferences() + { + try { - string element = enumerator.GetTextElement(); - // Only single BMP chars can be diacritics (combining marks are all in the BMP) - bool isDiacritic = element.Length == 1 && categorizer.IsDiacritic(element[0]); - - if (isDiacritic && diacriticsFollow) - { - key += element; - } - else - { - if (key.Length > 0) - yield return key; - key = element; - } - } + // If this whole valid characters area wasn't such a mess I'd move this bit into a unit + // testable chunk to make sure the reflection didn't break again. If this breaks again + // maybe it will encourage us to fix the whole mess. -jn 7/2020 + Assembly asm = Assembly.LoadFile(m_scrChecksDllFile); + Type type = asm.GetType("SILUBS.ScriptureChecks." + m_scrCheck); + IScrCheckInventory scrCharInventoryBldr = + Activator.CreateInstance(type, this) as IScrCheckInventory; - if (key.Length > 0) - yield return key; + return scrCharInventoryBldr.GetReferences(m_tftList, string.Empty); + } + catch + { + return null; + } } } @@ -131,26 +214,114 @@ internal TextFileToken(string text, int iLine, string scrRefFormatString) } #region ITextToken Members - /// <summary>Not used.</summary> - public bool IsNoteStart => false; + /// -------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------- + public bool IsNoteStart + { + get { return false; } + } + + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public bool IsParagraphStart + { + get { return true; } + } + + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public string Locale + { + get { return null; } + } + + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public string ScrRefString + { + get { return string.Format(m_scrRefFmtString, m_iLine); } + set { ; } + } - /// <summary>Not used.</summary> - public bool IsParagraphStart => true; + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public string ParaStyleName + { + get { return null; } + } - /// <summary>Not used.</summary> - public string Locale => null; + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Not used. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public string CharStyleName + { + get { return null; } + } - /// <summary>Gets the scripture reference string.</summary> - public string ScrRefString => string.Format(m_scrRefFmtString, m_iLine); + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Gets the text. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public string Text + { + get { return m_text; } + } - /// <summary>Gets the text.</summary> - public string Text => m_text; + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// Force the check to treat the text like verse text. + /// </summary> + /// -------------------------------------------------------------------------------------------- + public TextType TextType + { + get { return TextType.Verse; } + } - /// <summary>Force the check to treat the text like verse text.</summary> - public TextType TextType => TextType.Verse; + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// + /// </summary> + /// -------------------------------------------------------------------------------------------- + public BCVRef MissingEndRef + { + get { return null; } + set { ; } + } + /// -------------------------------------------------------------------------------------------- + /// <summary> + /// + /// </summary> + /// -------------------------------------------------------------------------------------------- + public BCVRef MissingStartRef + { + get { return null; } + set { ; } + } - /// <summary>Makes a deep copy of the specified text token.</summary> + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Makes a deep copy of the specified text token. + /// </summary> + /// ------------------------------------------------------------------------------------ public ITextToken Clone() { return new TextFileToken(m_text, m_iLine, m_scrRefFmtString); diff --git a/Src/Common/FwUtils/TextTokenSubstring.cs b/Src/Common/FwUtils/TextTokenSubstring.cs index 31634f22a1..a5e25c3611 100644 --- a/Src/Common/FwUtils/TextTokenSubstring.cs +++ b/Src/Common/FwUtils/TextTokenSubstring.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Text; using System.Diagnostics; +using SIL.LCModel.Core.Scripture; namespace SIL.FieldWorks.Common.FwUtils { @@ -239,6 +240,16 @@ public int Length get { return m_length; } } + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the paragraph style name. + /// </summary> + /// ------------------------------------------------------------------------------------ + public string ParagraphStyle + { + get { return m_tokens[0].ParaStyleName; } + } + /// ------------------------------------------------------------------------------------ /// <summary> /// Gets the first text token. @@ -258,6 +269,26 @@ public ITextToken LastToken get { return m_tokens.Count == 0 ? null : m_tokens[m_tokens.Count - 1]; } } + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the missing start reference, if any. + /// </summary> + /// ------------------------------------------------------------------------------------ + public BCVRef MissingStartRef + { + get { return m_tokens.Count != 1 ? null : m_tokens[0].MissingStartRef; } + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// Gets the missing and reference, if any. + /// </summary> + /// ------------------------------------------------------------------------------------ + public BCVRef MissingEndRef + { + get { return m_tokens.Count != 1 ? null : m_tokens[0].MissingEndRef; } + } + /// ------------------------------------------------------------------------------------ /// <summary> /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. diff --git a/Src/Common/SimpleRootSite/EditingHelper.cs b/Src/Common/SimpleRootSite/EditingHelper.cs index 7a22205912..753f975d28 100644 --- a/Src/Common/SimpleRootSite/EditingHelper.cs +++ b/Src/Common/SimpleRootSite/EditingHelper.cs @@ -1537,22 +1537,24 @@ public static Font GetFontForNormalStyle(int hvoWs, IVwStylesheet styleSheet, ttpBldr.SetIntPropValues((int)FwTextPropType.ktptWs, 0, hvoWs); ITsTextProps ttp = ttpBldr.GetTextProps(); - VwPropertyStoreManaged vwps = new VwPropertyStoreManaged(); - vwps.Stylesheet = styleSheet; - vwps.WritingSystemFactory = wsf; - LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); - ILgWritingSystem ws = wsf.get_EngineOrNull(hvoWs); - ws.InterpretChrp(ref chrps); - int dympHeight = chrps.dympHeight; - StringBuilder bldr = new StringBuilder(chrps.szFaceName.Length); - for (int i = 0; i < chrps.szFaceName.Length; i++) - { - ushort ch = chrps.szFaceName[i]; - if (ch == 0) - break; // null termination - bldr.Append(Convert.ToChar(ch)); - } - return new Font(bldr.ToString(), (float)(dympHeight / 1000.0)); + using (VwPropertyStoreManaged vwps = new VwPropertyStoreManaged()) + { + vwps.Stylesheet = styleSheet; + vwps.WritingSystemFactory = wsf; + LgCharRenderProps chrps = vwps.get_ChrpFor(ttp); + ILgWritingSystem ws = wsf.get_EngineOrNull(hvoWs); + ws.InterpretChrp(ref chrps); + int dympHeight = chrps.dympHeight; + StringBuilder bldr = new StringBuilder(chrps.szFaceName.Length); + for (int i = 0; i < chrps.szFaceName.Length; i++) + { + ushort ch = chrps.szFaceName[i]; + if (ch == 0) + break; // null termination + bldr.Append(Convert.ToChar(ch)); + } + return new Font(bldr.ToString(), (float)(dympHeight / 1000.0)); + } } /// ------------------------------------------------------------------------------------ diff --git a/Src/FwCoreDlgs/CharContextCtrl.cs b/Src/FwCoreDlgs/CharContextCtrl.cs index 37c2fc2ef6..70575f8866 100644 --- a/Src/FwCoreDlgs/CharContextCtrl.cs +++ b/Src/FwCoreDlgs/CharContextCtrl.cs @@ -60,6 +60,7 @@ public delegate void GetContextInfoHandler(int index, out string sKey, private string[] m_fileData; private DataGridView m_tokenGrid; + private string m_scrChecksDllFile; private List<ContextInfo> m_currContextInfoList; private Dictionary<string, List<ContextInfo>> m_contextInfoLists; private LcmCache m_cache; @@ -67,6 +68,7 @@ public delegate void GetContextInfoHandler(int index, out string sKey, private IApp m_app; private CoreWritingSystemDefinition m_ws; private int m_gridRowHeight; + private CheckType m_checkToRun; private string m_sListName; private readonly string m_sInitialScanMsgLabel; private Dictionary<string, string> m_chkParams = new Dictionary<string, string>(); @@ -111,6 +113,12 @@ public void Initialize(LcmCache cache, IWritingSystemContainer wsContainer, ContextFont = contextFont; TokenGrid = tokenGrid; + var isOkToDisplayScripture = m_cache != null && m_cache.ServiceLocator.GetInstance<IScrBookRepository>().AllInstances().Any(); + if (isOkToDisplayScripture) + { + m_scrChecksDllFile = FwDirectoryFinder.BasicEditorialChecksDll; + } + if (m_ws != null) { if (m_ws.RightToLeftScript) @@ -201,6 +209,33 @@ private set } } + /// <summary> + /// Kinds of checks this control might run if activated. + /// </summary> + public enum CheckType + { + /// <summary> + /// Use PunctuationCheck + /// </summary> + Punctuation, + /// <summary> + /// Use CharactersCheck + /// </summary> + Characters + } + + /// ------------------------------------------------------------------------------------ + /// <summary> + /// + /// </summary> + /// ------------------------------------------------------------------------------------ + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public CheckType CheckToRun + { + get { return m_checkToRun; } + set { m_checkToRun = value; } + } + /// ------------------------------------------------------------------------------------ /// <summary> /// @@ -542,8 +577,10 @@ private List<TextTokenSubstring> ReadFile(string fileName) m_fileData = File.ReadAllLines(fileName); NormalizeFileData(); - var data = new TextFileDataSource(m_fileData, - ResourceHelper.GetResourceString("kstidFileLineRef"), CharacterCategorizer); + var data = new TextFileDataSource(m_scrChecksDllFile, + m_checkToRun == CheckType.Punctuation ? "PunctuationCheck" : "CharactersCheck", + m_fileData, + ResourceHelper.GetResourceString("kstidFileLineRef"), m_chkParams, CharacterCategorizer); tokens = data.GetReferences(); } @@ -615,7 +652,7 @@ public class ContextInfo /// <param name="tts">The TextTokenSubstring.</param> /// ------------------------------------------------------------------------------------ internal ContextInfo(PuncPattern pattern, TextTokenSubstring tts) : - this(pattern, tts.Offset, tts.FullTokenText) + this(pattern, tts.Offset, tts.FullTokenText, tts.FirstToken.ScrRefString) { } @@ -627,7 +664,7 @@ internal ContextInfo(PuncPattern pattern, TextTokenSubstring tts) : /// <param name="tts">The TextTokenSubstring.</param> /// ------------------------------------------------------------------------------------ internal ContextInfo(string chr, TextTokenSubstring tts) - : this(chr, tts.Offset, tts.FullTokenText) + : this(chr, tts.Offset, tts.FullTokenText, tts.FirstToken.ScrRefString) { } @@ -640,7 +677,7 @@ internal ContextInfo(string chr, TextTokenSubstring tts) /// <param name="context">The context (a string with the line contents).</param> /// <param name="reference">The reference (line number).</param> /// ------------------------------------------------------------------------------------ - internal ContextInfo(PuncPattern pattern, int offset, string context) + internal ContextInfo(PuncPattern pattern, int offset, string context, string reference) { m_position = pattern.ContextPos; string chr = pattern.Pattern; @@ -657,7 +694,7 @@ internal ContextInfo(PuncPattern pattern, int offset, string context) offset--; } } - Initialize(chr, offset, context); + Initialize(chr, offset, context, reference); } /// ------------------------------------------------------------------------------------ @@ -669,9 +706,9 @@ internal ContextInfo(PuncPattern pattern, int offset, string context) /// <param name="context">The context (a string with the line contents).</param> /// <param name="reference">The reference (line number).</param> /// ------------------------------------------------------------------------------------ - internal ContextInfo(string chr, int offset, string context) + internal ContextInfo(string chr, int offset, string context, string reference) { - Initialize(chr, offset, context); + Initialize(chr, offset, context, reference); } /// ------------------------------------------------------------------------------------ @@ -683,9 +720,10 @@ internal ContextInfo(string chr, int offset, string context) /// <param name="context">The context (a string with the line contents).</param> /// <param name="reference">The reference (line number).</param> /// ------------------------------------------------------------------------------------ - private void Initialize(string chr, int offset, string context) + private void Initialize(string chr, int offset, string context, string reference) { m_chr = chr; + m_ref = reference; int startPos = Math.Max(0, offset - 50); int length = Math.Max(0, (startPos == 0 ? offset : offset - startPos)); diff --git a/Src/FwCoreDlgs/FwCoreDlgsTests/CharContextCtrlTests.cs b/Src/FwCoreDlgs/FwCoreDlgsTests/CharContextCtrlTests.cs index fc7c4123e5..9b6c31899b 100644 --- a/Src/FwCoreDlgs/FwCoreDlgsTests/CharContextCtrlTests.cs +++ b/Src/FwCoreDlgs/FwCoreDlgsTests/CharContextCtrlTests.cs @@ -9,12 +9,102 @@ using System; using NUnit.Framework; using SIL.LCModel.Utils; +using System.Collections.Generic; using System.Text; +using SIL.FieldWorks.Common.FwUtils; using SIL.LCModel; using SIL.LCModel.Core.Text; namespace SIL.FieldWorks.FwCoreDlgs { + #region class DummyScrInventory + /// ---------------------------------------------------------------------------------------- + /// <summary> + /// Dummy class because NMock can't generate a dynamic mock for this interface. Grr... + /// </summary> + /// ---------------------------------------------------------------------------------------- + internal class DummyScrInventory : IScrCheckInventory + { + internal List<TextTokenSubstring> m_references; + #region IScrCheckInventory Members + + public List<TextTokenSubstring> GetReferences(IEnumerable<ITextToken> tokens, string desiredKey) + { + return m_references; + } + + public string InvalidItems + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public string InventoryColumnHeader + { + get { throw new NotImplementedException(); } + } + + public void Save() + { + throw new NotImplementedException(); + } + + public string ValidItems + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + #endregion + + #region IScriptureCheck Members + + public void Check(IEnumerable<ITextToken> toks, RecordErrorHandler record) + { + throw new NotImplementedException(); + } + + public string CheckGroup + { + get { throw new NotImplementedException(); } + } + + public Guid CheckId + { + get { throw new NotImplementedException(); } + } + + public string CheckName + { + get { throw new NotImplementedException(); } + } + + public string Description + { + get { throw new NotImplementedException(); } + } + + public float RelativeOrder + { + get { throw new NotImplementedException(); } + } + + #endregion + } + #endregion + #region class CharContextCtrlTests /// ---------------------------------------------------------------------------------------- /// <summary> diff --git a/Src/FwCoreDlgs/ValidCharactersDlg.cs b/Src/FwCoreDlgs/ValidCharactersDlg.cs index 2223f1585d..668e926986 100644 --- a/Src/FwCoreDlgs/ValidCharactersDlg.cs +++ b/Src/FwCoreDlgs/ValidCharactersDlg.cs @@ -700,6 +700,8 @@ public ValidCharactersDlg(LcmCache cache, IWritingSystemContainer wsContainer, contextCtrl.Initialize(cache, wsContainer, m_ws, m_app, fnt, gridCharInventory); contextCtrl.Dock = DockStyle.Fill; + contextCtrl.CheckToRun = CharContextCtrl.CheckType.Characters; + colChar.HeaderCell.SortGlyphDirection = SortOrder.Ascending; gridCharInventory.AutoGenerateColumns = false; diff --git a/build.ps1 b/build.ps1 index c1143e4676..010283a5c8 100644 --- a/build.ps1 +++ b/build.ps1 @@ -12,9 +12,6 @@ .PARAMETER Configuration The build configuration (Debug or Release). Default is Debug. -.PARAMETER Platform - The target platform. Only x64 is supported. Default is x64. - .PARAMETER Serial If set, disables parallel build execution (/m). Default is false (parallel enabled). @@ -80,37 +77,42 @@ Only used with -BuildInstaller. Enables local signing when signing tools are available. By default, local installer builds capture files to sign later instead of signing. -.PARAMETER UseLocalLcm - If set, builds liblcm from a local checkout (default: ../liblcm) after the FieldWorks build - and copies the resulting DLLs into the output directory, overwriting the NuGet package versions. - Use this to test local liblcm fixes without publishing a NuGet package. +.PARAMETER LcmMode + Controls how FieldWorks resolves liblcm. + - Auto: use package mode by default, but report whether local Localizations/LCM inputs are ready. + - Package: force the package-backed path. + - Local: force the nested Localizations/LCM source-backed path. -.PARAMETER LocalLcmPath - Path to the local liblcm repository. Defaults to ../liblcm relative to the FieldWorks repo root. - Only used when -UseLocalLcm is specified. +.PARAMETER ManagedDebugType + Optionally overrides the managed project PDB format for this build. + Use 'portable' for VS Code debugging. Windows PDBs are not supported by the VS Code debugger path used for FieldWorks. .PARAMETER SkipDependencyCheck If set, skips the dependency preflight check that verifies that required SDKs and tools are installed. .EXAMPLE .\build.ps1 - Builds Debug x64 in parallel with minimal logging. + Builds Debug in parallel with minimal logging. .EXAMPLE .\build.ps1 -Configuration Release -BuildTests - Builds Release x64 including test projects. + Builds Release including test projects. .EXAMPLE .\build.ps1 -RunTests - Builds Debug x64 including test projects and runs all tests. + Builds Debug including test projects and runs all tests. .EXAMPLE .\build.ps1 -Serial -Verbosity detailed - Builds Debug x64 serially with detailed logging. + Builds Debug serially with detailed logging. + +.EXAMPLE + .\build.ps1 -LcmMode Local + Builds FieldWorks against the nested Localizations/LCM checkout. .EXAMPLE - .\build.ps1 -UseLocalLcm - Builds FieldWorks, then builds liblcm from ../liblcm and copies DLLs into Output. + .\build.ps1 -LcmMode Local -ManagedDebugType portable + Builds FieldWorks against the nested Localizations/LCM checkout with portable managed PDBs for VS Code debugging. .NOTES FieldWorks is x64-only. The x86 platform is no longer supported. @@ -118,8 +120,6 @@ [CmdletBinding()] param( [string]$Configuration = "Debug", - [ValidateSet('x64')] - [string]$Platform = "x64", [switch]$Serial, [switch]$BuildTests, [switch]$RunTests, @@ -141,26 +141,53 @@ param( [switch]$ForceInstallerOnly, [switch]$SignInstaller, [switch]$TraceCrashes, - [switch]$UseLocalLcm, - [string]$LocalLcmPath, + [ValidateSet('Auto', 'Package', 'Local')] + [string]$LcmMode = 'Auto', + [ValidateSet('portable', 'full', 'pdbonly', 'embedded')] + [string]$ManagedDebugType, [switch]$SkipDependencyCheck ) $ErrorActionPreference = "Stop" -# Add WiX to the PATH for installer builds (required for harvesting localizations) -$env:PATH = "$env:WIX/bin;$env:PATH" +$platform = 'x64' +$validLcmModes = @('Auto', 'Package', 'Local') +# PowerShell requires single-dash named parameters. Some callers still pass GNU-style +# double-dash options, which bind positionally before the script starts. Normalize the +# common cases here so build.ps1 remains tolerant of that invocation style. if ($Configuration -like "--*") { - if ($Configuration -eq "--TraceCrashes" -and -not $TraceCrashes) { - $TraceCrashes = $true - $Configuration = "Debug" - Write-Output "[WARN] Detected '--TraceCrashes' passed without PowerShell switch parsing. Using -TraceCrashes and defaulting Configuration to Debug." - } - else { - throw "Invalid Configuration value '$Configuration'. Use -TraceCrashes (single dash) for the trace option." + $doubleDashOption = $Configuration.Substring(2) + switch ($doubleDashOption) { + 'TraceCrashes' { + if (-not $TraceCrashes) { + $TraceCrashes = $true + $Configuration = 'Debug' + Write-Output "[WARN] Detected '--TraceCrashes' passed without PowerShell switch parsing. Using -TraceCrashes and defaulting Configuration to Debug." + } + } + 'LcmMode' { + if ([string]::IsNullOrWhiteSpace($TestFilter)) { + throw "Detected '--LcmMode' without a mode value. Use -LcmMode <Auto|Package|Local>." + } + + $requestedMode = $TestFilter.Trim() + if ($requestedMode -notin $validLcmModes) { + throw "Invalid LCM mode '$requestedMode'. Use -LcmMode with one of: $($validLcmModes -join ', ')." + } + + $LcmMode = $requestedMode + $Configuration = 'Debug' + $TestFilter = '' + Write-Output "[WARN] Detected '--LcmMode $requestedMode' passed without PowerShell parameter parsing. Using -LcmMode $requestedMode and defaulting Configuration to Debug." + } + default { + throw "Invalid Configuration value '$Configuration'. Use PowerShell parameter syntax like -Configuration Release or -LcmMode Local." + } } } +# Add WiX to the PATH for installer builds (required for harvesting localizations) +$env:PATH = "$env:WIX/bin;$env:PATH" if ($BuildInstaller -and -not $BuildAdditionalApps) { $BuildAdditionalApps = $true @@ -275,6 +302,46 @@ function Get-RepoStamp { } } +function Get-DebugRebuildCheckPathspecs { + param( + [Parameter(Mandatory = $true)][ValidateSet('Package', 'Local')][string]$ResolvedLcmMode + ) + + $pathspecs = @( + 'build.ps1', + 'Directory.Build.props', + 'Directory.Build.targets', + 'Directory.Packages.props', + 'FieldWorks.proj', + 'Build', + 'Src', + 'Lib' + ) + + if ($ResolvedLcmMode -eq 'Local') { + $pathspecs += @('FieldWorks.LocalLcm.sln', 'Localizations/LCM') + } + else { + $pathspecs += 'FieldWorks.sln' + } + + return $pathspecs | ForEach-Object { $_ -replace '\\', '/' } +} + +function Get-GitStatusForDebugRebuildCheck { + param( + [Parameter(Mandatory = $true)][string[]]$Pathspecs + ) + + $gitArgs = @('status', '--porcelain=v1', '--untracked-files=all', '--') + $Pathspecs + $statusOutput = & git @gitArgs + if ($LASTEXITCODE -ne 0) { + throw "Failed to determine git status snapshot for build stamp." + } + + return @($statusOutput | ForEach-Object { $_.TrimEnd() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) +} + function Get-BuildStampPath { param( [Parameter(Mandatory = $true)][string]$RepoRoot, @@ -284,6 +351,52 @@ function Get-BuildStampPath { return Join-Path $outputDir "BuildStamp.json" } +function Get-LocalLcmState { + param( + [Parameter(Mandatory = $true)][string]$RepoRoot, + [Parameter(Mandatory = $true)][string]$ConfigurationName + ) + + $nestedRoot = Join-Path $RepoRoot 'Localizations\LCM' + $localSolution = Join-Path $RepoRoot 'FieldWorks.LocalLcm.sln' + $lcmSolution = Join-Path $nestedRoot 'LCM.sln' + $artifactsDir = Join-Path $nestedRoot ("artifacts\{0}\net462" -f $ConfigurationName) + $buildTasksPath = Join-Path $artifactsDir 'SIL.LCModel.Build.Tasks.dll' + + return [pscustomobject]@{ + NestedRoot = $nestedRoot + LocalSolutionPath = $localSolution + LcmSolutionPath = $lcmSolution + ArtifactsDir = $artifactsDir + NestedRootExists = (Test-Path $nestedRoot) + LocalSolutionExists = (Test-Path $localSolution) + LcmSolutionExists = (Test-Path $lcmSolution) + ArtifactsReady = (Test-Path $buildTasksPath) + BuildTasksPath = $buildTasksPath + } +} + +function Resolve-LcmMode { + param( + [Parameter(Mandatory = $true)][ValidateSet('Auto', 'Package', 'Local')][string]$RequestedMode, + [Parameter(Mandatory = $true)][string]$ProjectArgument + ) + + if ($RequestedMode -eq 'Local') { + return 'Local' + } + + if ($RequestedMode -eq 'Package') { + return 'Package' + } + + if ([System.IO.Path]::GetFileName($ProjectArgument) -ieq 'FieldWorks.LocalLcm.sln') { + return 'Local' + } + + return 'Package' +} + try { Invoke-WithFileLockRetry -Context "FieldWorks build" -IncludeOmniSharp -Action { # Initialize Visual Studio Developer environment @@ -363,10 +476,14 @@ try { # Properties $finalMsBuildArgs += "/p:Configuration=$Configuration" - $finalMsBuildArgs += "/p:Platform=$Platform" + $finalMsBuildArgs += "/p:Platform=$platform" if ($SkipNative) { $finalMsBuildArgs += "/p:SkipNative=true" } + if ($ManagedDebugType) { + $finalMsBuildArgs += "/p:DebugSymbols=true" + $finalMsBuildArgs += "/p:DebugType=$ManagedDebugType" + } $installerMsBuildArgs = $finalMsBuildArgs @@ -392,6 +509,36 @@ try { $finalMsBuildArgs += $MsBuildArgs $installerMsBuildArgs += $MsBuildArgs + $localLcmState = Get-LocalLcmState -RepoRoot $PSScriptRoot -ConfigurationName $Configuration + $resolvedLcmMode = Resolve-LcmMode -RequestedMode $LcmMode -ProjectArgument $Project + $useLocalLcmSource = ($resolvedLcmMode -eq 'Local') + $restoreSolution = if ($useLocalLcmSource) { $localLcmState.LocalSolutionPath } else { Join-Path $PSScriptRoot 'FieldWorks.sln' } + + Write-Host "LCM mode: $resolvedLcmMode (requested: $LcmMode)" -ForegroundColor Cyan + if ($ManagedDebugType) { + Write-Host "Managed debug symbols: $ManagedDebugType" -ForegroundColor Cyan + } + Write-Host "Local LCM checkout: $(if ($localLcmState.LcmSolutionExists) { 'ready' } elseif ($localLcmState.NestedRootExists) { 'partial' } else { 'missing' }) at $($localLcmState.NestedRoot)" -ForegroundColor Cyan + Write-Host "Local LCM artifacts: $(if ($localLcmState.ArtifactsReady) { 'ready' } else { 'missing' }) at $($localLcmState.ArtifactsDir)" -ForegroundColor Cyan + if ($LcmMode -eq 'Auto' -and -not $useLocalLcmSource -and $localLcmState.NestedRootExists) { + Write-Host "Auto mode kept the package-backed path. Use -LcmMode Local to build against Localizations/LCM." -ForegroundColor Yellow + } + + if ($useLocalLcmSource) { + if (-not $localLcmState.LocalSolutionExists) { + throw "Local LCM mode requested but FieldWorks.LocalLcm.sln was not found at $($localLcmState.LocalSolutionPath)." + } + if (-not $localLcmState.LcmSolutionExists) { + throw "Local LCM mode requested but the nested liblcm checkout was not found at $($localLcmState.LcmSolutionPath)." + } + if (-not $localLcmState.ArtifactsReady) { + Write-Host "Local LCM build tasks are missing from $($localLcmState.ArtifactsDir). The build will bootstrap them from source." -ForegroundColor Yellow + } + } + + $finalMsBuildArgs += "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" + $installerMsBuildArgs += "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" + # ============================================================================= # Build Execution # ============================================================================= @@ -399,7 +546,7 @@ try { Write-Host "" Write-Host "Building FieldWorks..." -ForegroundColor Cyan Write-Host "Project: $projectPath" -ForegroundColor Cyan - Write-Host "Configuration: $Configuration | Platform: $Platform | Parallel: $(-not $Serial) | Tests: $($BuildTests -or $RunTests)" -ForegroundColor Cyan + Write-Host "Configuration: $Configuration | Parallel: $(-not $Serial) | Tests: $($BuildTests -or $RunTests)" -ForegroundColor Cyan if ($BuildAdditionalApps) { Write-Host "Including optional FieldWorks executables" -ForegroundColor Yellow @@ -408,7 +555,7 @@ try { # Bootstrap: Build FwBuildTasks first (required by SetupInclude.targets) $fwBuildTasksOutputDir = Join-Path $PSScriptRoot "BuildTools/FwBuildTasks/$Configuration/" Invoke-MSBuild ` - -Arguments @('Build/Src/FwBuildTasks/FwBuildTasks.csproj', '/t:Restore;Build', "/p:Configuration=$Configuration", "/p:Platform=$Platform", ` + -Arguments @('Build/Src/FwBuildTasks/FwBuildTasks.csproj', '/t:Restore;Build', "/p:Configuration=$Configuration", "/p:Platform=$platform", ` "/p:FwBuildTasksOutputPath=$fwBuildTasksOutputDir", "/p:SkipFwBuildTasksAssemblyCheck=true", "/p:SkipFwBuildTasksUsingTask=true", "/p:SkipGenerateFwTargets=true", ` "/p:SkipSetupTargets=true", "/v:quiet", "/nologo") ` -Description 'FwBuildTasks (Bootstrap)' @@ -432,9 +579,9 @@ try { if (-not (Test-Path $packagesDir)) { New-Item -Path $packagesDir -ItemType Directory -Force | Out-Null } - & dotnet restore "$PSScriptRoot\FieldWorks.sln" /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true "/p:Configuration=$Configuration" "/p:Platform=$Platform" --verbosity quiet + & dotnet restore $restoreSolution /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true "/p:Configuration=$Configuration" "/p:Platform=$platform" "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" --verbosity quiet if ($LASTEXITCODE -ne 0) { - throw "NuGet package restore failed for FieldWorks.sln" + throw "NuGet package restore failed for $([System.IO.Path]::GetFileName($restoreSolution))" } Write-Host "Package restore complete." -ForegroundColor Green } else { @@ -477,8 +624,16 @@ try { $stampConfig = $stamp.Configuration $stampPlatform = $stamp.Platform - if (($stampConfig -ne $Configuration) -or ($stampPlatform -ne $Platform)) { - throw "-InstallerOnly stamp mismatch: stamp is Configuration='$stampConfig' Platform='$stampPlatform' but this run is Configuration='$Configuration' Platform='$Platform'. Run a full build in this configuration/platform." + $platformMismatch = ($stamp.PSObject.Properties.Name -contains 'Platform') -and ($stampPlatform -ne $platform) + if (($stampConfig -ne $Configuration) -or $platformMismatch) { + $stampDescription = if ($platformMismatch) { + "Configuration='$stampConfig' Platform='$stampPlatform'" + } + else { + "Configuration='$stampConfig'" + } + + throw "-InstallerOnly stamp mismatch: stamp is $stampDescription but this run is Configuration='$Configuration'. Run a full build in this configuration." } $headChanged = ($stamp.GitHead -ne $current.GitHead) @@ -508,12 +663,20 @@ try { } $repoStamp = Get-RepoStamp + $relevantDebugPathspecs = Get-DebugRebuildCheckPathspecs -ResolvedLcmMode $resolvedLcmMode + $relevantDebugStatus = Get-GitStatusForDebugRebuildCheck -Pathspecs $relevantDebugPathspecs $stampObject = [pscustomobject]@{ Configuration = $Configuration - Platform = $Platform + Platform = $platform + RequestedLcmMode = $LcmMode + ResolvedLcmMode = $resolvedLcmMode + UseLocalLcmSource = $useLocalLcmSource + ManagedDebugType = $(if ($ManagedDebugType) { $ManagedDebugType } else { '' }) GitHead = $repoStamp.GitHead IsDirty = $repoStamp.IsDirty IsDirtyOutsideInstaller = $repoStamp.IsDirtyOutsideInstaller + RelevantDebugPathspecs = $relevantDebugPathspecs + RelevantDebugStatus = $relevantDebugStatus TimestampUtc = (Get-Date).ToUniversalTime().ToString('o') } diff --git a/scripts/Agent/Copy-LocalLcm.ps1 b/scripts/Agent/Copy-LocalLcm.ps1 deleted file mode 100644 index a7ed2fd826..0000000000 --- a/scripts/Agent/Copy-LocalLcm.ps1 +++ /dev/null @@ -1,184 +0,0 @@ -<# -.SYNOPSIS - Copies locally-built LCM assemblies from an adjacent liblcm folder into the FieldWorks output directory. - -.DESCRIPTION - This script enables developers to test local liblcm fixes without publishing a NuGet package. - It builds liblcm from a local checkout (by default ../liblcm) and copies the resulting - assemblies into FieldWorks' Output/<Configuration> folder, overwriting the NuGet versions. - -.PARAMETER LcmRoot - Path to the liblcm repository root. Defaults to ../liblcm (relative to FieldWorks repo). - -.PARAMETER FwOutputDir - Path to FieldWorks output directory. Defaults to Output/<Configuration>. - -.PARAMETER Configuration - Build configuration (Debug or Release). Default is Debug. - -.PARAMETER BuildLcm - If set, builds liblcm before copying. If not set, just copies existing DLLs. - -.PARAMETER SkipConfirm - If set, skips the confirmation prompt. - -.EXAMPLE - .\Copy-LocalLcm.ps1 -BuildLcm - Builds liblcm from ../liblcm and copies DLLs to Output/Debug. - -.EXAMPLE - .\Copy-LocalLcm.ps1 -Configuration Release -BuildLcm - Builds liblcm for Release and copies to Output/Release. - -.NOTES - Use this for local debugging of liblcm issues. Never commit code that depends on this. -#> -[CmdletBinding()] -param( - [string]$LcmRoot, - [string]$FwOutputDir, - [string]$Configuration = "Debug", - [switch]$BuildLcm, - [switch]$SkipConfirm -) - -$ErrorActionPreference = "Stop" - -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "../..") - -if (-not $LcmRoot) { - $LcmRoot = Join-Path (Split-Path $repoRoot -Parent) "liblcm" -} - -if (-not (Test-Path $LcmRoot)) { - Write-Error "liblcm not found at '$LcmRoot'. Clone it there or specify -LcmRoot." - exit 1 -} - -$lcmSolution = Join-Path $LcmRoot "LCM.sln" -if (-not (Test-Path $lcmSolution)) { - Write-Error "LCM.sln not found at '$lcmSolution'. Is '$LcmRoot' a valid liblcm checkout?" - exit 1 -} - -if (-not $FwOutputDir) { - $FwOutputDir = Join-Path $repoRoot "Output\$Configuration" -} - -Write-Host "" -Write-Host "===============================================" -ForegroundColor Cyan -Write-Host " Local LCM Copy Utility" -ForegroundColor Cyan -Write-Host "===============================================" -ForegroundColor Cyan -Write-Host " LCM Source: $LcmRoot" -ForegroundColor White -Write-Host " FW Output: $FwOutputDir" -ForegroundColor White -Write-Host " Config: $Configuration" -ForegroundColor White -Write-Host " Build LCM: $($BuildLcm.IsPresent)" -ForegroundColor White -Write-Host "" - -if (-not $SkipConfirm) { - Write-Host "This will OVERWRITE NuGet LCM DLLs in the FW output directory." -ForegroundColor Yellow - Write-Host "Only use for local debugging - never commit changes that depend on this." -ForegroundColor Yellow - Write-Host "" - $confirm = Read-Host "Continue? [y/N]" - if ($confirm -notin @('y', 'Y', 'yes', 'Yes')) { - Write-Host "Aborted." -ForegroundColor Red - exit 0 - } -} - -# Build liblcm if requested -if ($BuildLcm) { - Write-Host "" - Write-Host "Building liblcm ($Configuration)..." -ForegroundColor Cyan - - Push-Location $LcmRoot - try { - # liblcm uses build.cmd for building - $buildScript = Join-Path $LcmRoot "build.cmd" - if (-not (Test-Path $buildScript)) { - throw "build.cmd not found at '$buildScript'. Is '$LcmRoot' a valid liblcm checkout?" - } - - Write-Host "Running: $buildScript" -ForegroundColor Gray - & cmd /c $buildScript - if ($LASTEXITCODE -ne 0) { - throw "liblcm build failed with exit code $LASTEXITCODE" - } - Write-Host "[OK] liblcm build complete." -ForegroundColor Green - } - finally { - Pop-Location - } -} - -# Find the LCM output directory -# liblcm builds to: artifacts/<Configuration>/net462/<dll> -$lcmBinDir = Join-Path $LcmRoot "artifacts\$Configuration\net462" -if (-not (Test-Path $lcmBinDir)) { - # Try net472 (alternative target) - $lcmBinDir = Join-Path $LcmRoot "artifacts\$Configuration\net472" -} -if (-not (Test-Path $lcmBinDir)) { - # Try net48 (newer versions) - $lcmBinDir = Join-Path $LcmRoot "artifacts\$Configuration\net48" -} -if (-not (Test-Path $lcmBinDir)) { - Write-Error "LCM bin directory not found. Build liblcm first with -BuildLcm or manually run 'build.cmd' in '$LcmRoot'." - exit 1 -} - -Write-Host "" -Write-Host "Copying LCM assemblies..." -ForegroundColor Cyan -Write-Host " From: $lcmBinDir" -ForegroundColor Gray - -# List of LCM assemblies to copy -$lcmAssemblies = @( - "SIL.LCModel.dll", - "SIL.LCModel.pdb", - "SIL.LCModel.Core.dll", - "SIL.LCModel.Core.pdb", - "SIL.LCModel.Utils.dll", - "SIL.LCModel.Utils.pdb" -) - -$copied = 0 -$missing = @() - -foreach ($asm in $lcmAssemblies) { - $sourcePath = Join-Path $lcmBinDir $asm - $destPath = Join-Path $FwOutputDir $asm - - if (Test-Path $sourcePath) { - if (-not (Test-Path $FwOutputDir)) { - New-Item -Path $FwOutputDir -ItemType Directory -Force | Out-Null - } - Copy-Item -Path $sourcePath -Destination $destPath -Force - Write-Host " [COPIED] $asm" -ForegroundColor Green - $copied++ - } - else { - # PDB files are optional - if ($asm -match '\.pdb$') { - Write-Host " [SKIP] $asm (not found)" -ForegroundColor Gray - } - else { - $missing += $asm - Write-Host " [WARN] $asm not found!" -ForegroundColor Yellow - } - } -} - -Write-Host "" -if ($missing.Count -gt 0) { - Write-Host "WARNING: Some assemblies were not found:" -ForegroundColor Yellow - $missing | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } - Write-Host "" - Write-Host "This may indicate liblcm wasn't built, or has a different output structure." -ForegroundColor Yellow - Write-Host "Try running: dotnet build LCM.sln -c $Configuration in '$LcmRoot'" -ForegroundColor Yellow - exit 1 -} - -Write-Host "[OK] Copied $copied LCM assembly file(s) to FW output." -ForegroundColor Green -Write-Host "" -Write-Host "IMPORTANT: These local DLLs will be overwritten on next clean build." -ForegroundColor Yellow -Write-Host "To persist the fix, it must be merged to liblcm and a new NuGet package published." -ForegroundColor Yellow diff --git a/scripts/Agent/Invoke-Installer.ps1 b/scripts/Agent/Invoke-Installer.ps1 index b4c2f3bb62..319421d359 100644 --- a/scripts/Agent/Invoke-Installer.ps1 +++ b/scripts/Agent/Invoke-Installer.ps1 @@ -8,10 +8,6 @@ param( [ValidateSet('Debug', 'Release')] [string]$Configuration = 'Debug', - [Parameter(Mandatory = $false)] - [ValidateSet('x64', 'x86')] - [string]$Platform = 'x64', - [Parameter(Mandatory = $false)] [string]$InstallerPath, @@ -46,6 +42,8 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +$platform = 'x64' + function Resolve-RepoRoot { $repoRoot = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') return $repoRoot.Path @@ -69,12 +67,10 @@ function Resolve-DefaultInstallerPath { [Parameter(Mandatory = $true)] [string]$ResolvedType, [Parameter(Mandatory = $true)] - [string]$Configuration, - [Parameter(Mandatory = $true)] - [string]$Platform + [string]$Configuration ) - $installerDir = Join-Path $RepoRoot ('FLExInstaller\bin\{0}\{1}' -f $Platform, $Configuration) + $installerDir = Join-Path $RepoRoot ('FLExInstaller\bin\{0}\{1}' -f $platform, $Configuration) if ($ResolvedType -eq 'Msi') { return Join-Path $installerDir 'en-US\FieldWorks.msi' @@ -180,7 +176,7 @@ $repoRoot = Resolve-RepoRoot $resolvedType = Resolve-InstallerType -InstallerType $InstallerType -InstallerPath $InstallerPath if ([string]::IsNullOrWhiteSpace($InstallerPath)) { - $InstallerPath = Resolve-DefaultInstallerPath -RepoRoot $repoRoot -ResolvedType $resolvedType -Configuration $Configuration -Platform $Platform + $InstallerPath = Resolve-DefaultInstallerPath -RepoRoot $repoRoot -ResolvedType $resolvedType -Configuration $Configuration } if (!(Test-Path -LiteralPath $InstallerPath)) { diff --git a/scripts/Agent/Invoke-InstallerCheck.ps1 b/scripts/Agent/Invoke-InstallerCheck.ps1 index 575a0e248b..541e43cede 100644 --- a/scripts/Agent/Invoke-InstallerCheck.ps1 +++ b/scripts/Agent/Invoke-InstallerCheck.ps1 @@ -8,10 +8,6 @@ param( [ValidateSet('Debug', 'Release')] [string]$Configuration = 'Debug', - [Parameter(Mandatory = $false)] - [ValidateSet('x64', 'x86')] - [string]$Platform = 'x64', - [Parameter(Mandatory = $false)] [string]$InstallerPath, @@ -75,7 +71,7 @@ Write-Output "Collecting snapshot: before install" & "$repoRoot\scripts\Agent\Collect-InstallerSnapshot.ps1" -OutputPath $beforePath -Name 'before-install' -MaxFileCount $MaxFileCount Write-Output "Running installer" -$installResult = & "$repoRoot\scripts\Agent\Invoke-Installer.ps1" -InstallerType $InstallerType -Configuration $Configuration -Platform $Platform -InstallerPath $InstallerPath -LogRoot $LogRoot -RunId $RunId -Arguments $InstallArguments -IncludeTempLogs -PassThru -NoExit +$installResult = & "$repoRoot\scripts\Agent\Invoke-Installer.ps1" -InstallerType $InstallerType -Configuration $Configuration -InstallerPath $InstallerPath -LogRoot $LogRoot -RunId $RunId -Arguments $InstallArguments -IncludeTempLogs -PassThru -NoExit if ($null -eq $installResult) { throw "Invoke-Installer did not return a result." @@ -97,7 +93,7 @@ if ($RunUninstall) { } else { Write-Output "Running uninstall" $uninstallRunId = "$RunId-uninstall" - $uninstallResult = & "$repoRoot\scripts\Agent\Invoke-Installer.ps1" -InstallerType 'Bundle' -Configuration $Configuration -Platform $Platform -InstallerPath $InstallerPath -LogRoot $LogRoot -RunId $uninstallRunId -Arguments $UninstallArguments -IncludeTempLogs -PassThru -NoExit + $uninstallResult = & "$repoRoot\scripts\Agent\Invoke-Installer.ps1" -InstallerType 'Bundle' -Configuration $Configuration -InstallerPath $InstallerPath -LogRoot $LogRoot -RunId $uninstallRunId -Arguments $UninstallArguments -IncludeTempLogs -PassThru -NoExit $uninstallExit = [int]$uninstallResult.ExitCode Write-Output "Uninstall exit code: $uninstallExit" diff --git a/test.ps1 b/test.ps1 index b5f5c81238..71edca16b4 100644 --- a/test.ps1 +++ b/test.ps1 @@ -236,6 +236,10 @@ try { # build.ps1 bootstraps this into BuildTools/FwBuildTasks/<Configuration>/FwBuildTasks.dll. $testDlls = @(Join-Path $PSScriptRoot "BuildTools/FwBuildTasks/$Configuration/FwBuildTasks.dll") } + elseif ($normalizedTestProject -match '(^|/)Lib/src/ScrChecks/ScrChecksTests($|/)') { + # ScrChecksTests builds under Lib/src and is not copied into Output/<Configuration>. + $testDlls = @(Join-Path $PSScriptRoot "Lib/src/ScrChecks/ScrChecksTests/bin/x64/$Configuration/net48/ScrChecksTests.dll") + } elseif ($TestProject -match '\.dll$') { $testDlls = @(Join-Path $outputDir (Split-Path $TestProject -Leaf)) } @@ -247,6 +251,31 @@ try { } $testDlls = @(Join-Path $outputDir "$projectName.dll") } + + # Fallback: some test projects build into their own bin folder and are not copied into Output/<Configuration>. + # If the expected Output/<Configuration>/<Name>.dll isn't present, look for bin/x64/<Configuration>/net48/<Name>.dll. + if ($testDlls.Count -eq 1 -and -not (Test-Path $testDlls[0]) -and ($TestProject -notmatch '\\.dll$')) { + $projectPathCandidate = Join-Path $PSScriptRoot $TestProject + + $projectDir = $null + $projectBaseName = $null + + if (Test-Path -LiteralPath $projectPathCandidate -PathType Container) { + $projectDir = $projectPathCandidate + $projectBaseName = Split-Path $projectDir -Leaf + } + elseif (Test-Path -LiteralPath $projectPathCandidate -PathType Leaf) { + $projectDir = Split-Path $projectPathCandidate -Parent + $projectBaseName = [System.IO.Path]::GetFileNameWithoutExtension($projectPathCandidate) + } + + if ($projectDir -and $projectBaseName) { + $binDll = Join-Path $projectDir "bin/x64/$Configuration/net48/$projectBaseName.dll" + if (Test-Path -LiteralPath $binDll -PathType Leaf) { + $testDlls = @($binDll) + } + } + } } else { # Find all test DLLs, excluding: @@ -257,6 +286,12 @@ try { $testDlls = Get-ChildItem -Path $outputDir -Filter "*Tests.dll" -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '^nunit|^Microsoft|^xunit|^SIL\.LCModel|^SIL\.WritingSystems\.Tests' } | Select-Object -ExpandProperty FullName + + # Some test projects (e.g., under Lib/src) are not copied into Output/<Configuration>. + $scrChecksTestsDll = Join-Path $PSScriptRoot "Lib/src/ScrChecks/ScrChecksTests/bin/x64/$Configuration/net48/ScrChecksTests.dll" + if (Test-Path $scrChecksTestsDll) { + $testDlls = @($testDlls + $scrChecksTestsDll | Select-Object -Unique) + } } $missingTestDlls = @($testDlls | Where-Object { -not (Test-Path $_) }) From a12287e4a9c448dc5d823aad68d5b9ef43617d05 Mon Sep 17 00:00:00 2001 From: John Lambert <john_lambert@sil.org> Date: Tue, 24 Mar 2026 19:33:39 -0400 Subject: [PATCH 2/3] Use local package workflow for SIL dependencies --- .gitignore | 1 + .vscode/launch.json | 7 +- .vscode/tasks.json | 34 +- Build/Agent/Invoke-VsCodeDebugBuild.ps1 | 28 +- Build/Agent/Pack-LocalDependencies.ps1 | 1155 ++++++++++++++++++++++ Build/SilVersions.props | 21 +- Build/Src/NativeBuild/NativeBuild.csproj | 4 +- Directory.Build.props | 8 - Directory.Build.targets | 37 - Directory.Packages.props | 38 +- Docs/architecture/dependencies.md | 94 +- Docs/architecture/liblcm-debugging.md | 56 +- FieldWorks.LocalLcm.sln | 1028 ------------------- Src/Common/FieldWorks/FieldWorks.cs | 4 +- build.ps1 | 208 ++-- nuget.config | 7 + test.ps1 | 34 +- 17 files changed, 1411 insertions(+), 1353 deletions(-) create mode 100644 Build/Agent/Pack-LocalDependencies.ps1 delete mode 100644 FieldWorks.LocalLcm.sln diff --git a/.gitignore b/.gitignore index 5ed27ca330..8379edd026 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ Collection.cpp .vs/ Build/GlobalInclude.properties Build/LibraryDevelopment.properties +Build/SilVersions.Local.props Build/NuGet.exe Build/nunit.framework.dll Build/nunit.framework.xml diff --git a/.vscode/launch.json b/.vscode/launch.json index 9f835bd181..d1a01c3827 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,10 +18,10 @@ } }, { - "name": "FieldWorks (.NET Framework, Local LCM)", + "name": "FieldWorks (.NET Framework, Local Packages)", "type": "clr", "request": "launch", - "preLaunchTask": "Prepare Debug (Local LCM)", + "preLaunchTask": "Prepare Debug (Local Packages)", "program": "${workspaceFolder}\\Output\\Debug\\FieldWorks.exe", "cwd": "${workspaceFolder}\\Output\\Debug", "console": "externalTerminal", @@ -29,8 +29,7 @@ "requireExactSource": false, "symbolOptions": { "searchPaths": [ - "${workspaceFolder}\\Output\\Debug", - "${workspaceFolder}\\Localizations\\LCM\\artifacts\\Debug\\net462" + "${workspaceFolder}\\Output\\Debug" ] } }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 15a8dcd2ce..78f0d4c969 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -211,12 +211,12 @@ { "label": "Build", "type": "shell", - "command": "./build.ps1 -LcmMode Auto", + "command": "./build.ps1", "group": { "kind": "build", "isDefault": true }, - "detail": "Build FieldWorks (package-backed by default; use explicit local LCM tasks for source mode)", + "detail": "Build FieldWorks against the pinned dependency packages", "options": { "shell": { "executable": "powershell.exe", @@ -228,9 +228,9 @@ { "label": "Build (Package)", "type": "shell", - "command": "./build.ps1 -LcmMode Package", + "command": "./build.ps1", "group": "build", - "detail": "Build FieldWorks against the pinned liblcm packages", + "detail": "Build FieldWorks against the pinned dependency packages", "options": { "shell": { "executable": "powershell.exe", @@ -242,9 +242,9 @@ { "label": "Build (Package, VS Code Debug)", "type": "shell", - "command": "./build.ps1 -LcmMode Package -ManagedDebugType portable", + "command": "./build.ps1 -ManagedDebugType portable", "group": "build", - "detail": "Build FieldWorks against pinned liblcm packages with portable managed PDBs for the VS Code debugger", + "detail": "Build FieldWorks against pinned dependency packages with portable managed PDBs for the VS Code debugger", "options": { "shell": { "executable": "powershell.exe", @@ -256,9 +256,9 @@ { "label": "Prepare Debug (Package)", "type": "shell", - "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -LcmMode Package -ManagedDebugType portable", + "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -ManagedDebugType portable", "group": "build", - "detail": "Build for VS Code debugging only when relevant files changed since the last successful package-mode portable-PDB debug build", + "detail": "Build for VS Code debugging only when relevant files changed since the last successful portable-PDB package-mode debug build", "options": { "shell": { "executable": "powershell.exe", @@ -268,11 +268,11 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Local LCM)", + "label": "Build (Local Packages)", "type": "shell", - "command": "./build.ps1 -LcmMode Local", + "command": "./build.ps1 -LocalPalaso -LocalLcm -LocalChorus", "group": "build", - "detail": "Build FieldWorks against the nested Localizations/LCM checkout", + "detail": "Pack libpalaso first, then liblcm and chorus in parallel, then build FieldWorks against those local packages", "options": { "shell": { "executable": "powershell.exe", @@ -282,11 +282,11 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Local LCM, VS Code Debug)", + "label": "Build (Local Packages, VS Code Debug)", "type": "shell", - "command": "./build.ps1 -LcmMode Local -ManagedDebugType portable", + "command": "./build.ps1 -LocalPalaso -LocalLcm -LocalChorus -ManagedDebugType portable", "group": "build", - "detail": "Build FieldWorks against the nested local LCM checkout with portable managed PDBs for the VS Code debugger", + "detail": "Build FieldWorks against locally packed dependency packages with portable FieldWorks PDBs for the VS Code debugger", "options": { "shell": { "executable": "powershell.exe", @@ -296,11 +296,11 @@ "problemMatcher": "$msCompile" }, { - "label": "Prepare Debug (Local LCM)", + "label": "Prepare Debug (Local Packages)", "type": "shell", - "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -LcmMode Local -ManagedDebugType portable", + "command": "./build.ps1 -LocalPalaso -LocalLcm -LocalChorus -ManagedDebugType portable", "group": "build", - "detail": "Build for VS Code debugging only when relevant files changed since the last successful local-LCM portable-PDB debug build", + "detail": "Pack the local dependency repos and build FieldWorks for VS Code debugging", "options": { "shell": { "executable": "powershell.exe", diff --git a/Build/Agent/Invoke-VsCodeDebugBuild.ps1 b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 index 367195a14b..dafb13aeaf 100644 --- a/Build/Agent/Invoke-VsCodeDebugBuild.ps1 +++ b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 @@ -1,7 +1,5 @@ [CmdletBinding()] param( - [ValidateSet('Package', 'Local')] - [string]$LcmMode, [ValidateSet('Debug', 'Release')] [string]$Configuration = 'Debug', [ValidateSet('full', 'portable', 'pdbonly', 'embedded')] @@ -16,28 +14,22 @@ $stampPath = Join-Path $outputDir 'BuildStamp.json' $runtimeExePath = Join-Path $outputDir 'FieldWorks.exe' function Get-DebugRebuildCheckPathspecs { - param( - [Parameter(Mandatory = $true)][ValidateSet('Package', 'Local')][string]$ResolvedLcmMode - ) - $pathspecs = @( 'build.ps1', + 'test.ps1', + 'nuget.config', 'Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', + 'Build/SilVersions.props', + 'Build/SilVersions.Local.props', 'FieldWorks.proj', + 'FieldWorks.sln', 'Build', 'Src', 'Lib' ) - if ($ResolvedLcmMode -eq 'Local') { - $pathspecs += @('FieldWorks.LocalLcm.sln', 'Localizations/LCM') - } - else { - $pathspecs += 'FieldWorks.sln' - } - return $pathspecs | ForEach-Object { $_ -replace '\\', '/' } } @@ -118,8 +110,6 @@ function Invoke-DebugBuild { 'Bypass', '-File', (Join-Path $repoRoot 'build.ps1'), - '-LcmMode', - $LcmMode, '-Configuration', $Configuration, '-ManagedDebugType', @@ -139,18 +129,16 @@ if (-not (Test-Path $stampPath) -or -not (Test-Path $runtimeExePath)) { } $stamp = Get-Content -LiteralPath $stampPath -Raw | ConvertFrom-Json -$resolvedLcmMode = if ($LcmMode -eq 'Local') { 'Local' } else { 'Package' } - -$modeMatches = ($stamp.PSObject.Properties.Name -contains 'ResolvedLcmMode') -and ($stamp.ResolvedLcmMode -eq $resolvedLcmMode) +$localDependencyMatches = ($stamp.PSObject.Properties.Name -contains 'LocalDependencies') -and (@($stamp.LocalDependencies).Count -eq 0) $debugTypeMatches = ($stamp.PSObject.Properties.Name -contains 'ManagedDebugType') -and ($stamp.ManagedDebugType -eq $ManagedDebugType) -if (-not $modeMatches -or -not $debugTypeMatches) { +if (-not $localDependencyMatches -or -not $debugTypeMatches) { Write-Host "Build stamp mode does not match requested VS Code debug mode. Rebuilding..." -ForegroundColor Yellow Invoke-DebugBuild exit 0 } -$pathspecsToCheck = Get-DebugRebuildCheckPathspecs -ResolvedLcmMode $resolvedLcmMode +$pathspecsToCheck = Get-DebugRebuildCheckPathspecs if (Test-GitStateRequiresDebugRebuild -Stamp $stamp -Pathspecs $pathspecsToCheck) { Invoke-DebugBuild exit 0 diff --git a/Build/Agent/Pack-LocalDependencies.ps1 b/Build/Agent/Pack-LocalDependencies.ps1 new file mode 100644 index 0000000000..56f6658fbf --- /dev/null +++ b/Build/Agent/Pack-LocalDependencies.ps1 @@ -0,0 +1,1155 @@ +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug', + [switch]$LocalPalaso, + [switch]$LocalLcm, + [switch]$LocalChorus, + [string]$LocalPackageVersion = '99.0.0-local' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$helpersPath = Join-Path $PSScriptRoot 'FwBuildHelpers.psm1' +if (Test-Path -LiteralPath $helpersPath) { + Import-Module $helpersPath -Force +} + +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$feedDir = Join-Path $repoRoot 'Output\LocalNuGetFeed' +$overridePath = Join-Path $repoRoot 'Build\SilVersions.Local.props' +$nugetSources = @( + $feedDir, + 'https://api.nuget.org/v3/index.json' +) + +$selectedDependencies = @() +if ($LocalPalaso) { + $selectedDependencies += 'Palaso' +} +if ($LocalLcm) { + $selectedDependencies += 'Lcm' +} +if ($LocalChorus) { + $selectedDependencies += 'Chorus' +} + +if ($selectedDependencies.Count -eq 0) { + if (Test-Path $overridePath) { + Remove-Item -LiteralPath $overridePath -Force + Write-Output 'Removed Build\SilVersions.Local.props; using pinned package versions.' + } + return +} + +$repoEnvVarByDependency = @{ + Palaso = 'FW_LOCAL_PALASO' + Lcm = 'FW_LOCAL_LCM' + Chorus = 'FW_LOCAL_CHORUS' +} + +$projectPathsByDependency = @{ + Palaso = @( + 'SIL.Archiving\SIL.Archiving.csproj', + 'SIL.Core\SIL.Core.csproj', + 'SIL.Core.Desktop\SIL.Core.Desktop.csproj', + 'SIL.Lexicon\SIL.Lexicon.csproj', + 'SIL.Lift\SIL.Lift.csproj', + 'SIL.Media\SIL.Media.csproj', + 'SIL.Scripture\SIL.Scripture.csproj', + 'SIL.TestUtilities\SIL.TestUtilities.csproj', + 'SIL.Windows.Forms\SIL.Windows.Forms.csproj', + 'SIL.Windows.Forms.Archiving\SIL.Windows.Forms.Archiving.csproj', + 'SIL.Windows.Forms.GeckoBrowserAdapter\SIL.Windows.Forms.GeckoBrowserAdapter.csproj', + 'SIL.Windows.Forms.Keyboarding\SIL.Windows.Forms.Keyboarding.csproj', + 'SIL.Windows.Forms.WritingSystems\SIL.Windows.Forms.WritingSystems.csproj', + 'SIL.WritingSystems\SIL.WritingSystems.csproj' + ) + Lcm = @( + 'src\CSTools\Tools\Tools.csproj', + 'src\SIL.LCModel\SIL.LCModel.csproj', + 'src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj', + 'src\SIL.LCModel.Core\SIL.LCModel.Core.csproj', + 'src\SIL.LCModel.FixData\SIL.LCModel.FixData.csproj', + 'src\SIL.LCModel.Utils\SIL.LCModel.Utils.csproj', + 'tests\SIL.LCModel.Core.Tests\SIL.LCModel.Core.Tests.csproj', + 'tests\SIL.LCModel.Tests\SIL.LCModel.Tests.csproj', + 'tests\SIL.LCModel.Utils.Tests\SIL.LCModel.Utils.Tests.csproj' + ) + Chorus = @( + 'src\Chorus\Chorus.csproj', + 'src\LibChorus\LibChorus.csproj' + ) +} + +$switchNameByDependency = @{ + Palaso = 'LocalPalaso' + Lcm = 'LocalLcm' + Chorus = 'LocalChorus' +} + +$packageIdsByDependency = @{ + Palaso = @( + 'sil.archiving', + 'sil.core', + 'sil.core.desktop', + 'sil.lexicon', + 'sil.lift', + 'sil.media', + 'sil.scripture', + 'sil.testutilities', + 'sil.windows.forms', + 'sil.windows.forms.archiving', + 'sil.windows.forms.geckobrowseradapter', + 'sil.windows.forms.keyboarding', + 'sil.windows.forms.writingsystems', + 'sil.writingsystems' + ) + Lcm = @( + 'sil.lcmodel.tools', + 'sil.lcmodel', + 'sil.lcmodel.build.tasks', + 'sil.lcmodel.core', + 'sil.lcmodel.fixdata', + 'sil.lcmodel.utils', + 'sil.lcmodel.core.tests', + 'sil.lcmodel.tests', + 'sil.lcmodel.utils.tests' + ) + Chorus = @( + 'sil.chorus.app', + 'sil.chorus.libchorus' + ) +} + +$projectsWithoutSymbolPackages = @( + 'src\CSTools\Tools\Tools.csproj', + 'src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj', + 'tests\SIL.LCModel.Core.Tests\SIL.LCModel.Core.Tests.csproj', + 'tests\SIL.LCModel.Tests\SIL.LCModel.Tests.csproj', + 'tests\SIL.LCModel.Utils.Tests\SIL.LCModel.Utils.Tests.csproj' +) + +function Get-LocalDependencyStampDirectory { + return (Join-Path $feedDir '.stamp') +} + +function Get-LocalDependencyStampPath { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + return (Join-Path (Get-LocalDependencyStampDirectory) ("{0}.json" -f $DependencyName)) +} + +function Get-StringHash { + param( + [AllowEmptyString()] + [string]$Value + ) + + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value) + $hashBytes = $sha256.ComputeHash($bytes) + return ([System.BitConverter]::ToString($hashBytes).Replace('-', '').ToLowerInvariant()) + } + finally { + $sha256.Dispose() + } +} + +function Get-DependencyRepoFingerprint { + param( + [Parameter(Mandatory = $true)] + [string]$RepoPath + ) + + $resolvedRepoPath = [System.IO.Path]::GetFullPath($RepoPath) + $gitHeadOutput = @(& git -C $resolvedRepoPath rev-parse HEAD 2>$null) + if ($LASTEXITCODE -ne 0 -or $gitHeadOutput.Count -eq 0) { + throw "Could not determine git HEAD for local dependency repo '$resolvedRepoPath'." + } + + $gitHead = ($gitHeadOutput -join "`n").Trim() + $statusLines = @(& git -C $resolvedRepoPath status --porcelain=v1 --untracked-files=all 2>$null) + if ($LASTEXITCODE -ne 0) { + throw "Could not determine git status for local dependency repo '$resolvedRepoPath'." + } + + if ($statusLines.Count -eq 0) { + return [pscustomobject]@{ + RepoPath = $resolvedRepoPath + GitHead = $gitHead + IsDirty = $false + Fingerprint = "clean:$gitHead" + } + } + + $diffText = (@(& git -C $resolvedRepoPath diff --no-ext-diff --binary HEAD -- . 2>$null) -join "`n") + if ($LASTEXITCODE -ne 0) { + throw "Could not determine git diff for local dependency repo '$resolvedRepoPath'." + } + + $untrackedFileDescriptors = foreach ($statusLine in $statusLines | Where-Object { $_.StartsWith('?? ') }) { + if ($statusLine.Length -lt 4) { + continue + } + + $relativePath = $statusLine.Substring(3) + $fullPath = Join-Path $resolvedRepoPath $relativePath + if (Test-Path -LiteralPath $fullPath -PathType Leaf) { + $fileInfo = Get-Item -LiteralPath $fullPath + $fileHash = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash.ToLowerInvariant() + "$relativePath|$($fileInfo.Length)|$($fileInfo.LastWriteTimeUtc.Ticks)|$fileHash" + } + else { + "$relativePath|missing" + } + } + + $fingerprintSource = @( + $gitHead, + ($statusLines -join "`n"), + $diffText, + ($untrackedFileDescriptors -join "`n") + ) -join "`n---`n" + + return [pscustomobject]@{ + RepoPath = $resolvedRepoPath + GitHead = $gitHead + IsDirty = $true + Fingerprint = "dirty:$(Get-StringHash -Value $fingerprintSource)" + } +} + +function Read-DependencyStamp { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + $stampPath = Get-LocalDependencyStampPath -DependencyName $DependencyName + if (-not (Test-Path -LiteralPath $stampPath -PathType Leaf)) { + return $null + } + + return (Get-Content -LiteralPath $stampPath -Raw | ConvertFrom-Json) +} + +function Test-DependencyFeedArtifactsExist { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName, + [Parameter(Mandatory = $true)] + [string]$PackageVersion + ) + + foreach ($packageId in $packageIdsByDependency[$DependencyName]) { + $packageArtifact = Get-ChildItem -LiteralPath $feedDir -Filter "$packageId.$PackageVersion.nupkg" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike '*.snupkg' } | + Select-Object -First 1 + + if ($null -eq $packageArtifact) { + return $false + } + } + + return $true +} + +function Write-DependencyStamp { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName, + [Parameter(Mandatory = $true)] + [string]$RepoPath, + [Parameter(Mandatory = $true)] + [pscustomobject]$FingerprintInfo, + [Parameter(Mandatory = $true)] + [string]$PackageVersion + ) + + $stampDir = Get-LocalDependencyStampDirectory + New-Item -Path $stampDir -ItemType Directory -Force | Out-Null + + $stampObject = [pscustomobject]@{ + DependencyName = $DependencyName + RepoPath = [System.IO.Path]::GetFullPath($RepoPath) + Configuration = $Configuration + RequestedLocalPackageVersion = $LocalPackageVersion + PackageVersion = $PackageVersion + GitHead = $FingerprintInfo.GitHead + IsDirty = $FingerprintInfo.IsDirty + Fingerprint = $FingerprintInfo.Fingerprint + PackageIds = @($packageIdsByDependency[$DependencyName]) + } + + $stampPath = Get-LocalDependencyStampPath -DependencyName $DependencyName + $stampObject | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $stampPath -Encoding UTF8 +} + +function Get-DependencyReuseState { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName, + [Parameter(Mandatory = $true)] + [string]$RepoPath + ) + + $fingerprintInfo = Get-DependencyRepoFingerprint -RepoPath $RepoPath + $stamp = Read-DependencyStamp -DependencyName $DependencyName + if ($null -eq $stamp) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'no stamp' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + $resolvedRepoPath = [System.IO.Path]::GetFullPath($RepoPath) + if ($stamp.RepoPath -ne $resolvedRepoPath) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'repo path changed' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + if ($stamp.Configuration -ne $Configuration) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'configuration changed' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + if ($stamp.RequestedLocalPackageVersion -ne $LocalPackageVersion) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'requested package version changed' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + if ($stamp.Fingerprint -ne $fingerprintInfo.Fingerprint) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'repo contents changed' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + $packageVersion = [string]$stamp.PackageVersion + if ([string]::IsNullOrWhiteSpace($packageVersion)) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'stamp missing package version' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + if (-not (Test-DependencyFeedArtifactsExist -DependencyName $DependencyName -PackageVersion $packageVersion)) { + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $false + Reason = 'feed artifacts missing' + FingerprintInfo = $fingerprintInfo + PackageVersion = '' + } + } + + return [pscustomobject]@{ + DependencyName = $DependencyName + CanReuse = $true + Reason = 'stamp matched' + FingerprintInfo = $fingerprintInfo + PackageVersion = $packageVersion + } +} + +function Get-DependencyRepoPath { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + $envVarName = $repoEnvVarByDependency[$DependencyName] + $repoPath = [Environment]::GetEnvironmentVariable($envVarName) + if ([string]::IsNullOrWhiteSpace($repoPath)) { + throw "-$($switchNameByDependency[$DependencyName]) requires environment variable $envVarName to point to the $DependencyName repo checkout." + } + + if (-not (Test-Path -LiteralPath $repoPath -PathType Container)) { + throw "$envVarName points to '$repoPath', but that directory does not exist." + } + + foreach ($relativeProjectPath in $projectPathsByDependency[$DependencyName]) { + $projectPath = Join-Path $repoPath $relativeProjectPath + if (-not (Test-Path -LiteralPath $projectPath -PathType Leaf)) { + throw "$envVarName points to '$repoPath', but required project '$relativeProjectPath' was not found there." + } + } + + return $repoPath +} + +function Remove-PackageCacheDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$PackageDir + ) + + for ($attempt = 1; $attempt -le 2; $attempt++) { + try { + Remove-Item -LiteralPath $PackageDir -Recurse -Force + break + } + catch { + $fileLockDetected = (Get-Command Test-IsFileLockError -ErrorAction SilentlyContinue) -and (Test-IsFileLockError -ErrorRecord $_) + if ($attempt -lt 2 -and $fileLockDetected) { + Write-Warning "Package cache cleanup hit a file lock for '$PackageDir'. Stopping stale build processes and retrying." + if (Get-Command Stop-ConflictingProcesses -ErrorAction SilentlyContinue) { + Stop-ConflictingProcesses -IncludeOmniSharp -RepoRoot $repoRoot + } + Start-Sleep -Seconds 2 + continue + } + + if ($fileLockDetected) { + Write-Warning "Package cache cleanup could not remove '$PackageDir' because files are still locked. Continuing with the existing extracted package cache." + break + } + + throw + } + } +} + +function Test-IsPathAlreadyGoneError { + param( + [Parameter(Mandatory = $true)] + [System.Management.Automation.ErrorRecord]$ErrorRecord + ) + + if ($ErrorRecord.Exception -is [System.IO.FileNotFoundException] -or + $ErrorRecord.Exception -is [System.IO.DirectoryNotFoundException] -or + $ErrorRecord.Exception -is [System.Management.Automation.ItemNotFoundException]) { + return $true + } + + $message = $ErrorRecord.Exception.Message + return ($message -like 'Could not find file*' -or $message -like 'Could not find a part of the path*' -or $message -like 'Cannot find path*') +} + +function Get-LocalNuGetCacheRoot { + return (Join-Path $repoRoot 'Output\LocalNuGetCache') +} + +function Test-IsDependencyTempCacheDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$DirectoryName, + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + foreach ($dependency in $Dependencies) { + if ($DirectoryName -match ("^{0}(?:-l10n)?-[0-9a-f]{{32}}$" -f [regex]::Escape($dependency))) { + return $true + } + } + + return $false +} + +function Remove-TempNuGetCacheDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$CacheDir, + [switch]$SkipIfLocked + ) + + if (-not (Test-Path -LiteralPath $CacheDir -PathType Container)) { + return + } + + for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + Remove-Item -LiteralPath $CacheDir -Recurse -Force + return + } + catch { + if (Test-IsPathAlreadyGoneError -ErrorRecord $_) { + if (-not (Test-Path -LiteralPath $CacheDir -PathType Container)) { + return + } + + if ($attempt -lt 3) { + Start-Sleep -Milliseconds 500 + continue + } + + return + } + + $fileLockDetected = (Get-Command Test-IsFileLockError -ErrorAction SilentlyContinue) -and (Test-IsFileLockError -ErrorRecord $_) + if ($SkipIfLocked -and $fileLockDetected) { + Write-Output "Skipping in-use temp NuGet cache '$CacheDir'." + return + } + + if ($attempt -lt 3 -and $fileLockDetected) { + Start-Sleep -Seconds 2 + continue + } + + throw + } + } +} + + +function Clear-LocalNuGetCacheRoot { + param( + [switch]$SkipIfLocked + ) + + $cacheRoot = Get-LocalNuGetCacheRoot + if (-not (Test-Path -LiteralPath $cacheRoot -PathType Container)) { + return + } + + Remove-TempNuGetCacheDirectory -CacheDir $cacheRoot -SkipIfLocked:$SkipIfLocked +} + +function Clear-FieldWorksPackageCache { + param( + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + $packagesRoot = Join-Path $repoRoot 'packages' + if (-not (Test-Path -LiteralPath $packagesRoot -PathType Container)) { + return + } + + foreach ($dependency in $Dependencies) { + foreach ($packageId in $packageIdsByDependency[$dependency]) { + $packageDir = Join-Path $packagesRoot $packageId + if (Test-Path -LiteralPath $packageDir) { + Remove-PackageCacheDirectory -PackageDir $packageDir + } + } + } +} + +function Write-LocalOverrideFile { + param( + [Parameter(Mandatory = $true)] + [hashtable]$DependencyVersions, + [string]$L10NSharpVersion + ) + + $overrideLines = @( + '<Project>', + ' <PropertyGroup Label="Local SIL dependency overrides">' + ) + + if ($LocalPalaso) { + $overrideLines += " <SilLibPalasoVersion>$($DependencyVersions['Palaso'])</SilLibPalasoVersion>" + if (-not [string]::IsNullOrWhiteSpace($L10NSharpVersion)) { + $overrideLines += " <L10NSharpVersion>$L10NSharpVersion</L10NSharpVersion>" + } + } + + if ($LocalLcm) { + $overrideLines += " <SilLcmVersion>$($DependencyVersions['Lcm'])</SilLcmVersion>" + } + + if ($LocalChorus) { + $overrideLines += " <SilChorusVersion>$($DependencyVersions['Chorus'])</SilChorusVersion>" + } + + $overrideLines += @( + ' </PropertyGroup>', + '</Project>' + ) + + Set-Content -LiteralPath $overridePath -Value $overrideLines -Encoding UTF8 + Write-Output "Wrote local dependency version overrides to $overridePath." +} + +function Sync-FieldWorksPackageCache { + param( + [Parameter(Mandatory = $true)] + [hashtable]$DependencyVersions, + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + $packagesRoot = Join-Path $repoRoot 'packages' + New-Item -Path $packagesRoot -ItemType Directory -Force | Out-Null + Add-Type -AssemblyName System.IO.Compression.FileSystem + + foreach ($dependency in $Dependencies) { + $packageVersion = $DependencyVersions[$dependency] + foreach ($packageId in $packageIdsByDependency[$dependency]) { + $packageArtifact = Get-ChildItem -LiteralPath $feedDir -Filter "$packageId.$packageVersion.nupkg" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike '*.snupkg' } | + Select-Object -First 1 + + if ($null -eq $packageArtifact) { + continue + } + + $packageDir = Join-Path (Join-Path $packagesRoot $packageId) $packageVersion + New-Item -Path $packageDir -ItemType Directory -Force | Out-Null + + $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($packageArtifact.FullName) + try { + foreach ($entry in $zipArchive.Entries) { + if ([string]::IsNullOrWhiteSpace($entry.FullName)) { + continue + } + + $destinationPath = Join-Path $packageDir ($entry.FullName -replace '/', '\\') + if ($entry.FullName.EndsWith('/')) { + New-Item -Path $destinationPath -ItemType Directory -Force | Out-Null + continue + } + + $destinationDir = Split-Path $destinationPath -Parent + if (-not (Test-Path -LiteralPath $destinationDir)) { + New-Item -Path $destinationDir -ItemType Directory -Force | Out-Null + } + + $shouldCopy = $true + if (Test-Path -LiteralPath $destinationPath -PathType Leaf) { + $existingFile = Get-Item -LiteralPath $destinationPath + $entryTimestampUtc = $entry.LastWriteTime.UtcDateTime + $existingTimestampUtc = $existingFile.LastWriteTimeUtc + $shouldCopy = ($existingFile.Length -ne $entry.Length) -or ($existingTimestampUtc -ne $entryTimestampUtc) + } + + if (-not $shouldCopy) { + continue + } + + try { + $entryStream = $entry.Open() + try { + $fileStream = [System.IO.File]::Open($destinationPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read) + try { + $entryStream.CopyTo($fileStream) + } + finally { + $fileStream.Dispose() + } + } + finally { + $entryStream.Dispose() + } + + [System.IO.File]::SetLastWriteTimeUtc($destinationPath, $entry.LastWriteTime.UtcDateTime) + } + catch { + Write-Warning "Could not refresh extracted package file '$destinationPath'. Continuing with the existing file." + } + } + } + finally { + $zipArchive.Dispose() + } + } + } +} + +function Get-PackageReferenceVersion { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectPath, + [Parameter(Mandatory = $true)] + [string]$PackageId + ) + + [xml]$projectXml = Get-Content -LiteralPath $ProjectPath -Raw + $packageReference = Select-Xml -Xml $projectXml -XPath "//*[local-name()='PackageReference' and @Include='$PackageId']" | + Select-Object -First 1 + + if ($null -eq $packageReference) { + throw "PackageReference '$PackageId' was not found in $ProjectPath." + } + + return $packageReference.Node.Version +} + +function Get-ProjectPackageId { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectPath + ) + + [xml]$projectXml = Get-Content -LiteralPath $ProjectPath -Raw + $propertyGroups = @($projectXml.Project.PropertyGroup) + + foreach ($propertyGroup in $propertyGroups) { + if (($propertyGroup.PSObject.Properties.Name -contains 'PackageId') -and -not [string]::IsNullOrWhiteSpace($propertyGroup.PackageId)) { + return $propertyGroup.PackageId + } + } + + foreach ($propertyGroup in $propertyGroups) { + if (($propertyGroup.PSObject.Properties.Name -contains 'AssemblyName') -and -not [string]::IsNullOrWhiteSpace($propertyGroup.AssemblyName)) { + return $propertyGroup.AssemblyName + } + } + + return (Split-Path $ProjectPath -LeafBase) +} + +function Clear-LocalFeedPackages { + param( + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + if (-not (Test-Path -LiteralPath $feedDir -PathType Container)) { + return + } + + foreach ($dependency in $Dependencies) { + foreach ($packageId in $packageIdsByDependency[$dependency]) { + Get-ChildItem -LiteralPath $feedDir -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "$packageId.*.nupkg" -or $_.Name -like "$packageId.*.snupkg" } | + Remove-Item -Force + } + } +} + +function Get-DependencyPackageVersion { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + $versions = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase) + + foreach ($packageId in $packageIdsByDependency[$DependencyName]) { + $packageFile = Get-ChildItem -LiteralPath $feedDir -Filter "$packageId.*.nupkg" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike '*.snupkg' } | + Select-Object -First 1 + + if ($null -eq $packageFile) { + throw "Packed package '$packageId' was not found in $feedDir." + } + + $pattern = '^{0}\.(.+)\.nupkg$' -f [regex]::Escape($packageId) + $match = [regex]::Match($packageFile.Name, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + if (-not $match.Success) { + throw "Could not determine the package version from '$($packageFile.Name)'." + } + + [void]$versions.Add($match.Groups[1].Value) + } + + $resolvedVersions = @($versions) + if ($resolvedVersions.Count -ne 1) { + throw "Expected a single package version for $DependencyName, but found: $($resolvedVersions -join ', ')." + } + + return $resolvedVersions[0] +} + +function New-TempNuGetConfig { + param( + [Parameter(Mandatory = $true)] + [string]$ConfigPath, + [Parameter(Mandatory = $true)] + [string[]]$Sources + ) + + $configLines = @( + '<?xml version="1.0" encoding="utf-8"?>', + '<configuration>', + ' <packageSources>', + ' <clear />' + ) + + for ($index = 0; $index -lt $Sources.Count; $index++) { + $configLines += (' <add key="source{0}" value="{1}" />' -f $index, $Sources[$index]) + } + + $configLines += @( + ' </packageSources>', + '</configuration>' + ) + + Set-Content -LiteralPath $ConfigPath -Value $configLines -Encoding UTF8 + return $ConfigPath +} + +function New-DependencyCacheDir { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + $cacheRoot = Get-LocalNuGetCacheRoot + New-Item -Path $cacheRoot -ItemType Directory -Force | Out-Null + + $cacheDir = Join-Path $cacheRoot ("{0}-{1}" -f $DependencyName, [guid]::NewGuid().ToString('N')) + New-Item -Path $cacheDir -ItemType Directory -Force | Out-Null + return $cacheDir +} + +function Test-PackedProjectArtifactExists { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectPath, + [Parameter(Mandatory = $true)] + [string]$PackageFeedDir + ) + + $packageIdPrefix = Get-ProjectPackageId -ProjectPath $ProjectPath + $packageArtifact = Get-ChildItem -LiteralPath $PackageFeedDir -Filter "$packageIdPrefix.*.nupkg" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike '*.snupkg' } | + Select-Object -First 1 + + return ($null -ne $packageArtifact) +} + +function Invoke-Pack { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName, + [Parameter(Mandatory = $true)] + [string]$RepoPath + ) + + $cacheDir = New-DependencyCacheDir -DependencyName $DependencyName + $nugetConfigPath = Join-Path $cacheDir 'nuget.config' + + Write-Output "Packing $DependencyName from $RepoPath" + + $previousNugetPackages = [Environment]::GetEnvironmentVariable('NUGET_PACKAGES') + try { + $env:NUGET_PACKAGES = $cacheDir + New-TempNuGetConfig -ConfigPath $nugetConfigPath -Sources $nugetSources | Out-Null + + foreach ($relativeProjectPath in $projectPathsByDependency[$DependencyName]) { + $projectPath = Join-Path $RepoPath $relativeProjectPath + $restoreArgs = @( + 'restore', + $projectPath, + '--configfile', + $nugetConfigPath, + '--disable-build-servers', + '--verbosity', + 'minimal' + ) + + & dotnet @restoreArgs + if ($LASTEXITCODE -ne 0) { + throw "dotnet restore failed for $DependencyName project $projectPath." + } + + $packArgs = @( + 'pack', + $projectPath, + '-c', + $Configuration, + '--output', + $feedDir, + '--no-restore', + '--disable-build-servers', + '--verbosity', + 'minimal', + "-p:PackageVersion=$LocalPackageVersion" + ) + + if ($projectsWithoutSymbolPackages -contains $relativeProjectPath) { + $packArgs += '-p:DebugSymbols=false' + } + else { + $packArgs += @( + '-p:DebugType=embedded', + '-p:DebugSymbols=true' + ) + } + + & dotnet @packArgs + if ($LASTEXITCODE -ne 0) { + if (($projectsWithoutSymbolPackages -contains $relativeProjectPath) -and (Test-PackedProjectArtifactExists -ProjectPath $projectPath -PackageFeedDir $feedDir)) { + Write-Warning "$DependencyName project $projectPath returned a non-zero exit code after creating its main package artifact. Continuing without a symbol package." + continue + } + + throw "dotnet pack failed for $DependencyName project $projectPath." + } + } + } + finally { + if ([string]::IsNullOrWhiteSpace($previousNugetPackages)) { + Remove-Item Env:NUGET_PACKAGES -ErrorAction SilentlyContinue + } + else { + $env:NUGET_PACKAGES = $previousNugetPackages + } + + Remove-TempNuGetCacheDirectory -CacheDir $cacheDir + } +} + +function Start-PackJob { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName, + [Parameter(Mandatory = $true)] + [string]$RepoPath + ) + + $cacheDir = New-DependencyCacheDir -DependencyName $DependencyName + + return Start-Job -Name $DependencyName -ScriptBlock { + param($JobDependencyName, $JobRepoPath, $JobProjectPaths, $JobProjectsWithoutSymbolPackages, $JobConfiguration, $JobFeedDir, $JobLocalPackageVersion, $JobCacheDir, $JobSources) + + Set-StrictMode -Version Latest + $ErrorActionPreference = 'Stop' + + function Remove-JobTempNuGetCacheDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$CacheDir + ) + + if (-not (Test-Path -LiteralPath $CacheDir -PathType Container)) { + return + } + + for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + Remove-Item -LiteralPath $CacheDir -Recurse -Force + return + } + catch { + if ($attempt -lt 3) { + Start-Sleep -Seconds 2 + continue + } + + throw + } + } + } + + $env:NUGET_PACKAGES = $JobCacheDir + $jobNugetConfigPath = Join-Path $JobCacheDir 'nuget.config' + $configLines = @( + '<?xml version="1.0" encoding="utf-8"?>', + '<configuration>', + ' <packageSources>', + ' <clear />' + ) + + for ($index = 0; $index -lt $JobSources.Count; $index++) { + $configLines += (' <add key="source{0}" value="{1}" />' -f $index, $JobSources[$index]) + } + + $configLines += @( + ' </packageSources>', + '</configuration>' + ) + + try { + Set-Content -LiteralPath $jobNugetConfigPath -Value $configLines -Encoding UTF8 + foreach ($relativeProjectPath in $JobProjectPaths) { + $projectPath = Join-Path $JobRepoPath $relativeProjectPath + $restoreArgs = @( + 'restore', + $projectPath, + '--configfile', + $jobNugetConfigPath, + '--disable-build-servers', + '--verbosity', + 'minimal' + ) + + & dotnet @restoreArgs + if ($LASTEXITCODE -ne 0) { + throw "dotnet restore failed for $JobDependencyName project $projectPath." + } + + $packArgs = @( + 'pack', + $projectPath, + '-c', + $JobConfiguration, + '--output', + $JobFeedDir, + '--no-restore', + '--disable-build-servers', + '--verbosity', + 'minimal', + "-p:PackageVersion=$JobLocalPackageVersion" + ) + + if ($JobProjectsWithoutSymbolPackages -contains $relativeProjectPath) { + $packArgs += '-p:DebugSymbols=false' + } + else { + $packArgs += @( + '-p:DebugType=embedded', + '-p:DebugSymbols=true' + ) + } + + & dotnet @packArgs + if ($LASTEXITCODE -ne 0) { + if ($JobProjectsWithoutSymbolPackages -contains $relativeProjectPath) { + [xml]$jobProjectXml = Get-Content -LiteralPath $projectPath -Raw + $jobPropertyGroups = @($jobProjectXml.Project.PropertyGroup) + $packageIdPrefix = $null + foreach ($jobPropertyGroup in $jobPropertyGroups) { + if (($jobPropertyGroup.PSObject.Properties.Name -contains 'PackageId') -and -not [string]::IsNullOrWhiteSpace($jobPropertyGroup.PackageId)) { + $packageIdPrefix = $jobPropertyGroup.PackageId + break + } + } + if ([string]::IsNullOrWhiteSpace($packageIdPrefix)) { + foreach ($jobPropertyGroup in $jobPropertyGroups) { + if (($jobPropertyGroup.PSObject.Properties.Name -contains 'AssemblyName') -and -not [string]::IsNullOrWhiteSpace($jobPropertyGroup.AssemblyName)) { + $packageIdPrefix = $jobPropertyGroup.AssemblyName + break + } + } + } + if ([string]::IsNullOrWhiteSpace($packageIdPrefix)) { + $packageIdPrefix = Split-Path $projectPath -LeafBase + } + $packageArtifact = Get-ChildItem -LiteralPath $JobFeedDir -Filter "$packageIdPrefix.*.nupkg" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike '*.snupkg' } | + Select-Object -First 1 + + if ($null -ne $packageArtifact) { + Write-Warning "$JobDependencyName project $projectPath returned a non-zero exit code after creating its main package artifact. Continuing without a symbol package." + continue + } + } + + throw "dotnet pack failed for $JobDependencyName project $projectPath." + } + } + } + finally { + if (Test-Path Env:NUGET_PACKAGES) { + Remove-Item Env:NUGET_PACKAGES -ErrorAction SilentlyContinue + } + + Remove-JobTempNuGetCacheDirectory -CacheDir $JobCacheDir + } + } -ArgumentList $DependencyName, $RepoPath, $projectPathsByDependency[$DependencyName], $projectsWithoutSymbolPackages, $Configuration, $feedDir, $LocalPackageVersion, $cacheDir, $nugetSources +} + +New-Item -Path $feedDir -ItemType Directory -Force | Out-Null +Clear-LocalNuGetCacheRoot + +$repoPaths = @{} +foreach ($dependency in $selectedDependencies) { + $repoPaths[$dependency] = Get-DependencyRepoPath -DependencyName $dependency +} + +$dependencyReuseStates = @{} +$dependenciesToPack = New-Object System.Collections.Generic.List[string] +$dependenciesToSync = New-Object System.Collections.Generic.List[string] +$resolvedDependencyVersions = @{} + +foreach ($dependency in $selectedDependencies) { + $reuseState = Get-DependencyReuseState -DependencyName $dependency -RepoPath $repoPaths[$dependency] + $dependencyReuseStates[$dependency] = $reuseState + [void]$dependenciesToSync.Add($dependency) + + if ($reuseState.CanReuse) { + $resolvedDependencyVersions[$dependency] = $reuseState.PackageVersion + Write-Output "Reusing local $dependency packages from $feedDir ($($reuseState.PackageVersion); $($reuseState.FingerprintInfo.Fingerprint))." + } + else { + [void]$dependenciesToPack.Add($dependency) + Write-Output "Packing local $dependency packages because $($reuseState.Reason)." + } +} + +if ($dependenciesToPack.Count -gt 0) { + Clear-LocalFeedPackages -Dependencies @($dependenciesToPack) +} + +Write-Output "Using local dependency feed at $feedDir" +Write-Output "Selected local dependencies: $($selectedDependencies -join ', ')" + +if ($LocalPalaso -and $dependenciesToPack.Contains('Palaso')) { + Invoke-Pack -DependencyName 'Palaso' -RepoPath $repoPaths['Palaso'] +} + +$parallelJobs = @() +if ($LocalLcm -and $dependenciesToPack.Contains('Lcm')) { + $parallelJobs += Start-PackJob -DependencyName 'Lcm' -RepoPath $repoPaths['Lcm'] +} +if ($LocalChorus -and $dependenciesToPack.Contains('Chorus')) { + $parallelJobs += Start-PackJob -DependencyName 'Chorus' -RepoPath $repoPaths['Chorus'] +} + +foreach ($job in $parallelJobs) { + Wait-Job -Job $job | Out-Null + try { + Receive-Job -Job $job -ErrorAction Stop | Write-Output + } + finally { + Remove-Job -Job $job -Force + } +} +foreach ($dependency in $selectedDependencies) { + if ($dependenciesToPack.Contains($dependency)) { + $resolvedDependencyVersions[$dependency] = Get-DependencyPackageVersion -DependencyName $dependency + Write-DependencyStamp -DependencyName $dependency -RepoPath $repoPaths[$dependency] -FingerprintInfo $dependencyReuseStates[$dependency].FingerprintInfo -PackageVersion $resolvedDependencyVersions[$dependency] + } +} + +$resolvedL10NSharpVersion = '' +if ($LocalPalaso) { + $resolvedL10NSharpVersion = Get-PackageReferenceVersion -ProjectPath (Join-Path $repoPaths['Palaso'] 'SIL.Core.Desktop\SIL.Core.Desktop.csproj') -PackageId 'L10NSharp' +} + +if ($dependenciesToPack.Count -gt 0) { + Clear-FieldWorksPackageCache -Dependencies @($dependenciesToPack) +} +Sync-FieldWorksPackageCache -DependencyVersions $resolvedDependencyVersions -Dependencies @($dependenciesToSync) +Write-LocalOverrideFile -DependencyVersions $resolvedDependencyVersions -L10NSharpVersion $resolvedL10NSharpVersion + +foreach ($dependency in $selectedDependencies) { + $resolvedVersion = $resolvedDependencyVersions[$dependency] + if ($dependencyReuseStates[$dependency].CanReuse) { + Write-Output "$dependency packages were reused at version $resolvedVersion." + } + elseif ($resolvedVersion -ne $LocalPackageVersion) { + Write-Warning "$dependency packages were requested at version $LocalPackageVersion, but the packed artifacts use version $resolvedVersion. Using the actual packed version in Build\\SilVersions.Local.props." + } + else { + Write-Output "$dependency packages were packed at version $resolvedVersion." + } +} + +Clear-LocalNuGetCacheRoot diff --git a/Build/SilVersions.props b/Build/SilVersions.props index eff2e7a0bf..b6f0e63186 100644 --- a/Build/SilVersions.props +++ b/Build/SilVersions.props @@ -12,11 +12,20 @@ ============================================================= --> <PropertyGroup Label="SIL Ecosystem Versions"> - <SilLcmVersion>11.0.0-beta0159</SilLcmVersion> - <SilLibPalasoVersion>17.0.0</SilLibPalasoVersion> - <SilChorusVersion>6.0.0-beta0063</SilChorusVersion> - <SilMachineVersion>3.7.13</SilMachineVersion> - <SilIPCFrameworkVersion>1.1.1-beta0001</SilIPCFrameworkVersion> - <IcuNugetVersion>70.1.152</IcuNugetVersion> + <SilLcmVersion Condition="'$(SilLcmVersion)'==''">11.0.0-beta0159</SilLcmVersion> + <SilLibPalasoVersion Condition="'$(SilLibPalasoVersion)'==''">17.0.0</SilLibPalasoVersion> + <L10NSharpVersion Condition="'$(L10NSharpVersion)'==''">9.0.0</L10NSharpVersion> + <SilChorusVersion Condition="'$(SilChorusVersion)'==''">6.0.0-beta0063</SilChorusVersion> + <SilMachineVersion Condition="'$(SilMachineVersion)'==''">3.7.13</SilMachineVersion> + <SilIPCFrameworkVersion Condition="'$(SilIPCFrameworkVersion)'==''" + >1.1.1-beta0001</SilIPCFrameworkVersion> + <IcuNugetVersion Condition="'$(IcuNugetVersion)'==''">70.1.152</IcuNugetVersion> + </PropertyGroup> + <Import Project="SilVersions.Local.props" Condition="Exists('SilVersions.Local.props')" /> + <PropertyGroup Label="Derived SIL Ecosystem Versions"> + <SilLibPalasoL10nsVersion Condition="'$(SilLibPalasoL10nsVersion)'==''" + >17.0.0</SilLibPalasoL10nsVersion> + <SilChorusL10nsVersion Condition="'$(SilChorusL10nsVersion)'==''" + >3.0.1</SilChorusL10nsVersion> </PropertyGroup> </Project> diff --git a/Build/Src/NativeBuild/NativeBuild.csproj b/Build/Src/NativeBuild/NativeBuild.csproj index dd28f416db..abeb7795bc 100644 --- a/Build/Src/NativeBuild/NativeBuild.csproj +++ b/Build/Src/NativeBuild/NativeBuild.csproj @@ -53,11 +53,11 @@ They must be explicitly referenced here because NativeBuild.csproj is a traditional MSBuild project, not an SDK-style project, so it doesn't inherit from Directory.Build.props. --> - <PackageReference Include="SIL.Chorus.L10ns" Version="3.0.1"> + <PackageReference Include="SIL.Chorus.L10ns" Version="$(SilChorusL10nsVersion)"> <IncludeAssets>none</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="SIL.LibPalaso.L10ns" Version="$(SilLibPalasoVersion)"> + <PackageReference Include="SIL.LibPalaso.L10ns" Version="$(SilLibPalasoL10nsVersion)"> <IncludeAssets>none</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> diff --git a/Directory.Build.props b/Directory.Build.props index 63ff54390a..11e8bd00b6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -79,10 +79,6 @@ <PropertyGroup Label="Shared Paths for Traversal Build"> <!-- Root directory paths --> <FwRoot>$(MSBuildThisFileDirectory)</FwRoot> - <UseLocalLcmSource Condition="'$(UseLocalLcmSource)'=='' and '$(SolutionName)'=='FieldWorks.LocalLcm'">true</UseLocalLcmSource> - <UseLocalLcmSource Condition="'$(UseLocalLcmSource)'==''">false</UseLocalLcmSource> - <LocalLcmRootDir Condition="'$(UseLocalLcmSource)'=='true' and '$(LocalLcmRootDir)'==''">$(FwRoot)Localizations\LCM\</LocalLcmRootDir> - <LocalLcmArtifactsDir Condition="'$(UseLocalLcmSource)'=='true'">$([System.IO.Path]::GetFullPath('$(LocalLcmRootDir)artifacts\$(Configuration)\net462'))</LocalLcmArtifactsDir> <FwDistFiles>$(FwRoot)DistFiles\</FwDistFiles> <FwOutput>$(FwRoot)Output\</FwOutput> <FwOutputBase>$(FwOutput)$(Configuration)\</FwOutputBase> @@ -114,10 +110,6 @@ <!-- LCM artifacts directory --> <DownloadsDir Condition="'$(DownloadsDir)'==''">$(FwRoot)Downloads</DownloadsDir> <LcmArtifactsDir Condition="'$(LcmArtifactsDir)'==''">$(DownloadsDir)</LcmArtifactsDir> - <LcmRootDir Condition="'$(UseLocalLcmSource)'=='true' and '$(LcmRootDir)'==''">$(LocalLcmRootDir)</LcmRootDir> - <LcmArtifactsDir Condition="'$(UseLocalLcmSource)'=='true'">$(LocalLcmArtifactsDir)</LcmArtifactsDir> - <LcmModelArtifactsDir Condition="'$(UseLocalLcmSource)'=='true' and '$(LcmModelArtifactsDir)'==''">$(LocalLcmArtifactsDir)</LcmModelArtifactsDir> - <LcmBuildTasksDir Condition="'$(UseLocalLcmSource)'=='true' and '$(LcmBuildTasksDir)'==''">$(LocalLcmArtifactsDir)</LcmBuildTasksDir> <!-- Enable binding redirects for all projects --> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType> diff --git a/Directory.Build.targets b/Directory.Build.targets index fd7b62702a..d16b35b46d 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -6,43 +6,6 @@ <_InstallerBuildProj>$([System.IO.Path]::Combine('$(_TransformRoot)','Build','InstallerBuild.proj'))</_InstallerBuildProj> </PropertyGroup> - <ItemGroup Condition="'$(UseLocalLcmSource)'=='true'"> - <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel'))" /> - <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.Core'))" /> - <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.Utils'))" /> - <_LocalLcmPackageReference Include="@(PackageReference->WithMetadataValue('Identity','SIL.LCModel.FixData'))" /> - - <PackageReference Remove="@(_LocalLcmPackageReference)" /> - - <ProjectReference Include="$(LocalLcmRootDir)src\SIL.LCModel\SIL.LCModel.csproj" - Condition="'@(_LocalLcmPackageReference->WithMetadataValue("Identity","SIL.LCModel"))' != '' and Exists('$(LocalLcmRootDir)src\SIL.LCModel\SIL.LCModel.csproj')" /> - <ProjectReference Include="$(LocalLcmRootDir)src\SIL.LCModel.Core\SIL.LCModel.Core.csproj" - Condition="'@(_LocalLcmPackageReference->WithMetadataValue("Identity","SIL.LCModel.Core"))' != '' and Exists('$(LocalLcmRootDir)src\SIL.LCModel.Core\SIL.LCModel.Core.csproj')" /> - <ProjectReference Include="$(LocalLcmRootDir)src\SIL.LCModel.Utils\SIL.LCModel.Utils.csproj" - Condition="'@(_LocalLcmPackageReference->WithMetadataValue("Identity","SIL.LCModel.Utils"))' != '' and Exists('$(LocalLcmRootDir)src\SIL.LCModel.Utils\SIL.LCModel.Utils.csproj')" /> - <ProjectReference Include="$(LocalLcmRootDir)src\SIL.LCModel.FixData\SIL.LCModel.FixData.csproj" - Condition="'@(_LocalLcmPackageReference->WithMetadataValue("Identity","SIL.LCModel.FixData"))' != '' and Exists('$(LocalLcmRootDir)src\SIL.LCModel.FixData\SIL.LCModel.FixData.csproj')" /> - </ItemGroup> - - <Target Name="EnsureLocalLcmBuildTasks" - Condition="'$(UseLocalLcmSource)'=='true' and '$(DesignTimeBuild)'!='true' and '$(MSBuildProjectName)'!='SIL.LCModel.Build.Tasks' and !Exists('$(LcmBuildTasksDir)\SIL.LCModel.Build.Tasks.dll') and Exists('$(LocalLcmRootDir)src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj')"> - <Message Text="Bootstrapping local liblcm build tasks from source because $(LcmBuildTasksDir)\SIL.LCModel.Build.Tasks.dll is missing." Importance="High" /> - <MSBuild - Projects="$(LocalLcmRootDir)src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj" - Targets="Build" - Properties="Configuration=$(Configuration)" /> - </Target> - - <Target Name="ValidateLocalLcmSourceMode" - BeforeTargets="ResolveAssemblyReferences" - DependsOnTargets="EnsureLocalLcmBuildTasks" - Condition="'$(UseLocalLcmSource)'=='true' and '$(DesignTimeBuild)'!='true'"> - <Error Condition="!Exists('$(LocalLcmRootDir)LCM.sln')" - Text="UseLocalLcmSource=true but no liblcm checkout was found at $(LocalLcmRootDir). Use FieldWorks.LocalLcm.sln or build.ps1 -LcmMode Local only after Localizations/LCM has been cloned." /> - <Error Condition="!Exists('$(LcmBuildTasksDir)\SIL.LCModel.Build.Tasks.dll')" - Text="UseLocalLcmSource=true but local liblcm artifacts are missing from $(LcmBuildTasksDir). Build FieldWorks.LocalLcm.sln or Localizations/LCM first so artifacts/$(Configuration)/net462 contains SIL.LCModel.Build.Tasks.dll." /> - </Target> - <!-- EnsureTransformsForManagedBuilds — guarantee XSL transform DLLs are up-to-date before any managed project resolves assembly references. diff --git a/Directory.Packages.props b/Directory.Packages.props index 46288596f8..1b96862f41 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,8 +1,6 @@ <Project> <PropertyGroup> - <_IsNestedLcmProject Condition="$([System.String]::Copy('$(MSBuildProjectFullPath)').ToUpperInvariant().Contains('\\LOCALIZATIONS\\LCM\\'))">true</_IsNestedLcmProject> - <ManagePackageVersionsCentrally Condition="'$(_IsNestedLcmProject)'=='true'">false</ManagePackageVersionsCentrally> - <ManagePackageVersionsCentrally Condition="'$(_IsNestedLcmProject)'!='true'">true</ManagePackageVersionsCentrally> + <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <!-- Transitive Pinning: When a package is declared here but only consumed transitively (not directly referenced by a project), NuGet still pins it @@ -14,7 +12,9 @@ </PropertyGroup> <!-- SIL ecosystem version properties: single source of truth in Build/SilVersions.props --> - <Import Project="Build/SilVersions.props" Condition="'$(ManagePackageVersionsCentrally)'=='true'" /> + <Import + Project="Build/SilVersions.props" + Condition="'$(ManagePackageVersionsCentrally)'=='true'" /> <!-- ============================================================= @@ -24,7 +24,9 @@ all projects sharing Output/{Configuration}/. ============================================================= --> - <ItemGroup Label="Transitive Pins" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <ItemGroup + Label="Transitive Pins" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> <!-- System.Drawing.Common: SIL.LCModel.Core pulls 6.0.0 transitively, but ParatextData 9.5.x requires >= 9.0.9. Pin to 9.0.9. --> <PackageVersion Include="System.Drawing.Common" Version="9.0.13" /> @@ -49,7 +51,9 @@ SIL LCModel Ecosystem ============================================================= --> - <ItemGroup Label="SIL LCModel Ecosystem" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <ItemGroup + Label="SIL LCModel Ecosystem" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> <PackageVersion Include="SIL.LCModel" Version="$(SilLcmVersion)" /> <PackageVersion Include="SIL.LCModel.Core" Version="$(SilLcmVersion)" /> <PackageVersion Include="SIL.LCModel.Core.Tests" Version="$(SilLcmVersion)" /> @@ -64,7 +68,9 @@ SIL LibPalaso Ecosystem ============================================================= --> - <ItemGroup Label="SIL LibPalaso Ecosystem" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <ItemGroup + Label="SIL LibPalaso Ecosystem" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> <PackageVersion Include="SIL.Core" Version="$(SilLibPalasoVersion)" /> <PackageVersion Include="SIL.Core.Desktop" Version="$(SilLibPalasoVersion)" /> <PackageVersion Include="SIL.Archiving" Version="$(SilLibPalasoVersion)" /> @@ -88,10 +94,12 @@ SIL Localization (ExcludeAssets=all; consumed by Build/Localize.targets) ============================================================= --> - <ItemGroup Label="SIL Localization" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> - <PackageVersion Include="L10NSharp" Version="9.0.0" /> - <PackageVersion Include="SIL.Chorus.L10ns" Version="3.0.1" /> - <PackageVersion Include="SIL.LibPalaso.L10ns" Version="$(SilLibPalasoVersion)" /> + <ItemGroup + Label="SIL Localization" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <PackageVersion Include="L10NSharp" Version="$(L10NSharpVersion)" /> + <PackageVersion Include="SIL.Chorus.L10ns" Version="$(SilChorusL10nsVersion)" /> + <PackageVersion Include="SIL.LibPalaso.L10ns" Version="$(SilLibPalasoL10nsVersion)" /> </ItemGroup> <!-- @@ -99,7 +107,9 @@ SIL Tools ============================================================= --> - <ItemGroup Label="SIL Tools" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <ItemGroup + Label="SIL Tools" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> <PackageVersion Include="SIL.Chorus.App" Version="$(SilChorusVersion)" /> <PackageVersion Include="SIL.Chorus.LibChorus" Version="$(SilChorusVersion)" /> <PackageVersion Include="SIL.DesktopAnalytics" Version="4.0.0" /> @@ -117,7 +127,9 @@ Third-Party Runtime ============================================================= --> - <ItemGroup Label="Third-Party Runtime" Condition="'$(ManagePackageVersionsCentrally)'=='true'"> + <ItemGroup + Label="Third-Party Runtime" + Condition="'$(ManagePackageVersionsCentrally)'=='true'"> <PackageVersion Include="AdamsLair.TreeViewAdv" Version="1.7.7" /> <PackageVersion Include="CommandLineArgumentsParser" Version="3.0.22" /> <PackageVersion Include="CommonServiceLocator" Version="2.0.7" /> diff --git a/Docs/architecture/dependencies.md b/Docs/architecture/dependencies.md index a4f40c1685..f376798cb9 100644 --- a/Docs/architecture/dependencies.md +++ b/Docs/architecture/dependencies.md @@ -1,10 +1,10 @@ # Dependencies on Other Repositories -FieldWorks depends on several external libraries and related repositories. This document describes those dependencies and how to work with them. +FieldWorks depends on several external libraries and related repositories. This document describes those dependencies and the supported local-development workflow for them. ## Overview -Most dependencies are automatically downloaded as NuGet packages during the build process. If you need to debug into or modify these libraries, use either a local source workflow or a local package-validation workflow depending on the goal. +Most dependencies are automatically downloaded as NuGet packages during the build process. If you need to debug into or modify these libraries locally, use the local package workflow driven by `build.ps1`. ## Primary Dependencies @@ -32,22 +32,9 @@ By default, dependencies are downloaded as NuGet packages during the build. The <SilLcmVersion>...</SilLcmVersion> ``` -## Local Source Workflow - -Use local source mode when you are diagnosing or changing library code and need direct source-level debugging. - -For `liblcm`, the preferred local source workflow is: - -1. Clone `liblcm` under `Localizations/LCM`. -2. Use `FieldWorks.LocalLcm.sln` in Visual Studio or `./build.ps1 -LcmMode Local`. -3. Make and validate the `liblcm` fix locally. -4. Return to the package-backed FieldWorks workflow after a released `liblcm` package is available. - -This workflow is for development and local verification. It is not the CI truth. - ## Local Package Validation Workflow -Use a local package workflow only when you explicitly need to validate FieldWorks as a package consumer rather than as a source consumer. +Use a local package workflow when you are changing `libpalaso`, `liblcm`, or `chorus` and want FieldWorks to consume those changes exactly the way it consumes released packages. ### Step 1: Clone the Repositories @@ -58,65 +45,50 @@ git clone https://github.com/sillsdev/libpalaso.git git clone https://github.com/sillsdev/chorus.git ``` -### Step 2: Set Up Local NuGet Repository +### Step 2: Set the Repository Environment Variables -1. **Create a local NuGet folder** (e.g., `C:\localnugetpackages`) +Set one environment variable for each local dependency checkout you want FieldWorks to pack: -2. **Add as NuGet source in Visual Studio**: - - Tools → Options → NuGet Package Manager → Package Sources - - Add your local folder - -3. **Set environment variable**: - ```powershell - $env:LOCAL_NUGET_REPO = "C:\localnugetpackages" - # Add to your profile for persistence - ``` +```powershell +$env:FW_LOCAL_PALASO = 'C:\src\libpalaso' +$env:FW_LOCAL_LCM = 'C:\src\liblcm' +$env:FW_LOCAL_CHORUS = 'C:\src\chorus' +``` -4. **Add the CopyPackage target** to each dependency's `Directory.Build.targets`: - ```xml - <Target Name="CopyPackage" AfterTargets="Pack" - Condition="'$(LOCAL_NUGET_REPO)'!='' AND '$(IsPackable)'=='true'"> - <Copy SourceFiles="$(PackageOutputPath)/$(PackageId).$(PackageVersion).nupkg" - DestinationFolder="$(LOCAL_NUGET_REPO)"/> - </Target> - ``` +`build.ps1` validates these paths before it tries to pack anything. If you enable `-LocalPalaso`, `-LocalLcm`, or `-LocalChorus` without the matching environment variable, the build stops with an error. -### Step 3: Build in Order +### Step 3: Build in Order Through `build.ps1` -Dependencies must be built in a specific order: +The supported control surface is `build.ps1`. It packs selected dependency repos into `Output/LocalNuGetFeed`, writes `Build/SilVersions.Local.props` with the temporary version overrides, then restores and builds FieldWorks against those local packages. -1. **libpalaso** (no dependencies on other SIL libraries) -2. **chorus** and **liblcm** (depend on libpalaso) -3. **FieldWorks** (depends on all of the above) +Dependencies are packed in this order: -For each library: +1. `libpalaso` +2. `liblcm` and `chorus` in parallel +3. FieldWorks -```bash -# Create a local branch for versioning -git checkout -b localcommit +Examples: -# Make a small change to bump version (e.g., edit README.md) -git commit -am "Local build version bump" +```powershell +# Use only a local liblcm checkout +.\build.ps1 -LocalLcm -# Build -dotnet build +# Use all three local repos with the default local version +.\build.ps1 -LocalPalaso -LocalLcm -LocalChorus -# Pack and publish to local repo -dotnet pack +# Override the temporary package version written into the local feed +.\build.ps1 -LocalPalaso -LocalLcm -LocalChorus -LocalPackageVersion 99.0.0-dev42 ``` -### Step 4: Update FieldWorks +### Step 4: Run Tests the Same Way -Update the NuGet versions in FieldWorks to use your local packages: +`test.ps1` accepts the same switches and forwards them to `build.ps1` before running the selected test pass. -1. Clear cached packages: - - `~\.nuget\packages\` (user cache) - - `packages\` (solution packages) - - Your local NuGet folder - -2. Update version numbers in `Build/SilVersions.props` +```powershell +.\test.ps1 -LocalPalaso -LocalLcm -LocalChorus +``` -3. Build FieldWorks +The local package workflow is intended for local development only. CI stays on the pinned versions from `Build/SilVersions.props`. ## Debugging Dependencies @@ -125,7 +97,7 @@ For the detailed `liblcm` debugging workflow, see `Docs/architecture/liblcm-debu Short version: 1. Use Visual Studio 2022 as the primary debugger for `.NET Framework` plus native FieldWorks work. -2. If you need exact source-level debugging into `liblcm`, clone it under `Localizations/LCM`, then use `FieldWorks.LocalLcm.sln` or `./build.ps1 -LcmMode Local`. +2. If you need to step into local `liblcm` code, build FieldWorks with `./build.ps1 -LocalLcm` so the loaded package contains your local symbols. 3. Use VS Code only for limited managed-only sessions in this repo, and only with the legacy C# extension path. 4. If breakpoints show "No symbols loaded", verify the loaded module path and PDB match before changing debugger settings. @@ -139,7 +111,7 @@ Build dependency information is also available in: FieldWorks uses GitHub Actions for CI/CD. The workflow files are in `.github/workflows/`. -Dependencies are restored automatically from NuGet during CI builds. CI does not depend on a nested `Localizations/LCM` checkout. +Dependencies are restored automatically from NuGet during CI builds. CI does not use the local package feed or the generated `Build/SilVersions.Local.props` override file. ## See Also diff --git a/Docs/architecture/liblcm-debugging.md b/Docs/architecture/liblcm-debugging.md index 91e3ea5b8b..000ff2e3e6 100644 --- a/Docs/architecture/liblcm-debugging.md +++ b/Docs/architecture/liblcm-debugging.md @@ -14,7 +14,7 @@ Use this decision tree first: 1. Need to step between C# and native C++ or debug a process that loads native DLLs: Use Visual Studio 2022. 2. Need trustworthy source-level debugging into your local `liblcm` changes: - Use `FieldWorks.LocalLcm.sln` in Visual Studio or the local-LCM launcher/task pair in VS Code. + Use the local package workflow driven by `./build.ps1 -LocalLcm`. 3. Need to inspect package behavior without rebuilding `liblcm` locally: Use Visual Studio 2022 with symbols and Source Link when available. 4. Need a quick managed-only session from VS Code: @@ -24,7 +24,7 @@ Use this decision tree first: By default, FieldWorks consumes `liblcm` through NuGet packages. The version is pinned in `Build/SilVersions.props` and flowed into `Directory.Packages.props`. -For local debugging, the preferred workflow is now local source mode with a nested checkout at `Localizations/LCM`. In that mode, FieldWorks switches from the `SIL.LCModel*` packages to local project references and local `liblcm` build artifacts. +For local debugging, the supported workflow keeps FieldWorks package-backed. `build.ps1 -LocalLcm` packs your checked-out `liblcm` repo into `Output/LocalNuGetFeed`, updates `Build/SilVersions.Local.props`, and rebuilds FieldWorks against those local packages. ## Recommended workflow: Visual Studio 2022 @@ -50,31 +50,31 @@ Use this when you want to investigate the currently pinned package version witho Use this path when the issue reproduces against the pinned package and you do not need to modify `liblcm` itself. -### Local-source debugging with nested checkout +### Local package debugging with a local `liblcm` checkout -Use this when you are actively changing `liblcm` or you need exact source and symbol fidelity. +Use this when you are actively changing `liblcm` and want FieldWorks to behave like a package consumer while still stepping into your local source. Prerequisites: -- `liblcm` is cloned at `Localizations/LCM`. +- `FW_LOCAL_LCM` points to your `liblcm` checkout. - Build output is Debug/x64. Steps: -1. Open `FieldWorks.LocalLcm.sln` in Visual Studio 2022. -2. Build the solution. If the local liblcm build tasks have not been generated yet, the first build bootstraps them into `Localizations/LCM/artifacts/<Configuration>/net462`. +1. Run `./build.ps1 -LocalLcm`. +2. Open `FieldWorks.sln` in Visual Studio 2022, or use the `FieldWorks (.NET Framework, Local Packages)` launcher in VS Code. 3. Start debugging FieldWorks. 4. If breakpoints do not bind, check the Modules window before changing any debugger settings. Why this works: -- FieldWorks compiles against local `liblcm` projects instead of the published packages. -- Shared build-time artifacts come from the local `liblcm` checkout. -- Visual Studio can resolve the matching local PDBs without a post-build copy step. +- FieldWorks still restores `SIL.LCModel*` packages, but they were packed from your local checkout immediately before the build. +- The local package build uses embedded PDBs, so the package already contains the matching debug information. +- Visual Studio can resolve the matching source paths from the local `liblcm` checkout without a post-build copy step. Common reset step: -- Switch back to `FieldWorks.sln` in Visual Studio or use the package-backed launcher/task in VS Code. +- Run `./build.ps1` without local dependency switches to remove `Build/SilVersions.Local.props` and go back to the pinned package versions. ## Limited workflow: VS Code @@ -100,14 +100,14 @@ Do not treat VS Code as the primary workflow for: 2. Use the Microsoft C# extension path, not C# Dev Kit, for this repo. 3. Use the `clr` launch type for the .NET Framework host executable. 4. Stay x64 only. -5. Ensure matching PDBs are present next to the loaded `SIL.LCModel*.dll` files. -6. Use the VS Code launchers in this repo, which prebuild managed projects with portable PDBs and keep package mode and local-source mode explicit. +5. Use the VS Code launchers in this repo, which prebuild managed projects with portable PDBs and keep package mode and local-package mode explicit. +6. Ensure the local dependency build completed successfully so the feed under `Output/LocalNuGetFeed` contains the expected `SIL.LCModel*` packages. ### VS Code launch workflow 1. Build FieldWorks with `./build.ps1`. 2. Choose `FieldWorks (.NET Framework, Package)` when you want the pinned package path. -3. Choose `FieldWorks (.NET Framework, Local LCM)` when you want the nested `Localizations/LCM` path. +3. Choose `FieldWorks (.NET Framework, Local Packages)` after building with `./build.ps1 -LocalLcm` when you want the locally packed `liblcm` path. 4. Keep `justMyCode` disabled when stepping into `liblcm`. 5. The VS Code launchers first run `Prepare Debug (*)`, which checks the last successful debug-build stamp and skips the build when no relevant saved files changed. 6. Do not switch the VS Code debug path to Windows PDBs. The debugger used here requires portable PDBs. @@ -115,14 +115,14 @@ Do not treat VS Code as the primary workflow for: Important boundary: -- Local source mode is for diagnosis, development, and local verification. +- Local package mode is for diagnosis, development, and local verification. - Package-backed builds remain the final merge and CI validation path. Practical limit: - This path is best effort only. If the session turns into mixed managed/native debugging, move to Visual Studio. -## NuGet package versus local source +## NuGet package versus local packages ### When to stay on the package @@ -132,22 +132,22 @@ Stay on the package when: - Source Link and symbols are already good enough, - you only need to understand behavior, not change `liblcm`. -### When to switch to local source mode +### When to switch to local package mode -Switch to local source mode when: +Switch to local package mode when: - you are modifying `liblcm`, -- you want a checked-out `liblcm` solution in the same Visual Studio session, -- you need build-time artifacts to come from the local checkout, -- you want the package-backed mode to remain untouched when local mode is off. +- you want FieldWorks to restore a package built from your local checkout, +- you need matching symbols without a manual copy step, +- you want the default pinned-package workflow to remain untouched when local mode is off. ## Build entrypoints -- `./build.ps1 -LcmMode Package` forces the package-backed path. -- `./build.ps1 -LcmMode Local` forces the nested `Localizations/LCM` path and bootstraps the local build tasks when needed. -- `./build.ps1 -LcmMode Auto` stays package-backed by default, but prints whether local LCM inputs are available. -- `FieldWorks.sln` stays package-backed in Visual Studio. -- `FieldWorks.LocalLcm.sln` enables local source mode in Visual Studio. +- `./build.ps1` uses the pinned package versions from `Build/SilVersions.props`. +- `./build.ps1 -LocalLcm` packs your local `liblcm` checkout and rebuilds FieldWorks against that package. +- `./build.ps1 -LocalPalaso -LocalLcm -LocalChorus` packs all three local dependency repos in the supported build order. +- `./test.ps1` accepts the same local dependency switches and forwards them to `build.ps1`. +- `FieldWorks.sln` remains the Visual Studio solution for both pinned-package and local-package workflows. ## Failure modes to check first @@ -158,13 +158,13 @@ If debugging does not behave as expected, check these before changing tool setti 2. PDB mismatch: verify that the PDB came from the same build as the DLL. 3. Package cache confusion: - stale copies under `%USERPROFILE%\.nuget\packages` can mislead assumptions. + stale copies under `%USERPROFILE%\.nuget\packages` or `packages\` can mislead assumptions. 4. Architecture mismatch: keep the workflow x64 end to end. ## Minimal repo changes that improve this workflow -The current repo is close, but a few small changes make the workflow more obvious and less fragile. +This repo now uses a package-only inner loop, but a few small additions could still make the workflow more obvious and less fragile. ### Recommended now diff --git a/FieldWorks.LocalLcm.sln b/FieldWorks.LocalLcm.sln deleted file mode 100644 index cf6d252c8a..0000000000 --- a/FieldWorks.LocalLcm.sln +++ /dev/null @@ -1,1028 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36401.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheLightTests", "Src\CacheLight\CacheLightTests\CacheLightTests.csproj", "{6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertLib", "Lib\src\Converter\Convertlib\ConvertLib.csproj", "{7827DE67-1E76-5DFA-B3E7-122B2A5B2472}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertSFM", "Src\Utilities\SfmToXml\ConvertSFM\ConvertSFM.csproj", "{EB470157-7A33-5263-951E-2190FC2AD626}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Converter", "Lib\src\Converter\Converter\Converter.csproj", "{B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConverterConsole", "Lib\src\Converter\ConvertConsole\ConverterConsole.csproj", "{01C9D37F-BCFA-5353-A980-84EFD3821F8A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Design", "Src\Common\Controls\Design\Design.csproj", "{762BD8EC-F9B2-5927-BC21-9D31D5A14C10}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DetailControls", "Src\Common\Controls\DetailControls\DetailControls.csproj", "{43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DetailControlsTests", "Src\Common\Controls\DetailControls\DetailControlsTests\DetailControlsTests.csproj", "{36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discourse", "Src\LexText\Discourse\Discourse.csproj", "{A51BAFC3-1649-584D-8D25-101884EE9EAA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscourseTests", "Src\LexText\Discourse\DiscourseTests\DiscourseTests.csproj", "{1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FdoUi", "Src\FdoUi\FdoUi.csproj", "{D826C3DF-3501-5F31-BC84-24493A500F9D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FdoUiTests", "Src\FdoUi\FdoUiTests\FdoUiTests.csproj", "{33123A2A-FD82-5134-B385-ADAC0A433B85}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FieldWorks", "Src\Common\FieldWorks\FieldWorks.csproj", "{5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FieldWorksTests", "Src\Common\FieldWorks\FieldWorksTests\FieldWorksTests.csproj", "{DCA3866E-E101-5BBC-9E35-60E632A4EF24}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Filters", "Src\Common\Filters\Filters.csproj", "{9C375199-FB95-5FB0-A5F3-B1E68C447C49}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FiltersTests", "Src\Common\Filters\FiltersTests\FiltersTests.csproj", "{D7281406-A9A3-5B80-95CB-23D223A0FD2D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixFwData", "Src\Utilities\FixFwData\FixFwData.csproj", "{E6B2CDCC-E016-5328-AA87-BC095712FDE6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixFwDataDll", "Src\Utilities\FixFwDataDll\FixFwDataDll.csproj", "{AA147037-F6BB-5556-858E-FC03DE028A37}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexPathwayPlugin", "Src\LexText\FlexPathwayPlugin\FlexPathwayPlugin.csproj", "{BC6E6932-35C6-55F7-8638-89F6C7DCA43A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexPathwayPluginTests", "Src\LexText\FlexPathwayPlugin\FlexPathwayPluginTests\FlexPathwayPluginTests.csproj", "{221A2FA1-1710-5537-A125-5BE856B949CC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexUIAdapter", "Src\XCore\FlexUIAdapter\FlexUIAdapter.csproj", "{B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FormLanguageSwitch", "Lib\src\FormLanguageSwitch\FormLanguageSwitch.csproj", "{016A743C-BD3C-523B-B5BC-E3791D3C49E3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Framework", "Src\Common\Framework\Framework.csproj", "{3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Src\Common\Framework\FrameworkTests\FrameworkTests.csproj", "{CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwBuildTasks", "Build\Src\FwBuildTasks\FwBuildTasks.csproj", "{D5BC4B46-5126-563F-9537-B8FA5F573E55}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwControls", "Src\Common\Controls\FwControls\FwControls.csproj", "{6E80DBC7-731A-5918-8767-9A402EC483E6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwControlsTests", "Src\Common\Controls\FwControls\FwControlsTests\FwControlsTests.csproj", "{1EF0C15D-DF42-5457-841A-2F220B77304D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgControls", "Src\FwCoreDlgs\FwCoreDlgControls\FwCoreDlgControls.csproj", "{28A7428D-3BA0-576C-A7B6-BA998439A036}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgControlsTests", "Src\FwCoreDlgs\FwCoreDlgControls\FwCoreDlgControlsTests\FwCoreDlgControlsTests.csproj", "{74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgs", "Src\FwCoreDlgs\FwCoreDlgs.csproj", "{5E16031F-2584-55B4-86B8-B42D7EEE8F25}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwCoreDlgsTests", "Src\FwCoreDlgs\FwCoreDlgsTests\FwCoreDlgsTests.csproj", "{B46A3242-AAB2-5984-9F88-C65B7537D558}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwParatextLexiconPlugin", "Src\FwParatextLexiconPlugin\FwParatextLexiconPlugin.csproj", "{40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwParatextLexiconPluginTests", "Src\FwParatextLexiconPlugin\FwParatextLexiconPluginTests\FwParatextLexiconPluginTests.csproj", "{FE438201-74A1-5236-AE07-E502B853EA18}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwResources", "Src\FwResources\FwResources.csproj", "{C7533C60-BF48-5844-8220-A488387AC016}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwUtils", "Src\Common\FwUtils\FwUtils.csproj", "{DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FwUtilsTests", "Src\Common\FwUtils\FwUtilsTests\FwUtilsTests.csproj", "{A39B87BF-6846-559A-A01F-6251A0FE856E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxtDll", "Src\FXT\FxtDll\FxtDll.csproj", "{DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxtDllTests", "Src\FXT\FxtDll\FxtDllTests\FxtDllTests.csproj", "{3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateHCConfig", "Src\GenerateHCConfig\GenerateHCConfig.csproj", "{644A443A-1066-57D2-9DFA-35CD9E9A46BE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITextDll", "Src\LexText\Interlinear\ITextDll.csproj", "{ABC70BB4-125D-54DD-B962-6131F490AB10}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITextDllTests", "Src\LexText\Interlinear\ITextDllTests\ITextDllTests.csproj", "{6DA137DD-449E-57F1-8489-686CC307A561}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstallValidator", "Src\InstallValidator\InstallValidator.csproj", "{A2FDE99A-204A-5C10-995F-FD56039385C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstallValidatorTests", "Src\InstallValidator\InstallValidatorTests\InstallValidatorTests.csproj", "{43D44B32-899D-511D-9CF6-18CF7D3844CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCMBrowser", "Src\LCMBrowser\LCMBrowser.csproj", "{1F87EA7A-211A-562D-95ED-00F935966948}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexEdDll", "Src\LexText\Lexicon\LexEdDll.csproj", "{6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexEdDllTests", "Src\LexText\Lexicon\LexEdDllTests\LexEdDllTests.csproj", "{0434B036-FB8A-58B1-A075-B3D2D94BF492}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextControls", "Src\LexText\LexTextControls\LexTextControls.csproj", "{FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextControlsTests", "Src\LexText\LexTextControls\LexTextControlsTests\LexTextControlsTests.csproj", "{3C904B25-FE98-55A8-A9AB-2CBA065AE297}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextDll", "Src\LexText\LexTextDll\LexTextDll.csproj", "{44E4C722-DCE1-5A8A-A586-81D329771F66}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LexTextDllTests", "Src\LexText\LexTextDll\LexTextDllTests\LexTextDllTests.csproj", "{D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MGA", "Src\LexText\Morphology\MGA\MGA.csproj", "{1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MGATests", "Src\LexText\Morphology\MGA\MGATests\MGATests.csproj", "{78FB823E-35FE-5D1D-B44D-17C22FDF6003}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedLgIcuCollator", "Src\ManagedLgIcuCollator\ManagedLgIcuCollator.csproj", "{8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedLgIcuCollatorTests", "Src\ManagedLgIcuCollator\ManagedLgIcuCollatorTests\ManagedLgIcuCollatorTests.csproj", "{65C872FA-2DC7-5EC2-9A19-EDB4FA325934}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwDrawRootBuffered", "Src\ManagedVwDrawRootBuffered\ManagedVwDrawRootBuffered.csproj", "{BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwWindow", "Src\ManagedVwWindow\ManagedVwWindow.csproj", "{5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedVwWindowTests", "Src\ManagedVwWindow\ManagedVwWindowTests\ManagedVwWindowTests.csproj", "{FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessageBoxExLib", "Src\Utilities\MessageBoxExLib\MessageBoxExLib.csproj", "{C5AA04DD-F91B-5156-BD40-4A761058AC64}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessageBoxExLibTests", "Src\Utilities\MessageBoxExLib\MessageBoxExLibTests\MessageBoxExLibTests.csproj", "{F2525F78-38CD-5E36-A854-E16BE8A1B8FF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MigrateSqlDbs", "Src\MigrateSqlDbs\MigrateSqlDbs.csproj", "{170E9760-4036-5CC4-951D-DAFDBCEF7BEA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorphologyEditorDll", "Src\LexText\Morphology\MorphologyEditorDll.csproj", "{DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorphologyEditorDllTests", "Src\LexText\Morphology\MorphologyEditorDllTests\MorphologyEditorDllTests.csproj", "{83DC33D4-9323-56B1-865A-56CD516EE52A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnitReport", "Build\Src\NUnitReport\NUnitReport.csproj", "{DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObjectBrowser", "Lib\src\ObjectBrowser\ObjectBrowser.csproj", "{1B8FE336-2272-5424-A36A-7C786F9FE388}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paratext8Plugin", "Src\Paratext8Plugin\Paratext8Plugin.csproj", "{BF01268F-E755-5577-B8D7-9014D7591A2A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paratext8PluginTests", "Src\Paratext8Plugin\ParaText8PluginTests\Paratext8PluginTests.csproj", "{4B95DD96-AB0A-571E-81E8-3035ECCC8D47}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParatextImport", "Src\ParatextImport\ParatextImport.csproj", "{21F54BD0-152A-547C-A940-2BCFEA8D1730}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParatextImportTests", "Src\ParatextImport\ParatextImportTests\ParatextImportTests.csproj", "{66361165-1489-5B17-8969-4A6253C00931}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserCore", "Src\LexText\ParserCore\ParserCore.csproj", "{1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserCoreTests", "Src\LexText\ParserCore\ParserCoreTests\ParserCoreTests.csproj", "{E5F82767-7DC7-599F-BC29-AAFE4AC98060}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserUI", "Src\LexText\ParserUI\ParserUI.csproj", "{09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserUITests", "Src\LexText\ParserUI\ParserUITests\ParserUITests.csproj", "{2310A14E-5FFA-5939-885C-DA681EAFC168}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectUnpacker", "Src\ProjectUnpacker\ProjectUnpacker.csproj", "{3E1BAF09-02C0-55BF-8683-3FAACFE6F137}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reporting", "Src\Utilities\Reporting\Reporting.csproj", "{8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSite", "Src\Common\RootSite\RootSite.csproj", "{94AD32DE-8AA2-547E-90F9-99169687406F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RootSiteTests", "Src\Common\RootSite\RootSiteTests\RootSiteTests.csproj", "{EC934204-1D3A-5575-A500-CB7923C440E2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecks", "Lib\src\ScrChecks\ScrChecks.csproj", "{0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrChecksTests", "Lib\src\ScrChecks\ScrChecksTests\ScrChecksTests.csproj", "{37555756-6D42-5E46-B455-E58E3D1E8E0C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtils", "Src\Common\ScriptureUtils\ScriptureUtils.csproj", "{8336DC7C-954B-5076-9315-D7DC5317282B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptureUtilsTests", "Src\Common\ScriptureUtils\ScriptureUtilsTests\ScriptureUtilsTests.csproj", "{04546E35-9A3A-5629-8282-3683A5D848F9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sfm2Xml", "Src\Utilities\SfmToXml\Sfm2Xml.csproj", "{7C859385-3602-59D1-9A7E-E81E7C6EBBE4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sfm2XmlTests", "Src\Utilities\SfmToXml\Sfm2XmlTests\Sfm2XmlTests.csproj", "{46A84616-92E0-567E-846E-DF0C203CF0D2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SfmStats", "Src\Utilities\SfmStats\SfmStats.csproj", "{910ED78F-AE00-5547-ADEC-A0E54BF98B8D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SilSidePane", "Src\XCore\SilSidePane\SilSidePane.csproj", "{68C6DB83-7D0F-5F31-9307-6489E21F74E5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SilSidePaneTests", "Src\XCore\SilSidePane\SilSidePaneTests\SilSidePaneTests.csproj", "{E63B6F76-5CD3-5757-93D7-E050CB412F23}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleRootSite", "Src\Common\SimpleRootSite\SimpleRootSite.csproj", "{712CF492-5D74-5464-93CA-EAB5BE54D09B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleRootSiteTests", "Src\Common\SimpleRootSite\SimpleRootSiteTests\SimpleRootSiteTests.csproj", "{D2BAD63B-0914-5014-BCE8-8D767A871F06}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UIAdapterInterfaces", "Src\Common\UIAdapterInterfaces\UIAdapterInterfaces.csproj", "{98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicodeCharEditor", "Src\UnicodeCharEditor\UnicodeCharEditor.csproj", "{FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicodeCharEditorTests", "Src\UnicodeCharEditor\UnicodeCharEditorTests\UnicodeCharEditorTests.csproj", "{515DEC49-6C0F-5F02-AC05-69AC6AF51639}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewsInterfaces", "Src\Common\ViewsInterfaces\ViewsInterfaces.csproj", "{70163155-93C1-5816-A1D4-1EEA0215298C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewsInterfacesTests", "Src\Common\ViewsInterfaces\ViewsInterfacesTests\ViewsInterfacesTests.csproj", "{EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VwGraphicsReplayer", "Src\views\lib\VwGraphicsReplayer\VwGraphicsReplayer.csproj", "{AB011392-76C6-5D67-9623-CA9B2680B899}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Widgets", "Src\Common\Controls\Widgets\Widgets.csproj", "{3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WidgetsTests", "Src\Common\Controls\Widgets\WidgetsTests\WidgetsTests.csproj", "{17AE7011-A346-5BAE-A021-552E7A3A86DD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAmpleManagedWrapper", "Src\LexText\ParserCore\XAmpleManagedWrapper\XAmpleManagedWrapper.csproj", "{6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAmpleManagedWrapperTests", "Src\LexText\ParserCore\XAmpleManagedWrapper\XAmpleManagedWrapperTests\XAmpleManagedWrapperTests.csproj", "{5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComManifestTestHost", "Src\Utilities\ComManifestTestHost\ComManifestTestHost.csproj", "{9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLUtils", "Src\Utilities\XMLUtils\XMLUtils.csproj", "{D4F47DD8-A0E7-5081-808A-5286F873DC13}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLUtilsTests", "Src\Utilities\XMLUtils\XMLUtilsTests\XMLUtilsTests.csproj", "{2EB628C9-EC23-5394-8BEB-B7542360FEAE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLViews", "Src\Common\Controls\XMLViews\XMLViews.csproj", "{B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XMLViewsTests", "Src\Common\Controls\XMLViews\XMLViewsTests\XMLViewsTests.csproj", "{DA1CAEE2-340C-51E7-980B-916545074600}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCore", "Src\XCore\xCore.csproj", "{B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreInterfaces", "Src\XCore\xCoreInterfaces\xCoreInterfaces.csproj", "{1C758320-DE0A-50F3-8892-B0F7397CFA61}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreInterfacesTests", "Src\XCore\xCoreInterfaces\xCoreInterfacesTests\xCoreInterfacesTests.csproj", "{9B1C17E4-3086-53B9-B1DC-8A39117E237F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xCoreTests", "Src\XCore\xCoreTests\xCoreTests.csproj", "{2861A99F-3390-52B4-A2D8-0F80A62DB108}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xWorks", "Src\xWorks\xWorks.csproj", "{5B1DFFF7-6A59-5955-B77D-42DBF12721D1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xWorksTests", "Src\xWorks\xWorksTests\xWorksTests.csproj", "{1308E147-8B51-55E0-B475-10A0053F9AAF}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Generic", "Src\Generic\Generic.vcxproj", "{7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Kernel", "Src\Kernel\Kernel.vcxproj", "{6396B488-4D34-48B2-8639-EEB90707405B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "views", "Src\views\views.vcxproj", "{C86CA2EB-81B5-4411-B5B7-E983314E02DA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheLight", "Src\CacheLight\CacheLight.csproj", "{34442A32-31DE-45A8-AD36-0ECFE4095523}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FXT", "FXT", "{6D69D131-C928-6A46-F508-A4A608CBE30A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FxtExe", "Src\FXT\FxtExe\FxtExe.csproj", "{8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallerArtifactsTests", "Src\InstallValidator\InstallerArtifactsTests\InstallerArtifactsTests.csproj", "{8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossLib", "Src\Utilities\HCSynthByGloss\HCSynthByGlossLib\HCSynthByGlossLib.csproj", "{AF250D69-786B-40FA-A125-FD3F448CC283}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateHCConfigForFLExTrans", "Src\Utilities\HCSynthByGloss\GenerateHCConfig4FLExTrans\GenerateHCConfigForFLExTrans.csproj", "{91D55536-1DE3-4279-9DD1-CA2CED068F42}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGloss", "Src\Utilities\HCSynthByGloss\HCSynthByGloss\HCSynthByGloss.csproj", "{BF5AD9CA-6FD6-49C7-B351-0630C11479C0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossDll", "Src\Utilities\HCSynthByGloss\HCSynthByGlossDll\HCSynthByGlossDll.csproj", "{EEE765C8-6812-4F9F-A100-42AA71921926}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossDllTest", "Src\Utilities\HCSynthByGloss\HCSynthByGlossDll\HCSynthByGlossDllTest\HCSynthByGlossDllTest.csproj", "{8EF1E1AE-2226-4A9B-8942-CAB531956ED3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HCSynthByGlossTest", "Src\Utilities\HCSynthByGloss\HCSynthByGloss\HCSynthByGlossTest\HCSynthByGlossTest.csproj", "{5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Build.Tasks", "Localizations\LCM\src\SIL.LCModel.Build.Tasks\SIL.LCModel.Build.Tasks.csproj", "{5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel", "Localizations\LCM\src\SIL.LCModel\SIL.LCModel.csproj", "{E5E9DDC7-2855-4D92-AD46-960AC4C46457}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Core", "Localizations\LCM\src\SIL.LCModel.Core\SIL.LCModel.Core.csproj", "{4C7D6B65-A331-4ED7-9B53-3301E714F8E7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.FixData", "Localizations\LCM\src\SIL.LCModel.FixData\SIL.LCModel.FixData.csproj", "{4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.LCModel.Utils", "Localizations\LCM\src\SIL.LCModel.Utils\SIL.LCModel.Utils.csproj", "{4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Bounds|x64 = Bounds|x64 - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Bounds|x64.ActiveCfg = Release|x64 - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Bounds|x64.Build.0 = Release|x64 - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Debug|x64.ActiveCfg = Debug|x64 - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Debug|x64.Build.0 = Debug|x64 - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Release|x64.ActiveCfg = Release|x64 - {6E9C3A6D-5200-598B-A0DF-6AB5BAC33321}.Release|x64.Build.0 = Release|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Bounds|x64.ActiveCfg = Release|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Bounds|x64.Build.0 = Release|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Debug|x64.ActiveCfg = Debug|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Debug|x64.Build.0 = Debug|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Release|x64.ActiveCfg = Release|x64 - {7827DE67-1E76-5DFA-B3E7-122B2A5B2472}.Release|x64.Build.0 = Release|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Bounds|x64.ActiveCfg = Release|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Bounds|x64.Build.0 = Release|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Debug|x64.ActiveCfg = Debug|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Debug|x64.Build.0 = Debug|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Release|x64.ActiveCfg = Release|x64 - {EB470157-7A33-5263-951E-2190FC2AD626}.Release|x64.Build.0 = Release|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Bounds|x64.ActiveCfg = Release|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Bounds|x64.Build.0 = Release|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Debug|x64.ActiveCfg = Debug|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Debug|x64.Build.0 = Debug|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Release|x64.ActiveCfg = Release|x64 - {B26CBC5A-711C-5EA4-A2AA-AAF81565CA34}.Release|x64.Build.0 = Release|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Bounds|x64.ActiveCfg = Release|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Bounds|x64.Build.0 = Release|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Debug|x64.ActiveCfg = Debug|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Debug|x64.Build.0 = Debug|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Release|x64.ActiveCfg = Release|x64 - {01C9D37F-BCFA-5353-A980-84EFD3821F8A}.Release|x64.Build.0 = Release|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Bounds|x64.ActiveCfg = Release|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Bounds|x64.Build.0 = Release|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Debug|x64.ActiveCfg = Debug|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Debug|x64.Build.0 = Debug|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Release|x64.ActiveCfg = Release|x64 - {762BD8EC-F9B2-5927-BC21-9D31D5A14C10}.Release|x64.Build.0 = Release|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Bounds|x64.ActiveCfg = Release|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Bounds|x64.Build.0 = Release|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Debug|x64.ActiveCfg = Debug|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Debug|x64.Build.0 = Debug|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Release|x64.ActiveCfg = Release|x64 - {43FEB32F-DF19-5622-AAF3-7A4CFE118D0F}.Release|x64.Build.0 = Release|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Bounds|x64.ActiveCfg = Release|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Bounds|x64.Build.0 = Release|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Debug|x64.ActiveCfg = Debug|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Debug|x64.Build.0 = Debug|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Release|x64.ActiveCfg = Release|x64 - {36F2A7A6-C7F9-5D3D-87D7-B4C0D5C51C0E}.Release|x64.Build.0 = Release|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Bounds|x64.ActiveCfg = Release|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Bounds|x64.Build.0 = Release|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Debug|x64.ActiveCfg = Debug|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Debug|x64.Build.0 = Debug|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Release|x64.ActiveCfg = Release|x64 - {A51BAFC3-1649-584D-8D25-101884EE9EAA}.Release|x64.Build.0 = Release|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Bounds|x64.ActiveCfg = Release|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Bounds|x64.Build.0 = Release|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Debug|x64.ActiveCfg = Debug|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Debug|x64.Build.0 = Debug|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Release|x64.ActiveCfg = Release|x64 - {1CE6483D-5D10-51AD-B2A7-FD7F82CCBAB2}.Release|x64.Build.0 = Release|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Bounds|x64.ActiveCfg = Release|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Bounds|x64.Build.0 = Release|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Debug|x64.ActiveCfg = Debug|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Debug|x64.Build.0 = Debug|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Release|x64.ActiveCfg = Release|x64 - {D826C3DF-3501-5F31-BC84-24493A500F9D}.Release|x64.Build.0 = Release|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Bounds|x64.ActiveCfg = Release|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Bounds|x64.Build.0 = Release|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Debug|x64.ActiveCfg = Debug|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Debug|x64.Build.0 = Debug|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Release|x64.ActiveCfg = Release|x64 - {33123A2A-FD82-5134-B385-ADAC0A433B85}.Release|x64.Build.0 = Release|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Bounds|x64.ActiveCfg = Release|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Bounds|x64.Build.0 = Release|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Debug|x64.ActiveCfg = Debug|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Debug|x64.Build.0 = Debug|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Release|x64.ActiveCfg = Release|x64 - {5DF15966-BF60-5D21-BDE3-301BB1D4AB3B}.Release|x64.Build.0 = Release|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Bounds|x64.ActiveCfg = Release|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Bounds|x64.Build.0 = Release|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Debug|x64.ActiveCfg = Debug|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Debug|x64.Build.0 = Debug|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Release|x64.ActiveCfg = Release|x64 - {DCA3866E-E101-5BBC-9E35-60E632A4EF24}.Release|x64.Build.0 = Release|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Bounds|x64.ActiveCfg = Release|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Bounds|x64.Build.0 = Release|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Debug|x64.ActiveCfg = Debug|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Debug|x64.Build.0 = Debug|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Release|x64.ActiveCfg = Release|x64 - {9C375199-FB95-5FB0-A5F3-B1E68C447C49}.Release|x64.Build.0 = Release|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Bounds|x64.ActiveCfg = Release|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Bounds|x64.Build.0 = Release|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Debug|x64.ActiveCfg = Debug|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Debug|x64.Build.0 = Debug|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Release|x64.ActiveCfg = Release|x64 - {D7281406-A9A3-5B80-95CB-23D223A0FD2D}.Release|x64.Build.0 = Release|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Bounds|x64.ActiveCfg = Release|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Bounds|x64.Build.0 = Release|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Debug|x64.ActiveCfg = Debug|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Debug|x64.Build.0 = Debug|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Release|x64.ActiveCfg = Release|x64 - {E6B2CDCC-E016-5328-AA87-BC095712FDE6}.Release|x64.Build.0 = Release|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Bounds|x64.ActiveCfg = Release|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Bounds|x64.Build.0 = Release|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Debug|x64.ActiveCfg = Debug|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Debug|x64.Build.0 = Debug|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Release|x64.ActiveCfg = Release|x64 - {AA147037-F6BB-5556-858E-FC03DE028A37}.Release|x64.Build.0 = Release|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Bounds|x64.ActiveCfg = Release|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Bounds|x64.Build.0 = Release|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Debug|x64.ActiveCfg = Debug|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Debug|x64.Build.0 = Debug|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Release|x64.ActiveCfg = Release|x64 - {BC6E6932-35C6-55F7-8638-89F6C7DCA43A}.Release|x64.Build.0 = Release|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Bounds|x64.ActiveCfg = Release|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Bounds|x64.Build.0 = Release|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Debug|x64.ActiveCfg = Debug|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Debug|x64.Build.0 = Debug|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Release|x64.ActiveCfg = Release|x64 - {221A2FA1-1710-5537-A125-5BE856B949CC}.Release|x64.Build.0 = Release|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Bounds|x64.ActiveCfg = Release|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Bounds|x64.Build.0 = Release|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Debug|x64.ActiveCfg = Debug|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Debug|x64.Build.0 = Debug|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Release|x64.ActiveCfg = Release|x64 - {B9116D9B-CEC2-5917-A04D-8DDAEF5FA943}.Release|x64.Build.0 = Release|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Bounds|x64.ActiveCfg = Release|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Bounds|x64.Build.0 = Release|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Debug|x64.ActiveCfg = Debug|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Debug|x64.Build.0 = Debug|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Release|x64.ActiveCfg = Release|x64 - {016A743C-BD3C-523B-B5BC-E3791D3C49E3}.Release|x64.Build.0 = Release|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Bounds|x64.ActiveCfg = Release|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Bounds|x64.Build.0 = Release|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Debug|x64.ActiveCfg = Debug|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Debug|x64.Build.0 = Debug|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Release|x64.ActiveCfg = Release|x64 - {3B8923F8-CA27-5B0C-ABA8-735CE02B6A6D}.Release|x64.Build.0 = Release|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Bounds|x64.ActiveCfg = Release|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Bounds|x64.Build.0 = Release|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Debug|x64.ActiveCfg = Debug|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Debug|x64.Build.0 = Debug|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Release|x64.ActiveCfg = Release|x64 - {CFCBBE66-B323-53E4-93F1-5CFB00CE02E9}.Release|x64.Build.0 = Release|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Bounds|x64.ActiveCfg = Release|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Bounds|x64.Build.0 = Release|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Debug|x64.ActiveCfg = Debug|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Debug|x64.Build.0 = Debug|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Release|x64.ActiveCfg = Release|x64 - {D5BC4B46-5126-563F-9537-B8FA5F573E55}.Release|x64.Build.0 = Release|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Bounds|x64.ActiveCfg = Release|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Bounds|x64.Build.0 = Release|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Debug|x64.ActiveCfg = Debug|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Debug|x64.Build.0 = Debug|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Release|x64.ActiveCfg = Release|x64 - {6E80DBC7-731A-5918-8767-9A402EC483E6}.Release|x64.Build.0 = Release|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Bounds|x64.ActiveCfg = Release|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Bounds|x64.Build.0 = Release|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Debug|x64.ActiveCfg = Debug|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Debug|x64.Build.0 = Debug|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Release|x64.ActiveCfg = Release|x64 - {1EF0C15D-DF42-5457-841A-2F220B77304D}.Release|x64.Build.0 = Release|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Bounds|x64.ActiveCfg = Release|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Bounds|x64.Build.0 = Release|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Debug|x64.ActiveCfg = Debug|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Debug|x64.Build.0 = Debug|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Release|x64.ActiveCfg = Release|x64 - {28A7428D-3BA0-576C-A7B6-BA998439A036}.Release|x64.Build.0 = Release|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Bounds|x64.ActiveCfg = Release|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Bounds|x64.Build.0 = Release|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Debug|x64.ActiveCfg = Debug|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Debug|x64.Build.0 = Debug|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Release|x64.ActiveCfg = Release|x64 - {74AEB3F2-4C17-5196-AC93-C3B59EAB4C81}.Release|x64.Build.0 = Release|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Bounds|x64.ActiveCfg = Release|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Bounds|x64.Build.0 = Release|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Debug|x64.ActiveCfg = Debug|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Debug|x64.Build.0 = Debug|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Release|x64.ActiveCfg = Release|x64 - {5E16031F-2584-55B4-86B8-B42D7EEE8F25}.Release|x64.Build.0 = Release|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Bounds|x64.ActiveCfg = Release|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Bounds|x64.Build.0 = Release|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Debug|x64.ActiveCfg = Debug|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Debug|x64.Build.0 = Debug|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Release|x64.ActiveCfg = Release|x64 - {B46A3242-AAB2-5984-9F88-C65B7537D558}.Release|x64.Build.0 = Release|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Bounds|x64.ActiveCfg = Release|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Bounds|x64.Build.0 = Release|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Debug|x64.ActiveCfg = Debug|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Debug|x64.Build.0 = Debug|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Release|x64.ActiveCfg = Release|x64 - {40A22FC7-C3FD-5C1B-9E5D-82A7C649F311}.Release|x64.Build.0 = Release|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Bounds|x64.ActiveCfg = Release|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Bounds|x64.Build.0 = Release|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Debug|x64.ActiveCfg = Debug|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Debug|x64.Build.0 = Debug|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Release|x64.ActiveCfg = Release|x64 - {FE438201-74A1-5236-AE07-E502B853EA18}.Release|x64.Build.0 = Release|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Bounds|x64.ActiveCfg = Release|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Bounds|x64.Build.0 = Release|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Debug|x64.ActiveCfg = Debug|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Debug|x64.Build.0 = Debug|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Release|x64.ActiveCfg = Release|x64 - {C7533C60-BF48-5844-8220-A488387AC016}.Release|x64.Build.0 = Release|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Bounds|x64.ActiveCfg = Release|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Bounds|x64.Build.0 = Release|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Debug|x64.ActiveCfg = Debug|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Debug|x64.Build.0 = Debug|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Release|x64.ActiveCfg = Release|x64 - {DA4DE504-7FAF-5BEF-8B4E-395D24CB6CB4}.Release|x64.Build.0 = Release|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Bounds|x64.ActiveCfg = Release|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Bounds|x64.Build.0 = Release|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Debug|x64.ActiveCfg = Debug|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Debug|x64.Build.0 = Debug|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Release|x64.ActiveCfg = Release|x64 - {A39B87BF-6846-559A-A01F-6251A0FE856E}.Release|x64.Build.0 = Release|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Bounds|x64.ActiveCfg = Release|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Bounds|x64.Build.0 = Release|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Debug|x64.ActiveCfg = Debug|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Debug|x64.Build.0 = Debug|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Release|x64.ActiveCfg = Release|x64 - {DBB982C6-E9E4-5535-ADC2-D0BA1E18F66F}.Release|x64.Build.0 = Release|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Bounds|x64.ActiveCfg = Release|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Bounds|x64.Build.0 = Release|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Debug|x64.ActiveCfg = Debug|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Debug|x64.Build.0 = Debug|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Release|x64.ActiveCfg = Release|x64 - {3B5B2AE4-53B3-5021-B5CA-15BC94EAB282}.Release|x64.Build.0 = Release|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Bounds|x64.ActiveCfg = Release|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Bounds|x64.Build.0 = Release|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Debug|x64.ActiveCfg = Debug|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Debug|x64.Build.0 = Debug|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Release|x64.ActiveCfg = Release|x64 - {644A443A-1066-57D2-9DFA-35CD9E9A46BE}.Release|x64.Build.0 = Release|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Bounds|x64.ActiveCfg = Release|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Bounds|x64.Build.0 = Release|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Debug|x64.ActiveCfg = Debug|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Debug|x64.Build.0 = Debug|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Release|x64.ActiveCfg = Release|x64 - {ABC70BB4-125D-54DD-B962-6131F490AB10}.Release|x64.Build.0 = Release|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Bounds|x64.ActiveCfg = Release|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Bounds|x64.Build.0 = Release|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Debug|x64.ActiveCfg = Debug|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Debug|x64.Build.0 = Debug|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Release|x64.ActiveCfg = Release|x64 - {6DA137DD-449E-57F1-8489-686CC307A561}.Release|x64.Build.0 = Release|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Bounds|x64.ActiveCfg = Release|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Bounds|x64.Build.0 = Release|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Debug|x64.ActiveCfg = Debug|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Debug|x64.Build.0 = Debug|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Release|x64.ActiveCfg = Release|x64 - {A2FDE99A-204A-5C10-995F-FD56039385C8}.Release|x64.Build.0 = Release|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Bounds|x64.ActiveCfg = Release|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Bounds|x64.Build.0 = Release|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Debug|x64.ActiveCfg = Debug|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Debug|x64.Build.0 = Debug|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Release|x64.ActiveCfg = Release|x64 - {43D44B32-899D-511D-9CF6-18CF7D3844CF}.Release|x64.Build.0 = Release|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Bounds|x64.ActiveCfg = Release|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Bounds|x64.Build.0 = Release|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Debug|x64.ActiveCfg = Debug|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Debug|x64.Build.0 = Debug|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Release|x64.ActiveCfg = Release|x64 - {1F87EA7A-211A-562D-95ED-00F935966948}.Release|x64.Build.0 = Release|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Bounds|x64.ActiveCfg = Release|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Bounds|x64.Build.0 = Release|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Debug|x64.ActiveCfg = Debug|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Debug|x64.Build.0 = Debug|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Release|x64.ActiveCfg = Release|x64 - {6F79E30E-34D8-5938-B8C4-7B0FA9FDB5A6}.Release|x64.Build.0 = Release|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Bounds|x64.ActiveCfg = Release|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Bounds|x64.Build.0 = Release|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Debug|x64.ActiveCfg = Debug|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Debug|x64.Build.0 = Debug|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Release|x64.ActiveCfg = Release|x64 - {0434B036-FB8A-58B1-A075-B3D2D94BF492}.Release|x64.Build.0 = Release|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Bounds|x64.ActiveCfg = Release|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Bounds|x64.Build.0 = Release|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Debug|x64.ActiveCfg = Debug|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Debug|x64.Build.0 = Debug|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Release|x64.ActiveCfg = Release|x64 - {FFD4329F-ED9E-5EB6-BFEE-EE24E3759EA9}.Release|x64.Build.0 = Release|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Bounds|x64.ActiveCfg = Release|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Bounds|x64.Build.0 = Release|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Debug|x64.ActiveCfg = Debug|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Debug|x64.Build.0 = Debug|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Release|x64.ActiveCfg = Release|x64 - {3C904B25-FE98-55A8-A9AB-2CBA065AE297}.Release|x64.Build.0 = Release|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Bounds|x64.ActiveCfg = Release|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Bounds|x64.Build.0 = Release|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Debug|x64.ActiveCfg = Debug|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Debug|x64.Build.0 = Debug|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Release|x64.ActiveCfg = Release|x64 - {44E4C722-DCE1-5A8A-A586-81D329771F66}.Release|x64.Build.0 = Release|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Bounds|x64.ActiveCfg = Release|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Bounds|x64.Build.0 = Release|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Debug|x64.ActiveCfg = Debug|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Debug|x64.Build.0 = Debug|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Release|x64.ActiveCfg = Release|x64 - {D7A0A7EA-6C5A-5953-862B-0CF3B779C34F}.Release|x64.Build.0 = Release|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Bounds|x64.ActiveCfg = Release|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Bounds|x64.Build.0 = Release|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Debug|x64.ActiveCfg = Debug|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Debug|x64.Build.0 = Debug|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Release|x64.ActiveCfg = Release|x64 - {1E4C57D6-BB15-56CD-A901-6EC5C0835FBE}.Release|x64.Build.0 = Release|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Bounds|x64.ActiveCfg = Release|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Bounds|x64.Build.0 = Release|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Debug|x64.ActiveCfg = Debug|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Debug|x64.Build.0 = Debug|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Release|x64.ActiveCfg = Release|x64 - {78FB823E-35FE-5D1D-B44D-17C22FDF6003}.Release|x64.Build.0 = Release|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Bounds|x64.ActiveCfg = Release|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Bounds|x64.Build.0 = Release|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Debug|x64.ActiveCfg = Debug|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Debug|x64.Build.0 = Debug|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Release|x64.ActiveCfg = Release|x64 - {8ED64FCC-6F3E-55FF-AA04-B0F2A67A3044}.Release|x64.Build.0 = Release|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Bounds|x64.ActiveCfg = Release|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Bounds|x64.Build.0 = Release|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Debug|x64.ActiveCfg = Debug|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Debug|x64.Build.0 = Debug|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Release|x64.ActiveCfg = Release|x64 - {65C872FA-2DC7-5EC2-9A19-EDB4FA325934}.Release|x64.Build.0 = Release|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Bounds|x64.ActiveCfg = Release|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Bounds|x64.Build.0 = Release|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Debug|x64.ActiveCfg = Debug|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Debug|x64.Build.0 = Debug|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Release|x64.ActiveCfg = Release|x64 - {BD5AFBAD-6C0C-5C44-912D-D26745CF8F62}.Release|x64.Build.0 = Release|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Bounds|x64.ActiveCfg = Release|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Bounds|x64.Build.0 = Release|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Debug|x64.ActiveCfg = Debug|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Debug|x64.Build.0 = Debug|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Release|x64.ActiveCfg = Release|x64 - {5FD892A2-7F18-5DAA-B4DF-1C79A45E7025}.Release|x64.Build.0 = Release|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Bounds|x64.ActiveCfg = Release|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Bounds|x64.Build.0 = Release|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Debug|x64.ActiveCfg = Debug|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Debug|x64.Build.0 = Debug|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Release|x64.ActiveCfg = Release|x64 - {FF2D5865-1799-5EE8-A46B-3CD86EA9D9EE}.Release|x64.Build.0 = Release|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Bounds|x64.ActiveCfg = Release|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Bounds|x64.Build.0 = Release|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Debug|x64.ActiveCfg = Debug|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Debug|x64.Build.0 = Debug|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Release|x64.ActiveCfg = Release|x64 - {C5AA04DD-F91B-5156-BD40-4A761058AC64}.Release|x64.Build.0 = Release|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Bounds|x64.ActiveCfg = Release|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Bounds|x64.Build.0 = Release|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Debug|x64.ActiveCfg = Debug|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Debug|x64.Build.0 = Debug|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Release|x64.ActiveCfg = Release|x64 - {F2525F78-38CD-5E36-A854-E16BE8A1B8FF}.Release|x64.Build.0 = Release|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Bounds|x64.ActiveCfg = Release|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Bounds|x64.Build.0 = Release|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Debug|x64.ActiveCfg = Debug|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Debug|x64.Build.0 = Debug|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Release|x64.ActiveCfg = Release|x64 - {170E9760-4036-5CC4-951D-DAFDBCEF7BEA}.Release|x64.Build.0 = Release|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Bounds|x64.ActiveCfg = Release|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Bounds|x64.Build.0 = Release|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Debug|x64.ActiveCfg = Debug|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Debug|x64.Build.0 = Debug|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Release|x64.ActiveCfg = Release|x64 - {DDDCFA1C-DC3E-54B7-9B3A-497B4FBE1510}.Release|x64.Build.0 = Release|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Bounds|x64.ActiveCfg = Release|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Bounds|x64.Build.0 = Release|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Debug|x64.ActiveCfg = Debug|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Debug|x64.Build.0 = Debug|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Release|x64.ActiveCfg = Release|x64 - {83DC33D4-9323-56B1-865A-56CD516EE52A}.Release|x64.Build.0 = Release|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Bounds|x64.ActiveCfg = Release|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Bounds|x64.Build.0 = Release|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Debug|x64.ActiveCfg = Debug|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Debug|x64.Build.0 = Debug|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Release|x64.ActiveCfg = Release|x64 - {DD84503B-AB8B-5FFD-B15F-8DE447F7BCDD}.Release|x64.Build.0 = Release|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Bounds|x64.ActiveCfg = Release|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Bounds|x64.Build.0 = Release|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Debug|x64.ActiveCfg = Debug|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Debug|x64.Build.0 = Debug|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Release|x64.ActiveCfg = Release|x64 - {1B8FE336-2272-5424-A36A-7C786F9FE388}.Release|x64.Build.0 = Release|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Bounds|x64.ActiveCfg = Release|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Bounds|x64.Build.0 = Release|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Debug|x64.ActiveCfg = Debug|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Debug|x64.Build.0 = Debug|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Release|x64.ActiveCfg = Release|x64 - {BF01268F-E755-5577-B8D7-9014D7591A2A}.Release|x64.Build.0 = Release|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Bounds|x64.ActiveCfg = Release|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Bounds|x64.Build.0 = Release|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Debug|x64.ActiveCfg = Debug|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Debug|x64.Build.0 = Debug|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Release|x64.ActiveCfg = Release|x64 - {4B95DD96-AB0A-571E-81E8-3035ECCC8D47}.Release|x64.Build.0 = Release|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Bounds|x64.ActiveCfg = Release|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Bounds|x64.Build.0 = Release|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Debug|x64.ActiveCfg = Debug|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Debug|x64.Build.0 = Debug|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Release|x64.ActiveCfg = Release|x64 - {21F54BD0-152A-547C-A940-2BCFEA8D1730}.Release|x64.Build.0 = Release|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Bounds|x64.ActiveCfg = Release|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Bounds|x64.Build.0 = Release|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Debug|x64.ActiveCfg = Debug|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Debug|x64.Build.0 = Debug|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Release|x64.ActiveCfg = Release|x64 - {66361165-1489-5B17-8969-4A6253C00931}.Release|x64.Build.0 = Release|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Bounds|x64.ActiveCfg = Release|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Bounds|x64.Build.0 = Release|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Debug|x64.ActiveCfg = Debug|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Debug|x64.Build.0 = Debug|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Release|x64.ActiveCfg = Release|x64 - {1DD0C70B-EA9D-593E-BF23-72FEAB6849DF}.Release|x64.Build.0 = Release|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Bounds|x64.ActiveCfg = Release|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Bounds|x64.Build.0 = Release|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Debug|x64.ActiveCfg = Debug|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Debug|x64.Build.0 = Debug|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Release|x64.ActiveCfg = Release|x64 - {E5F82767-7DC7-599F-BC29-AAFE4AC98060}.Release|x64.Build.0 = Release|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Bounds|x64.ActiveCfg = Release|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Bounds|x64.Build.0 = Release|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Debug|x64.ActiveCfg = Debug|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Debug|x64.Build.0 = Debug|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Release|x64.ActiveCfg = Release|x64 - {09D7C8FE-DD9B-5C1C-9A4D-9D61B26E878E}.Release|x64.Build.0 = Release|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Bounds|x64.ActiveCfg = Release|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Bounds|x64.Build.0 = Release|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Debug|x64.ActiveCfg = Debug|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Debug|x64.Build.0 = Debug|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Release|x64.ActiveCfg = Release|x64 - {2310A14E-5FFA-5939-885C-DA681EAFC168}.Release|x64.Build.0 = Release|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Bounds|x64.ActiveCfg = Release|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Bounds|x64.Build.0 = Release|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Debug|x64.ActiveCfg = Debug|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Debug|x64.Build.0 = Debug|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Release|x64.ActiveCfg = Release|x64 - {3E1BAF09-02C0-55BF-8683-3FAACFE6F137}.Release|x64.Build.0 = Release|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Bounds|x64.ActiveCfg = Release|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Bounds|x64.Build.0 = Release|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Debug|x64.ActiveCfg = Debug|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Debug|x64.Build.0 = Debug|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Release|x64.ActiveCfg = Release|x64 - {8A29FFD3-0F85-58FE-94C1-ECA085D4C29A}.Release|x64.Build.0 = Release|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Bounds|x64.ActiveCfg = Release|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Bounds|x64.Build.0 = Release|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Debug|x64.ActiveCfg = Debug|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Debug|x64.Build.0 = Debug|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Release|x64.ActiveCfg = Release|x64 - {94AD32DE-8AA2-547E-90F9-99169687406F}.Release|x64.Build.0 = Release|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Bounds|x64.ActiveCfg = Release|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Bounds|x64.Build.0 = Release|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Debug|x64.ActiveCfg = Debug|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Debug|x64.Build.0 = Debug|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.ActiveCfg = Release|x64 - {EC934204-1D3A-5575-A500-CB7923C440E2}.Release|x64.Build.0 = Release|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.ActiveCfg = Release|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Bounds|x64.Build.0 = Release|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.ActiveCfg = Debug|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Debug|x64.Build.0 = Debug|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.ActiveCfg = Release|x64 - {0B5C69D2-8502-5FED-A22C-CE6A0269D9F1}.Release|x64.Build.0 = Release|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.ActiveCfg = Release|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Bounds|x64.Build.0 = Release|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.ActiveCfg = Debug|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Debug|x64.Build.0 = Debug|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.ActiveCfg = Release|x64 - {37555756-6D42-5E46-B455-E58E3D1E8E0C}.Release|x64.Build.0 = Release|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.ActiveCfg = Release|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Bounds|x64.Build.0 = Release|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Debug|x64.ActiveCfg = Debug|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Debug|x64.Build.0 = Debug|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Release|x64.ActiveCfg = Release|x64 - {8336DC7C-954B-5076-9315-D7DC5317282B}.Release|x64.Build.0 = Release|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Bounds|x64.ActiveCfg = Release|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Bounds|x64.Build.0 = Release|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Debug|x64.ActiveCfg = Debug|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Debug|x64.Build.0 = Debug|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Release|x64.ActiveCfg = Release|x64 - {04546E35-9A3A-5629-8282-3683A5D848F9}.Release|x64.Build.0 = Release|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Bounds|x64.ActiveCfg = Release|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Bounds|x64.Build.0 = Release|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Debug|x64.ActiveCfg = Debug|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Debug|x64.Build.0 = Debug|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Release|x64.ActiveCfg = Release|x64 - {7C859385-3602-59D1-9A7E-E81E7C6EBBE4}.Release|x64.Build.0 = Release|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Bounds|x64.ActiveCfg = Release|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Bounds|x64.Build.0 = Release|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Debug|x64.ActiveCfg = Debug|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Debug|x64.Build.0 = Debug|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Release|x64.ActiveCfg = Release|x64 - {46A84616-92E0-567E-846E-DF0C203CF0D2}.Release|x64.Build.0 = Release|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Bounds|x64.ActiveCfg = Release|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Bounds|x64.Build.0 = Release|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Debug|x64.ActiveCfg = Debug|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Debug|x64.Build.0 = Debug|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Release|x64.ActiveCfg = Release|x64 - {910ED78F-AE00-5547-ADEC-A0E54BF98B8D}.Release|x64.Build.0 = Release|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Bounds|x64.ActiveCfg = Release|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Bounds|x64.Build.0 = Release|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Debug|x64.ActiveCfg = Debug|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Debug|x64.Build.0 = Debug|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Release|x64.ActiveCfg = Release|x64 - {68C6DB83-7D0F-5F31-9307-6489E21F74E5}.Release|x64.Build.0 = Release|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Bounds|x64.ActiveCfg = Release|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Bounds|x64.Build.0 = Release|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Debug|x64.ActiveCfg = Debug|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Debug|x64.Build.0 = Debug|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Release|x64.ActiveCfg = Release|x64 - {E63B6F76-5CD3-5757-93D7-E050CB412F23}.Release|x64.Build.0 = Release|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Bounds|x64.ActiveCfg = Release|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Bounds|x64.Build.0 = Release|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Debug|x64.ActiveCfg = Debug|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Debug|x64.Build.0 = Debug|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Release|x64.ActiveCfg = Release|x64 - {712CF492-5D74-5464-93CA-EAB5BE54D09B}.Release|x64.Build.0 = Release|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Bounds|x64.ActiveCfg = Release|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Bounds|x64.Build.0 = Release|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Debug|x64.ActiveCfg = Debug|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Debug|x64.Build.0 = Debug|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Release|x64.ActiveCfg = Release|x64 - {D2BAD63B-0914-5014-BCE8-8D767A871F06}.Release|x64.Build.0 = Release|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Bounds|x64.ActiveCfg = Release|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Bounds|x64.Build.0 = Release|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Debug|x64.ActiveCfg = Debug|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Debug|x64.Build.0 = Debug|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Release|x64.ActiveCfg = Release|x64 - {98E5183C-F4A6-5DAA-AFB8-B63F75ACA860}.Release|x64.Build.0 = Release|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Bounds|x64.ActiveCfg = Release|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Bounds|x64.Build.0 = Release|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Debug|x64.ActiveCfg = Debug|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Debug|x64.Build.0 = Debug|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Release|x64.ActiveCfg = Release|x64 - {FDC1EE9E-73F7-5EF2-9868-E44ACB00F168}.Release|x64.Build.0 = Release|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Bounds|x64.ActiveCfg = Release|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Bounds|x64.Build.0 = Release|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Debug|x64.ActiveCfg = Debug|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Debug|x64.Build.0 = Debug|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Release|x64.ActiveCfg = Release|x64 - {515DEC49-6C0F-5F02-AC05-69AC6AF51639}.Release|x64.Build.0 = Release|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Bounds|x64.ActiveCfg = Release|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Bounds|x64.Build.0 = Release|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Debug|x64.ActiveCfg = Debug|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Debug|x64.Build.0 = Debug|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Release|x64.ActiveCfg = Release|x64 - {70163155-93C1-5816-A1D4-1EEA0215298C}.Release|x64.Build.0 = Release|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Bounds|x64.ActiveCfg = Release|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Bounds|x64.Build.0 = Release|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Debug|x64.ActiveCfg = Debug|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Debug|x64.Build.0 = Debug|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Release|x64.ActiveCfg = Release|x64 - {EFB41F48-1BF6-549C-8D93-59F99B3EA5D5}.Release|x64.Build.0 = Release|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Bounds|x64.ActiveCfg = Release|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Bounds|x64.Build.0 = Release|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Debug|x64.ActiveCfg = Debug|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Debug|x64.Build.0 = Debug|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Release|x64.ActiveCfg = Release|x64 - {AB011392-76C6-5D67-9623-CA9B2680B899}.Release|x64.Build.0 = Release|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Bounds|x64.ActiveCfg = Release|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Bounds|x64.Build.0 = Release|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Debug|x64.ActiveCfg = Debug|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Debug|x64.Build.0 = Debug|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Release|x64.ActiveCfg = Release|x64 - {3072F4ED-E1F0-5C16-8CCA-CE3AE6D8760A}.Release|x64.Build.0 = Release|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Bounds|x64.ActiveCfg = Release|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Bounds|x64.Build.0 = Release|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Debug|x64.ActiveCfg = Debug|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Debug|x64.Build.0 = Debug|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Release|x64.ActiveCfg = Release|x64 - {17AE7011-A346-5BAE-A021-552E7A3A86DD}.Release|x64.Build.0 = Release|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Bounds|x64.ActiveCfg = Release|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Bounds|x64.Build.0 = Release|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Debug|x64.ActiveCfg = Debug|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Debug|x64.Build.0 = Debug|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Release|x64.ActiveCfg = Release|x64 - {6AD8FA57-72AB-5C43-A2C6-02D5D26AC432}.Release|x64.Build.0 = Release|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Bounds|x64.ActiveCfg = Release|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Bounds|x64.Build.0 = Release|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Debug|x64.ActiveCfg = Debug|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Debug|x64.Build.0 = Debug|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Release|x64.ActiveCfg = Release|x64 - {5F9BBC7F-3CE1-5F77-956F-B7650E1FE52E}.Release|x64.Build.0 = Release|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Bounds|x64.ActiveCfg = Release|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Bounds|x64.Build.0 = Release|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Debug|x64.ActiveCfg = Debug|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Debug|x64.Build.0 = Debug|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Release|x64.ActiveCfg = Release|x64 - {9A7E3C5B-2D1F-4E8A-9B3C-F6D0E1A2B4C8}.Release|x64.Build.0 = Release|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Bounds|x64.ActiveCfg = Release|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Bounds|x64.Build.0 = Release|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Debug|x64.ActiveCfg = Debug|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Debug|x64.Build.0 = Debug|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Release|x64.ActiveCfg = Release|x64 - {D4F47DD8-A0E7-5081-808A-5286F873DC13}.Release|x64.Build.0 = Release|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Bounds|x64.ActiveCfg = Release|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Bounds|x64.Build.0 = Release|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Debug|x64.ActiveCfg = Debug|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Debug|x64.Build.0 = Debug|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Release|x64.ActiveCfg = Release|x64 - {2EB628C9-EC23-5394-8BEB-B7542360FEAE}.Release|x64.Build.0 = Release|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Bounds|x64.ActiveCfg = Release|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Bounds|x64.Build.0 = Release|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Debug|x64.ActiveCfg = Debug|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Debug|x64.Build.0 = Debug|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Release|x64.ActiveCfg = Release|x64 - {B9B1AF40-53E1-54A3-B2F1-85EFE95F5A89}.Release|x64.Build.0 = Release|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Bounds|x64.ActiveCfg = Release|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Bounds|x64.Build.0 = Release|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Debug|x64.ActiveCfg = Debug|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Debug|x64.Build.0 = Debug|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Release|x64.ActiveCfg = Release|x64 - {DA1CAEE2-340C-51E7-980B-916545074600}.Release|x64.Build.0 = Release|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Bounds|x64.ActiveCfg = Release|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Bounds|x64.Build.0 = Release|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Debug|x64.ActiveCfg = Debug|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Debug|x64.Build.0 = Debug|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Release|x64.ActiveCfg = Release|x64 - {B2E94D3C-45D7-5BE8-AEA0-0E9234FCF50D}.Release|x64.Build.0 = Release|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Bounds|x64.ActiveCfg = Release|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Bounds|x64.Build.0 = Release|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Debug|x64.ActiveCfg = Debug|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Debug|x64.Build.0 = Debug|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Release|x64.ActiveCfg = Release|x64 - {1C758320-DE0A-50F3-8892-B0F7397CFA61}.Release|x64.Build.0 = Release|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Bounds|x64.ActiveCfg = Release|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Bounds|x64.Build.0 = Release|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Debug|x64.ActiveCfg = Debug|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Debug|x64.Build.0 = Debug|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Release|x64.ActiveCfg = Release|x64 - {9B1C17E4-3086-53B9-B1DC-8A39117E237F}.Release|x64.Build.0 = Release|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Bounds|x64.ActiveCfg = Release|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Bounds|x64.Build.0 = Release|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Debug|x64.ActiveCfg = Debug|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Debug|x64.Build.0 = Debug|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Release|x64.ActiveCfg = Release|x64 - {2861A99F-3390-52B4-A2D8-0F80A62DB108}.Release|x64.Build.0 = Release|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Bounds|x64.ActiveCfg = Release|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Bounds|x64.Build.0 = Release|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Debug|x64.ActiveCfg = Debug|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Debug|x64.Build.0 = Debug|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Release|x64.ActiveCfg = Release|x64 - {5B1DFFF7-6A59-5955-B77D-42DBF12721D1}.Release|x64.Build.0 = Release|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Bounds|x64.ActiveCfg = Release|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Bounds|x64.Build.0 = Release|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Debug|x64.ActiveCfg = Debug|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Debug|x64.Build.0 = Debug|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Release|x64.ActiveCfg = Release|x64 - {1308E147-8B51-55E0-B475-10A0053F9AAF}.Release|x64.Build.0 = Release|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Bounds|x64.ActiveCfg = Bounds|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Bounds|x64.Build.0 = Bounds|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Debug|x64.ActiveCfg = Debug|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Debug|x64.Build.0 = Debug|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Release|x64.ActiveCfg = Release|x64 - {7F6B25EE-CD22-4E4C-898D-A0F846E6E9D4}.Release|x64.Build.0 = Release|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Bounds|x64.ActiveCfg = Debug|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Bounds|x64.Build.0 = Debug|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Debug|x64.ActiveCfg = Debug|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Debug|x64.Build.0 = Debug|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Release|x64.ActiveCfg = Release|x64 - {6396B488-4D34-48B2-8639-EEB90707405B}.Release|x64.Build.0 = Release|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Bounds|x64.ActiveCfg = Bounds|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Bounds|x64.Build.0 = Bounds|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Debug|x64.ActiveCfg = Debug|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Debug|x64.Build.0 = Debug|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Release|x64.ActiveCfg = Release|x64 - {C86CA2EB-81B5-4411-B5B7-E983314E02DA}.Release|x64.Build.0 = Release|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Bounds|x64.ActiveCfg = Release|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Bounds|x64.Build.0 = Release|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Debug|x64.ActiveCfg = Debug|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Debug|x64.Build.0 = Debug|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Release|x64.ActiveCfg = Release|x64 - {34442A32-31DE-45A8-AD36-0ECFE4095523}.Release|x64.Build.0 = Release|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Bounds|x64.ActiveCfg = Debug|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Bounds|x64.Build.0 = Debug|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Debug|x64.ActiveCfg = Debug|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Debug|x64.Build.0 = Debug|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Release|x64.ActiveCfg = Release|x64 - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5}.Release|x64.Build.0 = Release|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Bounds|x64.ActiveCfg = Debug|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Bounds|x64.Build.0 = Debug|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Debug|x64.ActiveCfg = Debug|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Debug|x64.Build.0 = Debug|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Release|x64.ActiveCfg = Release|x64 - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE}.Release|x64.Build.0 = Release|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Bounds|x64.ActiveCfg = Debug|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Bounds|x64.Build.0 = Debug|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Debug|x64.ActiveCfg = Debug|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Debug|x64.Build.0 = Debug|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Release|x64.ActiveCfg = Release|x64 - {AF250D69-786B-40FA-A125-FD3F448CC283}.Release|x64.Build.0 = Release|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Bounds|x64.ActiveCfg = Debug|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Bounds|x64.Build.0 = Debug|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Debug|x64.ActiveCfg = Debug|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Debug|x64.Build.0 = Debug|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Release|x64.ActiveCfg = Release|x64 - {91D55536-1DE3-4279-9DD1-CA2CED068F42}.Release|x64.Build.0 = Release|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Bounds|x64.ActiveCfg = Debug|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Bounds|x64.Build.0 = Debug|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Debug|x64.ActiveCfg = Debug|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Debug|x64.Build.0 = Debug|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Release|x64.ActiveCfg = Release|x64 - {BF5AD9CA-6FD6-49C7-B351-0630C11479C0}.Release|x64.Build.0 = Release|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Bounds|x64.ActiveCfg = Debug|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Bounds|x64.Build.0 = Debug|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Debug|x64.ActiveCfg = Debug|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Debug|x64.Build.0 = Debug|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Release|x64.ActiveCfg = Release|x64 - {EEE765C8-6812-4F9F-A100-42AA71921926}.Release|x64.Build.0 = Release|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Bounds|x64.ActiveCfg = Debug|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Bounds|x64.Build.0 = Debug|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Debug|x64.ActiveCfg = Debug|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Debug|x64.Build.0 = Debug|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Release|x64.ActiveCfg = Release|x64 - {8EF1E1AE-2226-4A9B-8942-CAB531956ED3}.Release|x64.Build.0 = Release|x64 - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Bounds|x64.ActiveCfg = Release|Any CPU - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Bounds|x64.Build.0 = Release|Any CPU - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Debug|x64.ActiveCfg = Debug|Any CPU - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Debug|x64.Build.0 = Debug|Any CPU - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Release|x64.ActiveCfg = Release|Any CPU - {5A9BADE9-763A-4B25-A65D-5E3EC044E4CF}.Release|x64.Build.0 = Release|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Bounds|x64.ActiveCfg = Release|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Bounds|x64.Build.0 = Release|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Debug|x64.ActiveCfg = Debug|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Debug|x64.Build.0 = Debug|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Release|x64.ActiveCfg = Release|Any CPU - {E5E9DDC7-2855-4D92-AD46-960AC4C46457}.Release|x64.Build.0 = Release|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Bounds|x64.ActiveCfg = Release|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Bounds|x64.Build.0 = Release|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Debug|x64.ActiveCfg = Debug|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Debug|x64.Build.0 = Debug|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Release|x64.ActiveCfg = Release|Any CPU - {4C7D6B65-A331-4ED7-9B53-3301E714F8E7}.Release|x64.Build.0 = Release|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Bounds|x64.ActiveCfg = Release|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Bounds|x64.Build.0 = Release|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Debug|x64.ActiveCfg = Debug|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Debug|x64.Build.0 = Debug|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Release|x64.ActiveCfg = Release|Any CPU - {4983B3EB-F54A-4DED-B89C-1A4D6A12C96D}.Release|x64.Build.0 = Release|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Bounds|x64.ActiveCfg = Release|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Bounds|x64.Build.0 = Release|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Debug|x64.ActiveCfg = Debug|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Debug|x64.Build.0 = Debug|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Release|x64.ActiveCfg = Release|Any CPU - {4E4CE84F-BB35-416A-8E4F-B8C096DA32B7}.Release|x64.Build.0 = Release|Any CPU - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Bounds|x64.ActiveCfg = Debug|x64 - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Bounds|x64.Build.0 = Debug|x64 - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Debug|x64.ActiveCfg = Debug|x64 - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Debug|x64.Build.0 = Debug|x64 - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Release|x64.ActiveCfg = Release|x64 - {5DB1CEDA-B7AA-4594-9CE0-6D3A6F5763DF}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6D69D131-C928-6A46-F508-A4A608CBE30A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {8D3F63D3-8EF8-43FD-B5C1-4DF686A242D5} = {6D69D131-C928-6A46-F508-A4A608CBE30A} - {8AC88510-3A3E-4AD0-B64D-C83AC81AD8FE} = {6D69D131-C928-6A46-F508-A4A608CBE30A} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9F385E4A-ED83-4896-ADB8-335A2065B865} - EndGlobalSection -EndGlobal diff --git a/Src/Common/FieldWorks/FieldWorks.cs b/Src/Common/FieldWorks/FieldWorks.cs index 2ff8466621..ca3202894e 100644 --- a/Src/Common/FieldWorks/FieldWorks.cs +++ b/Src/Common/FieldWorks/FieldWorks.cs @@ -3665,7 +3665,7 @@ internal static void InitializeLocalizationManager() var version = $"{versionObj.Major}.{versionObj.Minor}.{versionObj.Build}"; // First create localization manager for Chorus with english LocalizationManagerWinforms.Create("en", - "Chorus", "Chorus", version, installedL10nBaseDir, userL10nBaseDir, null, "flex_localization@sil.org", new [] { "Chorus", "LibChorus" }); + "Chorus", "Chorus", version, installedL10nBaseDir, userL10nBaseDir, null, new [] { "Chorus", "LibChorus" }); // Now that we have one manager initialized check and see if the users UI language has // localizations available var uiCulture = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; @@ -3678,7 +3678,7 @@ internal static void InitializeLocalizationManager() versionObj = Assembly.GetAssembly(typeof(ErrorReport)).GetName().Version; version = $"{versionObj.Major}.{versionObj.Minor}.{versionObj.Build}"; LocalizationManagerWinforms.Create(LocalizationManager.UILanguageId, "Palaso", "Palaso", version, installedL10nBaseDir, - userL10nBaseDir, null, "flex_localization@sil.org", new [] { "SIL.Windows.Forms" }); + userL10nBaseDir, null, new [] { "SIL.Windows.Forms" }); } catch (Exception e) { diff --git a/build.ps1 b/build.ps1 index 010283a5c8..84d4448d8b 100644 --- a/build.ps1 +++ b/build.ps1 @@ -77,11 +77,20 @@ Only used with -BuildInstaller. Enables local signing when signing tools are available. By default, local installer builds capture files to sign later instead of signing. -.PARAMETER LcmMode - Controls how FieldWorks resolves liblcm. - - Auto: use package mode by default, but report whether local Localizations/LCM inputs are ready. - - Package: force the package-backed path. - - Local: force the nested Localizations/LCM source-backed path. +.PARAMETER LocalPalaso + If set, packs the local libpalaso checkout referenced by FW_LOCAL_PALASO into the repo-local NuGet feed + and builds FieldWorks against those local packages. + +.PARAMETER LocalLcm + If set, packs the local liblcm checkout referenced by FW_LOCAL_LCM into the repo-local NuGet feed + and builds FieldWorks against those local packages. + +.PARAMETER LocalChorus + If set, packs the local chorus checkout referenced by FW_LOCAL_CHORUS into the repo-local NuGet feed + and builds FieldWorks against those local packages. + +.PARAMETER LocalPackageVersion + The fixed version used for locally packed dependency packages. Default is 99.0.0-local. .PARAMETER ManagedDebugType Optionally overrides the managed project PDB format for this build. @@ -107,12 +116,12 @@ Builds Debug serially with detailed logging. .EXAMPLE - .\build.ps1 -LcmMode Local - Builds FieldWorks against the nested Localizations/LCM checkout. + .\build.ps1 -LocalPalaso -LocalLcm -LocalChorus + Packs the selected local dependency repos into the repo-local NuGet feed, then builds FieldWorks against those packages. .EXAMPLE - .\build.ps1 -LcmMode Local -ManagedDebugType portable - Builds FieldWorks against the nested Localizations/LCM checkout with portable managed PDBs for VS Code debugging. + .\build.ps1 -LocalPalaso -LocalLcm -LocalChorus -ManagedDebugType portable + Builds FieldWorks against the locally packed dependency packages with portable FieldWorks PDBs for VS Code debugging. .NOTES FieldWorks is x64-only. The x86 platform is no longer supported. @@ -141,8 +150,10 @@ param( [switch]$ForceInstallerOnly, [switch]$SignInstaller, [switch]$TraceCrashes, - [ValidateSet('Auto', 'Package', 'Local')] - [string]$LcmMode = 'Auto', + [switch]$LocalPalaso, + [switch]$LocalLcm, + [switch]$LocalChorus, + [string]$LocalPackageVersion = '99.0.0-local', [ValidateSet('portable', 'full', 'pdbonly', 'embedded')] [string]$ManagedDebugType, [switch]$SkipDependencyCheck @@ -151,7 +162,6 @@ param( $ErrorActionPreference = "Stop" $platform = 'x64' -$validLcmModes = @('Auto', 'Package', 'Local') # PowerShell requires single-dash named parameters. Some callers still pass GNU-style # double-dash options, which bind positionally before the script starts. Normalize the @@ -166,23 +176,8 @@ if ($Configuration -like "--*") { Write-Output "[WARN] Detected '--TraceCrashes' passed without PowerShell switch parsing. Using -TraceCrashes and defaulting Configuration to Debug." } } - 'LcmMode' { - if ([string]::IsNullOrWhiteSpace($TestFilter)) { - throw "Detected '--LcmMode' without a mode value. Use -LcmMode <Auto|Package|Local>." - } - - $requestedMode = $TestFilter.Trim() - if ($requestedMode -notin $validLcmModes) { - throw "Invalid LCM mode '$requestedMode'. Use -LcmMode with one of: $($validLcmModes -join ', ')." - } - - $LcmMode = $requestedMode - $Configuration = 'Debug' - $TestFilter = '' - Write-Output "[WARN] Detected '--LcmMode $requestedMode' passed without PowerShell parameter parsing. Using -LcmMode $requestedMode and defaulting Configuration to Debug." - } default { - throw "Invalid Configuration value '$Configuration'. Use PowerShell parameter syntax like -Configuration Release or -LcmMode Local." + throw "Invalid Configuration value '$Configuration'. Use PowerShell parameter syntax like -Configuration Release." } } } @@ -249,6 +244,9 @@ $cleanupArgs = @{ } $testExitCode = 0 +$repoNuGetPackages = Join-Path $PSScriptRoot 'packages' +$previousNuGetPackages = [Environment]::GetEnvironmentVariable('NUGET_PACKAGES') +$didOverrideNuGetPackages = $false function Get-RepoStamp { $gitHead = & git rev-parse HEAD @@ -303,28 +301,22 @@ function Get-RepoStamp { } function Get-DebugRebuildCheckPathspecs { - param( - [Parameter(Mandatory = $true)][ValidateSet('Package', 'Local')][string]$ResolvedLcmMode - ) - $pathspecs = @( 'build.ps1', + 'test.ps1', + 'nuget.config', 'Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', + 'Build/SilVersions.props', + 'Build/SilVersions.Local.props', 'FieldWorks.proj', + 'FieldWorks.sln', 'Build', 'Src', 'Lib' ) - if ($ResolvedLcmMode -eq 'Local') { - $pathspecs += @('FieldWorks.LocalLcm.sln', 'Localizations/LCM') - } - else { - $pathspecs += 'FieldWorks.sln' - } - return $pathspecs | ForEach-Object { $_ -replace '\\', '/' } } @@ -351,50 +343,25 @@ function Get-BuildStampPath { return Join-Path $outputDir "BuildStamp.json" } -function Get-LocalLcmState { +function Get-SelectedLocalDependencies { param( - [Parameter(Mandatory = $true)][string]$RepoRoot, - [Parameter(Mandatory = $true)][string]$ConfigurationName - ) - - $nestedRoot = Join-Path $RepoRoot 'Localizations\LCM' - $localSolution = Join-Path $RepoRoot 'FieldWorks.LocalLcm.sln' - $lcmSolution = Join-Path $nestedRoot 'LCM.sln' - $artifactsDir = Join-Path $nestedRoot ("artifacts\{0}\net462" -f $ConfigurationName) - $buildTasksPath = Join-Path $artifactsDir 'SIL.LCModel.Build.Tasks.dll' - - return [pscustomobject]@{ - NestedRoot = $nestedRoot - LocalSolutionPath = $localSolution - LcmSolutionPath = $lcmSolution - ArtifactsDir = $artifactsDir - NestedRootExists = (Test-Path $nestedRoot) - LocalSolutionExists = (Test-Path $localSolution) - LcmSolutionExists = (Test-Path $lcmSolution) - ArtifactsReady = (Test-Path $buildTasksPath) - BuildTasksPath = $buildTasksPath - } -} - -function Resolve-LcmMode { - param( - [Parameter(Mandatory = $true)][ValidateSet('Auto', 'Package', 'Local')][string]$RequestedMode, - [Parameter(Mandatory = $true)][string]$ProjectArgument + [switch]$UseLocalPalaso, + [switch]$UseLocalLcm, + [switch]$UseLocalChorus ) - if ($RequestedMode -eq 'Local') { - return 'Local' + $selectedDependencies = @() + if ($UseLocalPalaso) { + $selectedDependencies += 'Palaso' } - - if ($RequestedMode -eq 'Package') { - return 'Package' + if ($UseLocalLcm) { + $selectedDependencies += 'Lcm' } - - if ([System.IO.Path]::GetFileName($ProjectArgument) -ieq 'FieldWorks.LocalLcm.sln') { - return 'Local' + if ($UseLocalChorus) { + $selectedDependencies += 'Chorus' } - return 'Package' + return $selectedDependencies } try { @@ -430,6 +397,18 @@ try { throw "Project path '$Project' was not found. Pass a path relative to the repo root or an absolute path." } + if (-not (Test-Path $repoNuGetPackages)) { + New-Item -Path $repoNuGetPackages -ItemType Directory -Force | Out-Null + } + + if ($previousNuGetPackages -ne $repoNuGetPackages) { + if (-not [string]::IsNullOrWhiteSpace($previousNuGetPackages)) { + Write-Host "Overriding NUGET_PACKAGES for this build: $previousNuGetPackages -> $repoNuGetPackages" -ForegroundColor Yellow + } + $didOverrideNuGetPackages = $true + } + $env:NUGET_PACKAGES = $repoNuGetPackages + # Clean stale per-project obj/ folders Remove-StaleObjFolders -RepoRoot $PSScriptRoot @@ -509,35 +488,25 @@ try { $finalMsBuildArgs += $MsBuildArgs $installerMsBuildArgs += $MsBuildArgs - $localLcmState = Get-LocalLcmState -RepoRoot $PSScriptRoot -ConfigurationName $Configuration - $resolvedLcmMode = Resolve-LcmMode -RequestedMode $LcmMode -ProjectArgument $Project - $useLocalLcmSource = ($resolvedLcmMode -eq 'Local') - $restoreSolution = if ($useLocalLcmSource) { $localLcmState.LocalSolutionPath } else { Join-Path $PSScriptRoot 'FieldWorks.sln' } + $selectedLocalDependencies = Get-SelectedLocalDependencies -UseLocalPalaso:$LocalPalaso -UseLocalLcm:$LocalLcm -UseLocalChorus:$LocalChorus + $restoreSolution = Join-Path $PSScriptRoot 'FieldWorks.sln' - Write-Host "LCM mode: $resolvedLcmMode (requested: $LcmMode)" -ForegroundColor Cyan - if ($ManagedDebugType) { - Write-Host "Managed debug symbols: $ManagedDebugType" -ForegroundColor Cyan - } - Write-Host "Local LCM checkout: $(if ($localLcmState.LcmSolutionExists) { 'ready' } elseif ($localLcmState.NestedRootExists) { 'partial' } else { 'missing' }) at $($localLcmState.NestedRoot)" -ForegroundColor Cyan - Write-Host "Local LCM artifacts: $(if ($localLcmState.ArtifactsReady) { 'ready' } else { 'missing' }) at $($localLcmState.ArtifactsDir)" -ForegroundColor Cyan - if ($LcmMode -eq 'Auto' -and -not $useLocalLcmSource -and $localLcmState.NestedRootExists) { - Write-Host "Auto mode kept the package-backed path. Use -LcmMode Local to build against Localizations/LCM." -ForegroundColor Yellow + $localPackScript = Join-Path $PSScriptRoot 'Build\Agent\Pack-LocalDependencies.ps1' + & $localPackScript -Configuration $Configuration -LocalPalaso:$LocalPalaso -LocalLcm:$LocalLcm -LocalChorus:$LocalChorus -LocalPackageVersion $LocalPackageVersion + if ($LASTEXITCODE -ne 0) { + throw 'Packing local dependency packages failed.' } - if ($useLocalLcmSource) { - if (-not $localLcmState.LocalSolutionExists) { - throw "Local LCM mode requested but FieldWorks.LocalLcm.sln was not found at $($localLcmState.LocalSolutionPath)." - } - if (-not $localLcmState.LcmSolutionExists) { - throw "Local LCM mode requested but the nested liblcm checkout was not found at $($localLcmState.LcmSolutionPath)." - } - if (-not $localLcmState.ArtifactsReady) { - Write-Host "Local LCM build tasks are missing from $($localLcmState.ArtifactsDir). The build will bootstrap them from source." -ForegroundColor Yellow - } + if ($selectedLocalDependencies.Count -gt 0) { + Write-Host "Local dependency packages: $($selectedLocalDependencies -join ', ') ($LocalPackageVersion)" -ForegroundColor Cyan + } + else { + Write-Host 'Dependency packages: pinned versions from Build/SilVersions.props' -ForegroundColor Cyan } - $finalMsBuildArgs += "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" - $installerMsBuildArgs += "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" + if ($ManagedDebugType) { + Write-Host "Managed debug symbols: $ManagedDebugType" -ForegroundColor Cyan + } # ============================================================================= # Build Execution @@ -579,7 +548,7 @@ try { if (-not (Test-Path $packagesDir)) { New-Item -Path $packagesDir -ItemType Directory -Force | Out-Null } - & dotnet restore $restoreSolution /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true "/p:Configuration=$Configuration" "/p:Platform=$platform" "/p:UseLocalLcmSource=$($useLocalLcmSource.ToString().ToLowerInvariant())" --verbosity quiet + & dotnet restore $restoreSolution /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true "/p:Configuration=$Configuration" "/p:Platform=$platform" --verbosity quiet if ($LASTEXITCODE -ne 0) { throw "NuGet package restore failed for $([System.IO.Path]::GetFileName($restoreSolution))" } @@ -588,27 +557,6 @@ try { Write-Host "Skipping package restore (-SkipRestore)" -ForegroundColor Yellow } - # Copy local LCM assemblies if requested - if ($UseLocalLcm) { - Write-Host "" - Write-Host "Applying local LCM assemblies..." -ForegroundColor Cyan - - $lcmCopyScript = Join-Path $PSScriptRoot "scripts\Agent\Copy-LocalLcm.ps1" - $lcmArgs = @{ - Configuration = $Configuration - BuildLcm = $true - SkipConfirm = $true - } - if ($LocalLcmPath) { - $lcmArgs['LcmRoot'] = $LocalLcmPath - } - - & $lcmCopyScript @lcmArgs - if ($LASTEXITCODE -ne 0) { - throw "Failed to copy local LCM assemblies." - } - } - if ($InstallerOnly) { if (-not $BuildInstaller -and -not $BuildPatch) { throw "-InstallerOnly requires -BuildInstaller or -BuildPatch." @@ -663,14 +611,13 @@ try { } $repoStamp = Get-RepoStamp - $relevantDebugPathspecs = Get-DebugRebuildCheckPathspecs -ResolvedLcmMode $resolvedLcmMode + $relevantDebugPathspecs = Get-DebugRebuildCheckPathspecs $relevantDebugStatus = Get-GitStatusForDebugRebuildCheck -Pathspecs $relevantDebugPathspecs $stampObject = [pscustomobject]@{ Configuration = $Configuration Platform = $platform - RequestedLcmMode = $LcmMode - ResolvedLcmMode = $resolvedLcmMode - UseLocalLcmSource = $useLocalLcmSource + LocalDependencies = @($selectedLocalDependencies) + LocalPackageVersion = $(if ($selectedLocalDependencies.Count -gt 0) { $LocalPackageVersion } else { '' }) ManagedDebugType = $(if ($ManagedDebugType) { $ManagedDebugType } else { '' }) GitHead = $repoStamp.GitHead IsDirty = $repoStamp.IsDirty @@ -773,6 +720,15 @@ try { } } finally { + if ($didOverrideNuGetPackages) { + if ([string]::IsNullOrWhiteSpace($previousNuGetPackages)) { + Remove-Item Env:NUGET_PACKAGES -ErrorAction SilentlyContinue + } + else { + $env:NUGET_PACKAGES = $previousNuGetPackages + } + } + # Kill any lingering build processes that might hold file locks Stop-ConflictingProcesses @cleanupArgs } diff --git a/nuget.config b/nuget.config index f74a57fd89..388a17fe29 100644 --- a/nuget.config +++ b/nuget.config @@ -28,12 +28,19 @@ </config> <packageSources> + <add key="fw-local" value="Output/LocalNuGetFeed" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> </packageSources> <packageSourceMapping> + <packageSource key="fw-local"> + <package pattern="SIL.*" /> + </packageSource> <!-- Require all packages to come from nuget.org (security best practice) --> <packageSource key="nuget.org"> + <!-- Local dependency packages use repo-specific versions, but unrelated SIL packages + such as SIL.BuildTasks still need to restore from nuget.org. --> + <package pattern="SIL.*" /> <package pattern="*" /> </packageSource> </packageSourceMapping> diff --git a/test.ps1 b/test.ps1 index 71edca16b4..749a2fc5ca 100644 --- a/test.ps1 +++ b/test.ps1 @@ -26,6 +26,15 @@ Test output verbosity: q[uiet], m[inimal], n[ormal], d[etailed]. Default is 'normal'. +.PARAMETER LocalPalaso + If set, packs the local libpalaso checkout referenced by FW_LOCAL_PALASO before building tests. + +.PARAMETER LocalLcm + If set, packs the local liblcm checkout referenced by FW_LOCAL_LCM before building tests. + +.PARAMETER LocalChorus + If set, packs the local chorus checkout referenced by FW_LOCAL_CHORUS before building tests. + .EXAMPLE .\test.ps1 Runs all tests in Debug configuration (builds first if needed). @@ -54,6 +63,10 @@ param( [switch]$ListTests, [ValidateSet('quiet', 'minimal', 'normal', 'detailed', 'q', 'm', 'n', 'd')] [string]$Verbosity = "normal", + [switch]$LocalPalaso, + [switch]$LocalLcm, + [switch]$LocalChorus, + [string]$LocalPackageVersion = '99.0.0-local', [switch]$Native, [switch]$SkipDependencyCheck ) @@ -84,6 +97,22 @@ $cleanupArgs = @{ $testExitCode = 0 +function Remove-StaleVersificationTestFiles { + param( + [Parameter(Mandatory = $true)] + [string[]]$Directories + ) + + foreach ($directory in ($Directories | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) { + foreach ($fileName in @('eng.vrs', 'lxx.vrs', 'org.vrs')) { + $filePath = Join-Path $directory $fileName + if (Test-Path -LiteralPath $filePath -PathType Leaf) { + Remove-Item -LiteralPath $filePath -Force + } + } + } +} + try { Invoke-WithFileLockRetry -Context "FieldWorks test run" -IncludeOmniSharp -Action { # Initialize VS environment @@ -201,7 +230,7 @@ try { } else { Write-Host "Building before running tests..." -ForegroundColor Cyan - & "$PSScriptRoot\build.ps1" -Configuration $Configuration -BuildTests + & "$PSScriptRoot\build.ps1" -Configuration $Configuration -BuildTests -LocalPalaso:$LocalPalaso -LocalLcm:$LocalLcm -LocalChorus:$LocalChorus -LocalPackageVersion $LocalPackageVersion if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] Build failed. Fix build errors before running tests." -ForegroundColor Red $script:testExitCode = $LASTEXITCODE @@ -305,6 +334,9 @@ try { return } + $testAssemblyDirectories = @($testDlls | ForEach-Object { Split-Path $_ -Parent }) + Remove-StaleVersificationTestFiles -Directories $testAssemblyDirectories + if (-not $testDlls -or $testDlls.Count -eq 0) { Write-Host "[ERROR] No test assemblies found in $outputDir" -ForegroundColor Red Write-Host " Run with -BuildTests first: .\build.ps1 -BuildTests" -ForegroundColor Yellow From fb1a9aec68c01dd6c83baea03a6671544f808fd4 Mon Sep 17 00:00:00 2001 From: John Lambert <john_lambert@sil.org> Date: Tue, 24 Mar 2026 20:43:01 -0400 Subject: [PATCH 3/3] Improve VS Code local package debugging --- .vscode/launch.json | 70 ++++--- .vscode/tasks.json | 4 +- Build/Agent/Invoke-VsCodeDebugBuild.ps1 | 252 +++++++++++++++++++++++- Build/Agent/Pack-LocalDependencies.ps1 | 6 +- Docs/architecture/liblcm-debugging.md | 59 ++++-- build.ps1 | 30 +++ 6 files changed, 376 insertions(+), 45 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d1a01c3827..aee4062b3a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,31 @@ "symbolOptions": { "searchPaths": [ "${workspaceFolder}\\Output\\Debug" - ] + ], + "searchNuGetOrgSymbolServer": true + } + }, + { + "name": "FieldWorks (.NET Framework, Package, Diagnostics)", + "type": "clr", + "request": "launch", + "preLaunchTask": "Prepare Debug (Package)", + "program": "${workspaceFolder}\\Output\\Debug\\FieldWorks.exe", + "cwd": "${workspaceFolder}\\Output\\Debug", + "console": "externalTerminal", + "justMyCode": false, + "requireExactSource": false, + "suppressJITOptimizations": true, + "logging": { + "moduleLoad": true, + "exceptions": true, + "programOutput": true + }, + "symbolOptions": { + "searchPaths": [ + "${workspaceFolder}\\Output\\Debug" + ], + "searchNuGetOrgSymbolServer": true } }, { @@ -30,35 +54,31 @@ "symbolOptions": { "searchPaths": [ "${workspaceFolder}\\Output\\Debug" - ] + ], + "searchNuGetOrgSymbolServer": true } }, { - "name": "Debug NUnit Tests", + "name": "FieldWorks (.NET Framework, Local Packages, Diagnostics)", "type": "clr", "request": "launch", - "preLaunchTask": "Build", - "program": "dotnet", - "args": [ - "test", - "${workspaceFolder}/${input:testProject}", - "--no-build", - "--settings", - "${workspaceFolder}/Test.runsettings", - "-c", - "Debug" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "justMyCode": false - } - ], - "inputs": [ - { - "id": "testProject", - "type": "promptString", - "description": "Path to test project (e.g., Src/CacheLight/CacheLightTests/CacheLightTests.csproj)", - "default": "Src/CacheLight/CacheLightTests/CacheLightTests.csproj" + "preLaunchTask": "Prepare Debug (Local Packages)", + "program": "${workspaceFolder}\\Output\\Debug\\FieldWorks.exe", + "cwd": "${workspaceFolder}\\Output\\Debug", + "console": "externalTerminal", + "justMyCode": false, + "requireExactSource": false, + "logging": { + "moduleLoad": true, + "exceptions": true, + "programOutput": true + }, + "symbolOptions": { + "searchPaths": [ + "${workspaceFolder}\\Output\\Debug" + ], + "searchNuGetOrgSymbolServer": true + } } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 78f0d4c969..e8eb6c6f8a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -298,9 +298,9 @@ { "label": "Prepare Debug (Local Packages)", "type": "shell", - "command": "./build.ps1 -LocalPalaso -LocalLcm -LocalChorus -ManagedDebugType portable", + "command": "./Build/Agent/Invoke-VsCodeDebugBuild.ps1 -LocalPalaso -LocalLcm -LocalChorus -ManagedDebugType portable", "group": "build", - "detail": "Pack the local dependency repos and build FieldWorks for VS Code debugging", + "detail": "Build for VS Code debugging only when relevant files or local dependency repo states changed since the last successful local-package portable-PDB debug build", "options": { "shell": { "executable": "powershell.exe", diff --git a/Build/Agent/Invoke-VsCodeDebugBuild.ps1 b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 index dafb13aeaf..78c5be1062 100644 --- a/Build/Agent/Invoke-VsCodeDebugBuild.ps1 +++ b/Build/Agent/Invoke-VsCodeDebugBuild.ps1 @@ -2,6 +2,10 @@ param( [ValidateSet('Debug', 'Release')] [string]$Configuration = 'Debug', + [switch]$LocalPalaso, + [switch]$LocalLcm, + [switch]$LocalChorus, + [string]$LocalPackageVersion = '99.0.0-local', [ValidateSet('full', 'portable', 'pdbonly', 'embedded')] [string]$ManagedDebugType = 'portable' ) @@ -18,6 +22,7 @@ function Get-DebugRebuildCheckPathspecs { 'build.ps1', 'test.ps1', 'nuget.config', + '.vscode', 'Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', @@ -33,6 +38,220 @@ function Get-DebugRebuildCheckPathspecs { return $pathspecs | ForEach-Object { $_ -replace '\\', '/' } } +function Get-SelectedLocalDependencies { + $selectedDependencies = @() + if ($LocalPalaso) { + $selectedDependencies += 'Palaso' + } + if ($LocalLcm) { + $selectedDependencies += 'Lcm' + } + if ($LocalChorus) { + $selectedDependencies += 'Chorus' + } + + return $selectedDependencies +} + +function Get-RepoEnvironmentVariableName { + param( + [Parameter(Mandatory = $true)] + [string]$DependencyName + ) + + switch ($DependencyName) { + 'Palaso' { return 'FW_LOCAL_PALASO' } + 'Lcm' { return 'FW_LOCAL_LCM' } + 'Chorus' { return 'FW_LOCAL_CHORUS' } + default { throw "Unknown local dependency '$DependencyName'." } + } +} + +function Get-StringHash { + param( + [AllowEmptyString()] + [string]$Value + ) + + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value) + $hashBytes = $sha256.ComputeHash($bytes) + return ([System.BitConverter]::ToString($hashBytes).Replace('-', '').ToLowerInvariant()) + } + finally { + $sha256.Dispose() + } +} + +function Get-DependencyRepoFingerprint { + param( + [Parameter(Mandatory = $true)] + [string]$RepoPath + ) + + $resolvedRepoPath = [System.IO.Path]::GetFullPath($RepoPath) + $gitHeadOutput = @(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath rev-parse HEAD 2>$null) + if ($LASTEXITCODE -ne 0 -or $gitHeadOutput.Count -eq 0) { + throw "Could not determine git HEAD for local dependency repo '$resolvedRepoPath'." + } + + $gitHead = ($gitHeadOutput -join "`n").Trim() + $statusLines = @(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath status --porcelain=v1 --untracked-files=all 2>$null) + if ($LASTEXITCODE -ne 0) { + throw "Could not determine git status for local dependency repo '$resolvedRepoPath'." + } + + if ($statusLines.Count -eq 0) { + return [pscustomobject]@{ + RepoPath = $resolvedRepoPath + GitHead = $gitHead + IsDirty = $false + Fingerprint = "clean:$gitHead" + } + } + + $diffText = (@(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath diff --no-ext-diff --binary HEAD -- . 2>$null) -join "`n") + if ($LASTEXITCODE -ne 0) { + throw "Could not determine git diff for local dependency repo '$resolvedRepoPath'." + } + + $untrackedFileDescriptors = foreach ($statusLine in $statusLines | Where-Object { $_.StartsWith('?? ') }) { + if ($statusLine.Length -lt 4) { + continue + } + + $relativePath = $statusLine.Substring(3) + $fullPath = Join-Path $resolvedRepoPath $relativePath + if (Test-Path -LiteralPath $fullPath -PathType Leaf) { + $fileInfo = Get-Item -LiteralPath $fullPath + $fileHash = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash.ToLowerInvariant() + "$relativePath|$($fileInfo.Length)|$($fileInfo.LastWriteTimeUtc.Ticks)|$fileHash" + } + else { + "$relativePath|missing" + } + } + + $fingerprintSource = @( + $gitHead, + ($statusLines -join "`n"), + $diffText, + ($untrackedFileDescriptors -join "`n") + ) -join "`n---`n" + + return [pscustomobject]@{ + RepoPath = $resolvedRepoPath + GitHead = $gitHead + IsDirty = $true + Fingerprint = "dirty:$(Get-StringHash -Value $fingerprintSource)" + } +} + +function Test-LocalDependencyStateMatches { + param( + [Parameter(Mandatory = $true)] + [psobject]$Stamp, + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + if (-not ($Stamp.PSObject.Properties.Name -contains 'LocalDependencyStates')) { + Write-Host "Build stamp is missing local dependency fingerprint metadata. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + + $stampStates = @($Stamp.LocalDependencyStates) + if ($stampStates.Count -ne $Dependencies.Count) { + Write-Host "Build stamp local dependency state count does not match the requested debug mode. Rebuilding..." -ForegroundColor Yellow + return $false + } + + $statesByDependency = @{} + foreach ($state in $stampStates) { + $statesByDependency[[string]$state.DependencyName] = $state + } + + foreach ($dependency in $Dependencies) { + if (-not $statesByDependency.ContainsKey($dependency)) { + Write-Host "Build stamp is missing local dependency state for $dependency. Rebuilding..." -ForegroundColor Yellow + return $false + } + + $envVarName = Get-RepoEnvironmentVariableName -DependencyName $dependency + $repoPath = [Environment]::GetEnvironmentVariable($envVarName) + if ([string]::IsNullOrWhiteSpace($repoPath) -or -not (Test-Path -LiteralPath $repoPath -PathType Container)) { + Write-Host "$envVarName is not set to a valid local repo path. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + + $currentFingerprint = Get-DependencyRepoFingerprint -RepoPath $repoPath + $expectedState = $statesByDependency[$dependency] + if ([System.IO.Path]::GetFullPath($repoPath) -ne [string]$expectedState.RepoPath -or $currentFingerprint.Fingerprint -ne [string]$expectedState.Fingerprint) { + Write-Host "Detected local dependency repo changes for $dependency since the last successful $Configuration debug build. Rebuilding..." -ForegroundColor Yellow + return $false + } + } + + return $true +} + +function Test-BuildOutputsMatchStamp { + param( + [Parameter(Mandatory = $true)] + [psobject]$Stamp, + [Parameter(Mandatory = $true)] + [string]$OutputDirectory, + [Parameter(Mandatory = $true)] + [string]$RuntimeExe, + [Parameter(Mandatory = $true)] + [string]$ManagedDebugTypeValue + ) + + if (-not ($Stamp.PSObject.Properties.Name -contains 'TimestampUtc') -or [string]::IsNullOrWhiteSpace($Stamp.TimestampUtc)) { + Write-Host "Build stamp is missing its completion timestamp. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + + if (-not (Test-Path -LiteralPath $RuntimeExe -PathType Leaf)) { + Write-Host "FieldWorks.exe is missing from the debug output. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + + if ($Stamp.TimestampUtc -is [DateTime]) { + $stampTimestamp = ([DateTime]$Stamp.TimestampUtc).ToUniversalTime() + } + else { + $stampTimestampOffset = [DateTimeOffset]::MinValue + if (-not [DateTimeOffset]::TryParse([string]$Stamp.TimestampUtc, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind, [ref]$stampTimestampOffset)) { + Write-Host "Build stamp timestamp could not be parsed. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + $stampTimestamp = $stampTimestampOffset.UtcDateTime + } + + $expectedPdbPath = [System.IO.Path]::ChangeExtension($RuntimeExe, '.pdb') + if ($ManagedDebugTypeValue -eq 'portable' -and -not (Test-Path -LiteralPath $expectedPdbPath -PathType Leaf)) { + Write-Host "Portable debug launch expects $(Split-Path $expectedPdbPath -Leaf) next to FieldWorks.exe. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + + $trackedOutputPaths = @($RuntimeExe) + if (Test-Path -LiteralPath $expectedPdbPath -PathType Leaf) { + $trackedOutputPaths += $expectedPdbPath + } + + foreach ($trackedOutputPath in $trackedOutputPaths) { + $fileInfo = Get-Item -LiteralPath $trackedOutputPath + if ($fileInfo.LastWriteTimeUtc -gt $stampTimestamp) { + Write-Host "Detected newer launch outputs than the last stamped VS Code debug build. Rebuilding before launch..." -ForegroundColor Yellow + return $false + } + } + + return $true +} + function Invoke-Git { param( [Parameter(Mandatory = $true)][string[]]$Arguments @@ -84,7 +303,8 @@ function Test-GitStateRequiresDebugRebuild { return $true } - $currentHead = (Invoke-Git -Arguments @('rev-parse', 'HEAD'))[0] + $currentHeadOutput = @(Invoke-Git -Arguments @('rev-parse', 'HEAD')) + $currentHead = [string]($currentHeadOutput -join '') if ($currentHead -ne $Stamp.GitHead) { $committedChanges = Invoke-Git -Arguments (@('diff', '--name-only', "$($Stamp.GitHead)..$currentHead", '--') + $Pathspecs) if ($committedChanges.Count -gt 0) { @@ -116,12 +336,27 @@ function Invoke-DebugBuild { $ManagedDebugType ) + if ($LocalPalaso) { + $buildArgs += '-LocalPalaso' + } + if ($LocalLcm) { + $buildArgs += '-LocalLcm' + } + if ($LocalChorus) { + $buildArgs += '-LocalChorus' + } + if ($LocalPalaso -or $LocalLcm -or $LocalChorus) { + $buildArgs += @('-LocalPackageVersion', $LocalPackageVersion) + } + & powershell.exe @buildArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } +$selectedLocalDependencies = Get-SelectedLocalDependencies + if (-not (Test-Path $stampPath) -or -not (Test-Path $runtimeExePath)) { Write-Host "No successful $Configuration debug build stamp found. Building before launch..." -ForegroundColor Yellow Invoke-DebugBuild @@ -129,15 +364,26 @@ if (-not (Test-Path $stampPath) -or -not (Test-Path $runtimeExePath)) { } $stamp = Get-Content -LiteralPath $stampPath -Raw | ConvertFrom-Json -$localDependencyMatches = ($stamp.PSObject.Properties.Name -contains 'LocalDependencies') -and (@($stamp.LocalDependencies).Count -eq 0) +$localDependencyMatches = ($stamp.PSObject.Properties.Name -contains 'LocalDependencies') -and ((@($stamp.LocalDependencies) -join "`n") -eq ($selectedLocalDependencies -join "`n")) +$localPackageVersionMatches = ($stamp.PSObject.Properties.Name -contains 'LocalPackageVersion') -and ([string]$stamp.LocalPackageVersion -eq $(if ($selectedLocalDependencies.Count -gt 0) { $LocalPackageVersion } else { '' })) $debugTypeMatches = ($stamp.PSObject.Properties.Name -contains 'ManagedDebugType') -and ($stamp.ManagedDebugType -eq $ManagedDebugType) -if (-not $localDependencyMatches -or -not $debugTypeMatches) { +if (-not $localDependencyMatches -or -not $localPackageVersionMatches -or -not $debugTypeMatches) { Write-Host "Build stamp mode does not match requested VS Code debug mode. Rebuilding..." -ForegroundColor Yellow Invoke-DebugBuild exit 0 } +if (-not (Test-BuildOutputsMatchStamp -Stamp $stamp -OutputDirectory $outputDir -RuntimeExe $runtimeExePath -ManagedDebugTypeValue $ManagedDebugType)) { + Invoke-DebugBuild + exit 0 +} + +if ($selectedLocalDependencies.Count -gt 0 -and -not (Test-LocalDependencyStateMatches -Stamp $stamp -Dependencies $selectedLocalDependencies)) { + Invoke-DebugBuild + exit 0 +} + $pathspecsToCheck = Get-DebugRebuildCheckPathspecs if (Test-GitStateRequiresDebugRebuild -Stamp $stamp -Pathspecs $pathspecsToCheck) { Invoke-DebugBuild diff --git a/Build/Agent/Pack-LocalDependencies.ps1 b/Build/Agent/Pack-LocalDependencies.ps1 index 56f6658fbf..a0534ee2a2 100644 --- a/Build/Agent/Pack-LocalDependencies.ps1 +++ b/Build/Agent/Pack-LocalDependencies.ps1 @@ -168,13 +168,13 @@ function Get-DependencyRepoFingerprint { ) $resolvedRepoPath = [System.IO.Path]::GetFullPath($RepoPath) - $gitHeadOutput = @(& git -C $resolvedRepoPath rev-parse HEAD 2>$null) + $gitHeadOutput = @(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath rev-parse HEAD 2>$null) if ($LASTEXITCODE -ne 0 -or $gitHeadOutput.Count -eq 0) { throw "Could not determine git HEAD for local dependency repo '$resolvedRepoPath'." } $gitHead = ($gitHeadOutput -join "`n").Trim() - $statusLines = @(& git -C $resolvedRepoPath status --porcelain=v1 --untracked-files=all 2>$null) + $statusLines = @(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath status --porcelain=v1 --untracked-files=all 2>$null) if ($LASTEXITCODE -ne 0) { throw "Could not determine git status for local dependency repo '$resolvedRepoPath'." } @@ -188,7 +188,7 @@ function Get-DependencyRepoFingerprint { } } - $diffText = (@(& git -C $resolvedRepoPath diff --no-ext-diff --binary HEAD -- . 2>$null) -join "`n") + $diffText = (@(& git -c core.safecrlf=false -c core.autocrlf=false -C $resolvedRepoPath diff --no-ext-diff --binary HEAD -- . 2>$null) -join "`n") if ($LASTEXITCODE -ne 0) { throw "Could not determine git diff for local dependency repo '$resolvedRepoPath'." } diff --git a/Docs/architecture/liblcm-debugging.md b/Docs/architecture/liblcm-debugging.md index 000ff2e3e6..7bf8af4533 100644 --- a/Docs/architecture/liblcm-debugging.md +++ b/Docs/architecture/liblcm-debugging.md @@ -30,6 +30,12 @@ For local debugging, the supported workflow keeps FieldWorks package-backed. `bu This is the default workflow for real debugging. +Visual Studio and VS Code intentionally share build artifacts, but they do not share one debugger configuration model. In practice that means: + +- use the same `build.ps1` entrypoints from either editor +- expect Visual Studio to own mixed managed/native and test-debugging scenarios +- expect VS Code to own the lightweight managed-only path through `.vscode/launch.json` + ### Package-based debugging Use this when you want to investigate the currently pinned package version without changing the dependency source. @@ -37,17 +43,21 @@ Use this when you want to investigate the currently pinned package version witho 1. Build FieldWorks with `./build.ps1`. 2. Open FieldWorks in Visual Studio 2022. 3. Start the FieldWorks host process under the debugger, or attach to the running process. -4. In Visual Studio debugger options: - Enable Source Link support. -5. In Visual Studio debugger options: - Disable Just My Code for sessions where you need to step into package code. -6. In Visual Studio symbol settings: - Enable the NuGet.org symbol server if the package publishes symbols there. +4. In Visual Studio debugger options, enable Source Link support. +5. In Visual Studio debugger options, disable Just My Code for sessions where you need to step into package code. +6. In Visual Studio symbol settings, leave symbol loading on the recommended automatic mode, then enable the NuGet.org symbol server if the package publishes symbols there. 7. Use the Modules window to verify: the exact `SIL.LCModel*.dll` path loaded, whether symbols were found, and whether the PDB matches the loaded binary. +Visual Studio checklist: + +- Debugger type: managed for package-only inspection, mixed-mode when native boundaries matter. +- Symbols: automatic loading, NuGet.org symbol server on when useful, local symbol paths only when needed. +- Source: Source Link enabled. +- Stepping: Just My Code off for dependency stepping. + Use this path when the issue reproduces against the pinned package and you do not need to modify `liblcm` itself. ### Local package debugging with a local `liblcm` checkout @@ -58,14 +68,19 @@ Prerequisites: - `FW_LOCAL_LCM` points to your `liblcm` checkout. - Build output is Debug/x64. +- The built-in VS Code `Local Packages` launchers are the full local-stack shortcut and currently assume `FW_LOCAL_PALASO`, `FW_LOCAL_LCM`, and `FW_LOCAL_CHORUS` are all set. If you only want a local `liblcm` checkout, run `./build.ps1 -LocalLcm` yourself and prefer Visual Studio for the debug session. Steps: 1. Run `./build.ps1 -LocalLcm`. -2. Open `FieldWorks.sln` in Visual Studio 2022, or use the `FieldWorks (.NET Framework, Local Packages)` launcher in VS Code. +2. Open `FieldWorks.sln` in Visual Studio 2022, or use the `FieldWorks (.NET Framework, Local Packages)` launcher in VS Code when all three local dependency repos are configured. 3. Start debugging FieldWorks. 4. If breakpoints do not bind, check the Modules window before changing any debugger settings. +Visual Studio mixed-mode note: + +- If the investigation crosses into native FieldWorks code, enable native code debugging for the startup project and stay in Visual Studio for the session. + Why this works: - FieldWorks still restores `SIL.LCModel*` packages, but they were packed from your local checkout immediately before the build. @@ -102,16 +117,35 @@ Do not treat VS Code as the primary workflow for: 4. Stay x64 only. 5. Use the VS Code launchers in this repo, which prebuild managed projects with portable PDBs and keep package mode and local-package mode explicit. 6. Ensure the local dependency build completed successfully so the feed under `Output/LocalNuGetFeed` contains the expected `SIL.LCModel*` packages. +7. Prefer the diagnostics launchers first when symbols do not bind; they log module loads and make symbol resolution problems easier to see. + +VS Code checklist: + +- Launch type: `clr` +- Platform: x64 only +- Symbols: portable PDBs, `justMyCode: false`, symbols searched next to the built outputs under `Output/Debug`, with NuGet.org available when package symbols exist +- Editor integration: classic C# extension preferred, not C# Dev Kit ### VS Code launch workflow 1. Build FieldWorks with `./build.ps1`. 2. Choose `FieldWorks (.NET Framework, Package)` when you want the pinned package path. -3. Choose `FieldWorks (.NET Framework, Local Packages)` after building with `./build.ps1 -LocalLcm` when you want the locally packed `liblcm` path. -4. Keep `justMyCode` disabled when stepping into `liblcm`. -5. The VS Code launchers first run `Prepare Debug (*)`, which checks the last successful debug-build stamp and skips the build when no relevant saved files changed. -6. Do not switch the VS Code debug path to Windows PDBs. The debugger used here requires portable PDBs. -7. If symbols still do not bind, inspect the loaded binaries and symbol paths before changing code. +3. Choose `FieldWorks (.NET Framework, Local Packages)` only when `FW_LOCAL_PALASO`, `FW_LOCAL_LCM`, and `FW_LOCAL_CHORUS` are all configured, because that launcher runs the repo's full local-package shortcut. If you only need local `liblcm`, run `./build.ps1 -LocalLcm` manually and use Visual Studio. +4. Use the `Diagnostics` variants when you need module-load evidence or symbol troubleshooting. +5. Keep `justMyCode` disabled when stepping into `liblcm`. +6. The VS Code launchers first run `Prepare Debug (*)`, which checks the last successful debug-build stamp and skips the build when no relevant workspace files changed. For local-package mode it also checks whether the selected local dependency repos changed since the last successful debug build. +7. If another tool rewrites the FieldWorks launch binary or its matching PDB later, such as a Visual Studio rebuild in the same worktree, the prelaunch helper treats those newer launch outputs as unstamped and rebuilds before launch. +8. Do not switch the VS Code debug path to Windows PDBs. The debugger used here requires portable PDBs. +9. If symbols still do not bind, inspect the loaded binaries and symbol paths before changing code. + +What VS Code does not try to share with Visual Studio: + +- startup project selection +- mixed-mode/native debug flags +- test debugging integration +- symbol cache and debugger option state + +Those remain editor-specific by design. Important boundary: @@ -121,6 +155,7 @@ Important boundary: Practical limit: - This path is best effort only. If the session turns into mixed managed/native debugging, move to Visual Studio. +- Do not use the old `dotnet test`-style VS Code launcher pattern for this repo's .NET Framework tests. Use `test.ps1` for normal test runs and Visual Studio when you need interactive test debugging. ## NuGet package versus local packages diff --git a/build.ps1 b/build.ps1 index 84d4448d8b..94c0271efc 100644 --- a/build.ps1 +++ b/build.ps1 @@ -305,6 +305,7 @@ function Get-DebugRebuildCheckPathspecs { 'build.ps1', 'test.ps1', 'nuget.config', + '.vscode', 'Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', @@ -364,6 +365,34 @@ function Get-SelectedLocalDependencies { return $selectedDependencies } +function Get-LocalDependencyDebugState { + param( + [Parameter(Mandatory = $true)] + [string[]]$Dependencies + ) + + $states = @() + $stampDir = Join-Path $PSScriptRoot 'Output\LocalNuGetFeed\.stamp' + foreach ($dependency in $Dependencies) { + $stampPath = Join-Path $stampDir ("{0}.json" -f $dependency) + if (-not (Test-Path -LiteralPath $stampPath -PathType Leaf)) { + throw "Local dependency stamp '$stampPath' was not found after packing $dependency." + } + + $stamp = Get-Content -LiteralPath $stampPath -Raw | ConvertFrom-Json + $states += [pscustomobject]@{ + DependencyName = [string]$stamp.DependencyName + RepoPath = [string]$stamp.RepoPath + PackageVersion = [string]$stamp.PackageVersion + GitHead = [string]$stamp.GitHead + IsDirty = [bool]$stamp.IsDirty + Fingerprint = [string]$stamp.Fingerprint + } + } + + return $states +} + try { Invoke-WithFileLockRetry -Context "FieldWorks build" -IncludeOmniSharp -Action { # Initialize Visual Studio Developer environment @@ -618,6 +647,7 @@ try { Platform = $platform LocalDependencies = @($selectedLocalDependencies) LocalPackageVersion = $(if ($selectedLocalDependencies.Count -gt 0) { $LocalPackageVersion } else { '' }) + LocalDependencyStates = $(if ($selectedLocalDependencies.Count -gt 0) { @(Get-LocalDependencyDebugState -Dependencies $selectedLocalDependencies) } else { @() }) ManagedDebugType = $(if ($ManagedDebugType) { $ManagedDebugType } else { '' }) GitHead = $repoStamp.GitHead IsDirty = $repoStamp.IsDirty