diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8482fb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,373 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# ANTLR generated +.antlr/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +/.vscode +launchSettings.json +Resources/Antlr4/ + +# for OSS version of dbMango: +Resources/ +nuget.config diff --git a/APACHE-2.0.md b/APACHE-2.0.md new file mode 100644 index 0000000..3734edf --- /dev/null +++ b/APACHE-2.0.md @@ -0,0 +1,203 @@ +> Included copy of Apache-2.0 license. This is NOT a license of `dbMango`! + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt new file mode 100644 index 0000000..d84a1c8 --- /dev/null +++ b/COPYRIGHT.txt @@ -0,0 +1,16 @@ +/* dbMango + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..77f4f54 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,38 @@ + + + net9.0 + 1.0.0 + preview + false + 1591,3021,NU3018,BL0007,SYSLIB0050 + $([MSBuild]::GetDirectoryNameOfFileAbove(`$(MSBuildProjectDirectory)`, `Directory.Build.props`))/ + true + true + true + x64 + true + $(SolutionDirectory)bin/obj/$(MSBuildProjectName)/ + + true + true + enable + enable + + BF0D1460-2D1E-40CC-8F10-127369F684FE + + + + $(SolutionDirectory)bin\Debug\ + DEBUG;TRACE + + + + $(SolutionDirectory)bin\Release\ + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..02bc734 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,43 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs new file mode 100644 index 0000000..c51f7ed --- /dev/null +++ b/GlobalAssemblyInfo.cs @@ -0,0 +1,39 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Reflection; + +// This assembly info file is used by all POD assemblies, it contains all common settings +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Deutsche Bank AG")] +[assembly: AssemblyCopyright("Copyright © Deutsche Bank AG 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyProduct("dbMango")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// These values are currently stamped via teamcity +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] +[assembly: AssemblyInformationalVersion("0.0.0.0-commit")] diff --git a/OSS-LICENSES.md b/OSS-LICENSES.md new file mode 100644 index 0000000..e636979 --- /dev/null +++ b/OSS-LICENSES.md @@ -0,0 +1,385 @@ +# Open Source Software Summary + +| Software Name | License Type | +|--------------------------------|--------------------| +| CodeMirror | MIT | +| Open Iconic | MIT | +| Font Awesome | CC BY 4.0, SIL OFL 1.1, MIT | +| ChartJS | MIT | +| ChartJs.Blazor.Fork | MIT | +| ANTLR 4 | BSD-3-Clause | +| Blazor Range Picker | MIT | +| Blazored Modal | MIT | +| Blazorise.Splitter | Apache-2.0 | +| GRPC | Apache-2.0 | +| Log4net | Apache-2.0 | +| Markdig | BSD-2-Clause | +| MongoDB.Driver | Apache-2.0 | +| Moq | BSD-3-Clause | +| Nunit | MIT | +| Newtonsoft.Json | MIT | +| Novell.Directory.Ldap.NETStandard | MIT | +| OpenTelemetry | Apache-2.0 | +| Oracle.ManagedDataAccess.Core | Oracle License Agreement | + +# Open Source Software used + +In no particular order: + +## CodeMirror + +https://codemirror.net/ +https://github.com/codemirror/dev/ + +License: [MIT](https://github.com/codemirror/dev/?tab=License-1-ov-file#readme) + +MIT License + +Copyright (C) 2018 by Marijn Haverbeke , Adrian +Heine , and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Open Iconic + +[Open Iconic v1.1.1](https://github.com/iconic/open-iconic) +License: [MIT](https://github.com/iconic/open-iconic?tab=MIT-1-ov-file) + +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Font Awesome + +Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com + +License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + +## ChartJS + +https://www.chartjs.org/ +https://github.com/chartjs/Chart.js + +License: [MIT](https://github.com/chartjs/Chart.js?tab=MIT-1-ov-file#readme) + +The MIT License (MIT) + +Copyright (c) 2014-2024 Chart.js Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## ChartJs.Blazor.Fork + +https://github.com/mariusmuntean/ChartJs.Blazor +https://www.iheartblazor.com/welcome + +License: [MIT](https://github.com/mariusmuntean/ChartJs.Blazor?tab=MIT-1-ov-file) + +MIT License + +Copyright (c) 2019 Marius Muntean + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## ANTLR 4 + +https://github.com/antlr/antlr4 +License: [BSD-3-Clause](https://github.com/antlr/antlr4?tab=BSD-3-Clause-1-ov-file#readme) + +Copyright (c) 2012-2022 The ANTLR Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither name of copyright holders nor the names of its contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Blazor Range Picker + +https://www.nuget.org/packages/BlazorDateRangePicker + +License: [MIT](https://licenses.nuget.org/MIT) + +## Blazored Modal + +https://github.com/Blazored/Modal + +License: [MIT](https://github.com/Blazored/Modal?tab=MIT-1-ov-file#readme) + +MIT License + +Copyright (c) 2019 Blazored + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Blazorise.Splitter + +https://blazorise.com/docs/extensions/splitter +https://github.com/Megabit/Blazorise + +License: +* https://blazorise.com/files/licences/SLA-2023-07.pdf +* https://opensource.org/license/apache-2-0 + +> If you wish to use the Community Plan/License of the Software, you may +download and access the source and/or binaries at no charge or payment +under the APACHE License (the APACHE) + +## GRPC + +https://github.com/grpc/grpc-dotnet + +License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0) + +Included copy: [APACHE-2.0.md](APACHE-2.0.md) + +## Log4net + +https://logging.apache.org/log4net/index.html + +License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0) + +Included copy: [APACHE-2.0.md](APACHE-2.0.md) + +## Markdig + +https://github.com/xoofx/markdig + +License: [BSD-2-Clause](https://github.com/xoofx/markdig?tab=BSD-2-Clause-1-ov-file#readme) + +Copyright (c) 2018-2019, Alexandre Mutel +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification +, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## MongoDB.Driver + +https://www.mongodb.com/docs/drivers/csharp/current/ + +License: [Apache-2.0](https://licenses.nuget.org/Apache-2.0) + +Included copy: [APACHE-2.0.md](APACHE-2.0.md) + +## Moq + +https://github.com/devlooped/moq + +License: [BSD-3-Clause](https://github.com/devlooped/moq?tab=License-1-ov-file#readme) + +BSD-3-Clause + +Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, +and Contributors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Nunit + +https://nunit.org/ +https://github.com/nunit/nunit + +License: [MIT](https://github.com/nunit/nunit?tab=MIT-1-ov-file#readme) + +Copyright (c) 2024 Charlie Poole, Rob Prouse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Newtonsoft.Json + +https://www.newtonsoft.com/json +https://github.com/JamesNK/Newtonsoft.Json + +License: [MIT](https://github.com/JamesNK/Newtonsoft.Json?tab=MIT-1-ov-file#readme) + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Novell.Directory.Ldap.NETStandard + +https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard + +License: [MIT](https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard?tab=MIT-1-ov-file#readme) + +The MIT License +Copyright (c) 2003 Novell Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## OpenTelemetry + +https://opentelemetry.io/ +https://github.com/open-telemetry/opentelemetry-dotnet + +License: [Apache-2.0](https://github.com/open-telemetry/opentelemetry-dotnet?tab=Apache-2.0-1-ov-file#readme) + +Included copy: [APACHE-2.0.md](APACHE-2.0.md) + +## Oracle.ManagedDataAccess.Core + +https://www.oracle.com/database/technologies/appdev/dotnet.html + +License: [Oracle license agreement](https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core/23.9.1/License) + + + + diff --git a/Rms.Risk.Mango.Interfaces/DatabasesConfig.cs b/Rms.Risk.Mango.Interfaces/DatabasesConfig.cs new file mode 100644 index 0000000..640c783 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/DatabasesConfig.cs @@ -0,0 +1,102 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Represents the configuration for multiple databases. +/// +public class DatabasesConfig +{ + /// + /// Represents the configuration for a single database. + /// + public class DatabaseConfig + { + /// + /// Represents LDAP group configurations for a database. + /// + public class LdapGroups + { + /// + /// Gets or sets the LDAP group for administrators. + /// + public string Admin { get; set; } = ""; + + /// + /// Gets or sets the LDAP group for read-only access. + /// + public string ReadOnly { get; set; } = ""; + + /// + /// Gets or sets the LDAP group for read-write access. + /// + public string ReadWrite { get; set; } = ""; + + /// + /// Creates a deep copy of the current instance. + /// + /// A new instance with the same values. + public LdapGroups Clone() + => new() + { + Admin = Admin, + ReadOnly = ReadOnly, + ReadWrite = ReadWrite + }; + } + + /// + /// Gets or sets the LDAP group configurations. + /// + public LdapGroups Groups { get; set; } = new(); + + /// + /// Gets or sets the MongoDB configuration record. + /// + public MongoDbConfigRecord Config { get; set; } = new(); + + /// + /// Gets or sets the contact information for the database. + /// + public string Contacts { get; set; } = ""; + + /// + /// Creates a deep copy of the current instance. + /// + /// A new instance with the same values. + public DatabaseConfig Clone() + { + var c = new DatabaseConfig + { + Config = Config.Clone(), + Groups = Groups.Clone(), + Contacts = Contacts + }; + return c; + } + } + + /// + /// Gets or sets the dictionary of database configurations, keyed by database name. + /// + // ReSharper disable once CollectionNeverUpdated.Global + public Dictionary Databases { get; set; } = new(); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/DbMangoDatabaseConfigContext.cs b/Rms.Risk.Mango.Interfaces/DbMangoDatabaseConfigContext.cs new file mode 100644 index 0000000..358a609 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/DbMangoDatabaseConfigContext.cs @@ -0,0 +1,95 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Represents the parameters required for database configuration. +/// +public class DatabaseParams +{ + /// + /// Gets or sets the contact information for the database. + /// + public string Contacts { get; set; } = ""; + + /// + /// Gets or sets the MongoDB connection URL. + /// + public string MongoDbUrl { get; set; } = ""; + + /// + /// Gets or sets the MongoDB database name. + /// + public string MongoDbDatabase { get; set; } = ""; + + /// + /// Gets or sets the username for user authentication. + /// + public string UserAuthUser { get; set; } = "admin"; + + /// + /// Gets or sets the password for user authentication. + /// + public string UserAuthPassword { get; set; } = "admin"; + + /// + /// Gets or sets the authentication database for user authentication. + /// + public string? UserAuthAuthDatabase { get; set; } + + /// + /// Gets or sets the authentication method for user authentication. + /// + public string? UserAuthMethod { get; set; } + + /// + /// Gets or sets the username for admin authentication. + /// + public string AdminAuthUser { get; set; } = "admin"; + + /// + /// Gets or sets the password for admin authentication. + /// + public string AdminAuthPassword { get; set; } = "admin"; + + /// + /// Gets or sets the authentication database for admin authentication. + /// + public string? AdminAuthAuthDatabase { get; set; } + + /// + /// Gets or sets the authentication method for admin authentication. + /// + public string? AdminAuthMethod { get; set; } + + /// + /// Gets or sets a value indicating whether to use a direct connection to the database. + /// + public bool DirectConnection { get; set; } + + /// + /// Gets or sets a value indicating whether to use TLS for the connection. + /// + public bool UseTls { get; set; } + + /// + /// Gets or sets a value indicating whether shard access is allowed. + /// + public bool AllowShardAccess { get; set; } +} diff --git a/Rms.Risk.Mango.Interfaces/IAuditService.cs b/Rms.Risk.Mango.Interfaces/IAuditService.cs new file mode 100644 index 0000000..bb46943 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/IAuditService.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Represents an audit record containing details about a database operation. +/// +/// The name of the database where the operation occurred. +/// The timestamp of when the operation was performed. +/// The email of the user who performed the operation. +/// The ticket identifier associated with the operation. +/// Indicates whether the operation was successful. +/// The MongoDB command executed during the operation. +/// Optional error message if the operation failed. +public record AuditRecord( + string DatabaseName, + DateTime Timestamp, + string Email, + string Ticket, + bool Success, + BsonDocument Command, + string? Error = null); + +/// +/// Defines methods for auditing database operations. +/// +public interface IAuditService +{ + /// + /// Performs a pre-check on the provided MongoDB command. + /// + /// The MongoDB command to validate before execution. + void PreCheck(BsonDocument command); + + /// + /// Records an audit entry for a database operation. + /// + /// The audit record containing details of the operation. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task Record(AuditRecord rec, CancellationToken token = default); + + /// + /// Retrieves a list of audit records within a specified date range. + /// + /// The start date of the range to retrieve audit records. + /// The end date of the range to retrieve audit records. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation, containing a list of audit records. + Task> Audit(DateTime startDate, DateTime endDate, CancellationToken token = default); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/IChangeNumberChecker.cs b/Rms.Risk.Mango.Interfaces/IChangeNumberChecker.cs new file mode 100644 index 0000000..08d12a9 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/IChangeNumberChecker.cs @@ -0,0 +1,26 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Interfaces; + +public record CheckerReply(bool IsValid, string? ErrorMessage = null, DateTime ValidFromUtc = default, DateTime ValidToUtc = default); + +public interface IChangeNumberChecker +{ + Task IsValid(string taskNumber, string email, DateTime whenTimeUtc = default); +} diff --git a/Rms.Risk.Mango.Interfaces/IDatabaseConfigurationStorage.cs b/Rms.Risk.Mango.Interfaces/IDatabaseConfigurationStorage.cs new file mode 100644 index 0000000..0111405 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/IDatabaseConfigurationStorage.cs @@ -0,0 +1,138 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Represents the base context for database configurations. +/// +public class ContextBase +{ + /// + /// Gets or sets the type of the context. + /// + public string Type { get; set; } = "dbMangoDatabase"; + + /// + /// Gets or sets the name of the context. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the unique identifier of the context. + /// + public long ID { get; set; } + + /// + /// Gets or sets the unique identifier of the parent context. + /// + public long ParentID { get; set; } + + /// + /// Gets or sets a value indicating whether the context is a template. + /// + public bool IsTemplate { get; set; } + + /// + /// Gets or sets a value indicating whether the context is proposed for deletion. + /// + public bool ProposedForDeletion { get; set; } +} + +/// +/// Represents the database configuration context for a Mango database. +/// +public class DbMangoDatabaseConfigContext : ContextBase +{ + /// + /// Gets or sets the database parameters for the configuration. + /// + public DatabaseParams DatabaseParams { get; set; } = new(); + + /// + /// Gets or sets the LDAP group parameters for the configuration. + /// + public DatabasesConfig.DatabaseConfig.LdapGroups LdapParams { get; set; } = new(); +} + + +/// +/// Interface for managing database configuration storage operations. +/// +public interface IDatabaseConfigurationStorage +{ + /// + /// Reads a database configuration by its identifier. + /// + /// The unique identifier of the database configuration. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the database configuration context. + Task Read(long id, CancellationToken token); + + /// + /// Creates a new database configuration. + /// + /// The database configuration context to create. + /// The email of the user performing the operation. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the unique identifier of the created configuration. + Task Create(DbMangoDatabaseConfigContext ctx, string email, CancellationToken token); + + /// + /// Updates an existing database configuration. + /// + /// The database configuration context to update. + /// The email of the user performing the operation. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task Update(DbMangoDatabaseConfigContext ctx, string email, CancellationToken token); + + /// + /// Deletes a database configuration by its identifier. + /// + /// The unique identifier of the database configuration to delete. + /// The email of the user performing the operation. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task Delete(long id, string email, CancellationToken token); + + /// + /// Lists all database configurations associated with a specific user. + /// + /// The email of the user whose configurations are to be listed. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains a list of database configuration contexts. + Task> List(string email, CancellationToken token); + + /// + /// Retrieves configuration settings for a specific user. + /// + /// The email of the user whose configuration settings are to be retrieved. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains a dictionary of configuration settings. + Task> GetConfiguration(string email, CancellationToken token); + + /// + /// Updates configuration settings for a specific user. + /// + /// The dictionary of configuration settings to update. + /// The email of the user performing the operation. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task UpdateConfiguration(Dictionary configuration, string email, CancellationToken token); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/IDbMangoPlugin.cs b/Rms.Risk.Mango.Interfaces/IDbMangoPlugin.cs new file mode 100644 index 0000000..02e4184 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/IDbMangoPlugin.cs @@ -0,0 +1,41 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.Extensions.Hosting; + +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Defines the contract for a plugin that integrates with dbMango. +/// +public interface IDbMangoPlugin +{ + /// + /// Configures the services required by the plugin. + /// + /// The application builder used to configure services. + /// The updated application builder. + IHostApplicationBuilder ConfigureServices(IHostApplicationBuilder builder); + + /// + /// Creates a secure audit service using the provided Oracle connection settings. + /// + /// The Oracle connection settings used to configure the audit service. + /// An instance of if successful; otherwise, null. + IAuditService? CreateSecureAuditService(OracleConnectionSettings settings); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/IUserSession.cs b/Rms.Risk.Mango.Interfaces/IUserSession.cs new file mode 100644 index 0000000..eaabf0d --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/IUserSession.cs @@ -0,0 +1,145 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Microsoft.AspNetCore.Authorization; +using Rms.Service.Bootstrap.Security; + +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Interface representing a user session, providing access to user-related services, database configurations, +/// and MongoDB services. +/// +public interface IUserSession +{ + /// + /// Gets the user service for the current session. + /// + UserService User { get; } + + /// + /// Gets or sets the task number associated with the session. + /// + string? TaskNumber { get; set; } + + /// + /// Gets the error message, if any, related to task checks. + /// + string? TaskCheckError { get; } + + /// + /// Gets or sets the name of the database being used in the session. + /// + string Database { get; set; } + + /// + /// Gets a value indicating whether database instance selection is allowed. + /// + bool IsDatabaseInstanceSelectionAllowed { get; } + + /// + /// Gets or sets the name of the collection being used in the session. + /// + string Collection { get; set; } + + /// + /// Gets or sets the database instance being used in the session. + /// + string DatabaseInstance { get; set; } + + /// + /// Gets the MongoDB service for interacting with the database. + /// + IMongoDbService MongoDb { get; } + + /// + /// Gets the MongoDB admin service for managing the database. + /// + IMongoDbDatabaseAdminService MongoDbAdmin { get; } + + /// + /// Gets the MongoDB admin service for managing the admin database. + /// + IMongoDbDatabaseAdminService MongoDbAdminForAdminDatabase { get; } + + /// + /// Gets the pivot table data source for the session. + /// + IPivotTableDataSource PivotDataSource { get; } + + /// + /// Gets the audit service for logging and tracking changes. + /// + IAuditService Audit { get; } + + /// + /// Gets the configuration record for the database. + /// + MongoDbConfigRecord DatabaseConfig { get; } + + /// + /// Gets the LDAP groups configuration for the session. + /// + DatabasesConfig.DatabaseConfig.LdapGroups LdapGroups { get; } + + /// + /// Checks if the session has a valid task. + /// + /// A task that resolves to true if the task is valid; otherwise, false. + Task HasValidTask(); + + /// + /// Determines if the user can access a specific resource based on the provided policy and database name. + /// + /// The authorization service. + /// The name of the policy to check. + /// The name of the database to check access for. + /// A task that resolves to true if access is allowed; otherwise, false. + Task CanAccess(IAuthorizationService auth, string policyName, string databaseName); + + /// + /// Determines if instance selection is allowed for the specified database. + /// + /// The name of the database. + /// True if instance selection is allowed; otherwise, false. + bool IsInstanceSelectionAllowed(string database); + + /// + /// Event triggered when the database changes. + /// + event Action? DatabaseChanged; + + /// + /// Gets a custom MongoDB admin service for the specified database and instance. + /// + /// The name of the database. + /// The name of the database instance. + /// The custom MongoDB admin service. + IMongoDbDatabaseAdminService GetCustomAdmin(string databaseName, string databaseInstance); + + /// + /// Gets a shard connection for the specified host and port. + /// + /// The host of the shard. + /// The port of the shard. + /// The MongoDB admin service for the shard connection. + IMongoDbDatabaseAdminService GetShardConnection(string host, int port); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/MenuService.cs b/Rms.Risk.Mango.Interfaces/MenuService.cs new file mode 100644 index 0000000..4c81237 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/MenuService.cs @@ -0,0 +1,54 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Interfaces; + +public record MenuItem +( + string Menu, + string Title, + string Url +); + +/// +/// Provides methods to manage and retrieve menu items and menus. +/// +public interface IMenuService +{ + /// + /// Adds a new menu item to a specified menu. + /// + /// The name of the menu to which the item will be added. + /// The title of the menu item. + /// The URL associated with the menu item. + public void AddMenuItem(string menu, string title, string url); + + /// + /// Retrieves all menu items for a specified menu. + /// + /// The name of the menu whose items are to be retrieved. + /// A list of objects for the specified menu. + public List Get(string menu); + + /// + /// Retrieves a list of all available menu names. + /// + /// A list of menu names. + public List GetMenus(); +} + diff --git a/Rms.Risk.Mango.Interfaces/MongoDbCommandHelper.cs b/Rms.Risk.Mango.Interfaces/MongoDbCommandHelper.cs new file mode 100644 index 0000000..2313a86 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/MongoDbCommandHelper.cs @@ -0,0 +1,87 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Provides helper methods for MongoDB command operations. +/// +public static class MongoDbCommandHelper +{ + /// + /// A set of MongoDB command names that are considered read-only and do not require auditing. + /// + private static readonly HashSet _noAudit = new(StringComparer.InvariantCultureIgnoreCase) + { + "find", + "aggregate", + "listcollections", + "listDatabases", + "hello", + "listIndexes", + "collStats", + "listCommands", + "ping", + "listShards", + "getShardMap", + "serverStatus", + "balancerStatus", + "dbStats", + "buildInfo", + "getShardVersion", + "getLog", + "rolesInfo", + "usersInfo", + "availableQueryOptions", + "analyzeShardKey", + "analyze", + "currentOp", + "connectionStatus", + "replSetGetStatus", + }; + + /// + /// Determines whether the specified MongoDB command is read-only. + /// + /// The MongoDB command represented as a . + /// + /// true if the command is read-only; otherwise, false. + /// + public static bool IsReadOnlyCommand(BsonDocument command) + { + if (command == null || command.IsBsonNull) + return false; + var commandType = command.ElementAt(0).Name; + return IsReadOnlyCommand(commandType); + } + + /// + /// Determines whether the specified MongoDB command name is read-only. + /// + /// The name of the MongoDB command. + /// + /// true if the command name is read-only; otherwise, false. + /// + public static bool IsReadOnlyCommand(string? command) + { + if (!string.IsNullOrWhiteSpace(command) && _noAudit.Contains(command)) return true; + return false; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/OracleConnectionSettings.cs b/Rms.Risk.Mango.Interfaces/OracleConnectionSettings.cs new file mode 100644 index 0000000..3f1b262 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/OracleConnectionSettings.cs @@ -0,0 +1,55 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Interfaces; + +/// +/// Represents the settings required to establish a connection to an Oracle database. +/// +public class OracleConnectionSettings +{ + /// + /// Gets or sets the connection string for the Oracle database. + /// + public string ConnectionString { get; set; } = ""; + + /// + /// Gets or sets the password for the Oracle database connection. + /// + public string Password { get; set; } = ""; + + /// + /// Gets or sets the connection string for the audit database, if applicable. + /// + public string? AuditConnectionString { get; set; } + + /// + /// Gets or sets the password for the audit database connection, if applicable. + /// + public string? AuditPassword { get; set; } + + /// + /// Gets or sets the path to the Oracle wallet, if applicable. + /// + public string? Wallet { get; set; } + + /// + /// Gets or sets the TNS_ADMIN directory path, if applicable. + /// + public string? TnsAdmin { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/README.md b/Rms.Risk.Mango.Interfaces/README.md new file mode 100644 index 0000000..17df2b5 --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/README.md @@ -0,0 +1,23 @@ +### This is a public interface for dbMango plugin. + +## Purpose + +> Implementation is optional! +Upon startup dbMango will look for the plugin and if it is not found, +it will continue without it. + +On startup dbMango will look for assembly called `Rms.Risk.Mango.Db.Plugin.dll`. If it exists, +it will be loaded and the plugin will be initialized. The plugin is the class with the full name of +`Rms.Risk.Mango.Db.Plugin.Plugin`. This name must match the plugin name mask what is hardcoded in dbMango code. + +For more information about creating plugins see [dbMango Plugins](../Rms.Risk.Mango/wwwroot/docs/plugins.md). + +## Functionality + +Currently it only provides interfaces for persistent storage for + +- configuration parameters overrides stored externally (Oracle database) +- persistent storage for onboarding information +- secure storage for audit records + +The only implementation is in Rms.Risk.Mango.Db.Plugin for Deutsche Bank specific implementation. \ No newline at end of file diff --git a/Rms.Risk.Mango.Interfaces/Rms.Risk.Mango.Interfaces.csproj b/Rms.Risk.Mango.Interfaces/Rms.Risk.Mango.Interfaces.csproj new file mode 100644 index 0000000..3401cbb --- /dev/null +++ b/Rms.Risk.Mango.Interfaces/Rms.Risk.Mango.Interfaces.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Rms.Risk.Mango.Language/Ast/AstAggregation.cs b/Rms.Risk.Mango.Language/Ast/AstAggregation.cs new file mode 100644 index 0000000..8d54ee3 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstAggregation.cs @@ -0,0 +1,36 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstAggregation(string _collection) : AstNodeBase +{ + public string Collection => _collection.Trim('\"'); + + public AstPipeline? Pipeline => Children.OfType().FirstOrDefault(); + + public override JsonNode? AsJson() => Pipeline?.AsJson(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"FROM \"{Collection}\" PIPELINE {{"); + Pipeline?.Append(sb, indent + 1); + sb.AppendLine("}"); + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstEquivalence.cs b/Rms.Risk.Mango.Language/Ast/AstEquivalence.cs new file mode 100644 index 0000000..1862278 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstEquivalence.cs @@ -0,0 +1,42 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstEquivalence : AstNodeBase +{ + public AstEquivalence(AstExpressionVariable left, AstExpressionVariable right) + { + Add(left); + Add(right); + } + + public AstExpressionVariable Left => Children.OfType().First(); + public AstExpressionVariable Right => Children.OfType().Last(); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append(Spaces(indent)); + Left.Append(sb, indent + 1); + sb.Append(" == "); + Right.Append(sb, indent + 1); + } + + public override JsonNode? AsJson() + => throw new NotImplementedException(); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpression.cs b/Rms.Risk.Mango.Language/Ast/AstExpression.cs new file mode 100644 index 0000000..e7350ab --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpression.cs @@ -0,0 +1,24 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public abstract class AstExpression : AstNodeBase +{ + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionArray.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionArray.cs new file mode 100644 index 0000000..5a173c6 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionArray.cs @@ -0,0 +1,50 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionArray : AstExpression +{ + public AstExpressionArray(IEnumerable elements) + { + foreach( var v in elements) + Add(v); + } + + public IReadOnlyList Elements => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine("["); + var first = true; + foreach (var v in Elements) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + sb.Append(Spaces(indent + 1)); + v.Append(sb, indent + 1); + } + sb.AppendLine(); + sb.Append($"{Spaces(indent)}]"); + } + + public override JsonNode? AsJson() => new JsonArray([.. Elements.Select(x => x.AsJson())]); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionBool.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionBool.cs new file mode 100644 index 0000000..4f6292e --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionBool.cs @@ -0,0 +1,32 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionBool(bool _value) : AstExpression +{ + public bool Value => _value; + + public override void Append(StringBuilder sb, int indent) + { + sb.Append(Value.ToString().ToLower()); + } + + public override JsonNode? AsJson() => JsonValue.Create(Value); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionBrackets.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionBrackets.cs new file mode 100644 index 0000000..8bc9276 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionBrackets.cs @@ -0,0 +1,42 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionBrackets : AstExpression +{ + public AstExpressionBrackets() {} + public AstExpressionBrackets(AstExpression expression) + { + Add(expression); + } + + public AstExpression Expression => Children.OfType().First(); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append("("); + Expression.Append(sb, indent+1); + sb.Append(")"); + } + + public override JsonNode? AsJson() + { + return Expression.AsJson(); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionExists.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionExists.cs new file mode 100644 index 0000000..94033bf --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionExists.cs @@ -0,0 +1,41 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionExists : AstExpression +{ + public AstExpressionExists() {} + public AstExpressionExists(string fieldName, bool exists) + { + Name = fieldName; + Exists = exists; + } + + public string Name { get; internal set{ field = PreprocessFieldName(value); }} = ""; + public bool Exists { get; internal set;} = false; + + + public override void Append(StringBuilder sb, int indent) + { + AppendField(sb, Name); + sb.Append($" EXISTS"); + } + + public override JsonNode? AsJson() => new JsonObject([new("$exists", Exists)]); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionFunctionCall.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionFunctionCall.cs new file mode 100644 index 0000000..11b243e --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionFunctionCall.cs @@ -0,0 +1,107 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionFunctionCall : AstExpression +{ + public const string WithinFunctionProperty = "within_function"; + + public AstExpressionFunctionCall(string name, IEnumerable namedArgs) + { + SetProperty(WithinFunctionProperty, true); + Name = PreprocessFieldName(name); + foreach (var arg in namedArgs) + Add(arg); + } + + public string Name { get; internal set{ field = PreprocessFieldName(value); } } + public IReadOnlyList UnnamedArgs => Children.OfType().Where(x => string.IsNullOrEmpty(x.Name)).ToList(); + public IReadOnlyList NamedArgs => Children.OfType().Where(x => !string.IsNullOrEmpty(x.Name)).ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append($"{Name}( "); + var namedArgs = NamedArgs; + if ( namedArgs.Count == 0 ) + { + // unnamed args + bool first = true; + foreach (var value in UnnamedArgs) + { + if ( !first ) + sb.Append(", "); + else + first = false; + + value.Append(sb, indent+1); + } + sb.Append(" )"); + } + else + { + // named args + sb.AppendLine(); + bool first = true; + foreach (var value in namedArgs) + { + if ( !first ) + { + sb.AppendLine(", "); + } + else + first = false; + + sb.Append(Spaces(indent + 1)); + value.Append(sb, indent + 2); + } + sb.AppendLine(); + sb.Append($"{Spaces(indent)})"); + } + } + + public override JsonNode? AsJson() + { + var namedArgs = NamedArgs; + var unnamedArgs = UnnamedArgs; + + if (namedArgs.Count == 0) + { + // no need for array if only one parameter + if ( unnamedArgs.Count == 1 ) + return new JsonObject( + [ + new($"${Name}", unnamedArgs[0].AsJson()) + ]); + return new JsonObject( + [ + new( + $"${Name}", + new JsonArray([.. unnamedArgs.Select(x => x.AsJson())])) + ]); + } + + return new JsonObject( + [ + new( + $"${Name}", + new JsonObject([.. namedArgs.Select(x => new KeyValuePair(x.Name, x.Value.AsJson()))]) + ) + ]); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionIn.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionIn.cs new file mode 100644 index 0000000..0370024 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionIn.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionIn : AstExpression +{ + private string _variable; + private bool _not; + + public AstExpressionIn(string variable, bool not, IEnumerable values) + { + _variable = variable; + _not = not; + foreach (var value in values) + Add(value); + } + + public string Variable => _variable; + public bool Not => _not; + public IReadOnlyList Values => [.. Children.OfType()]; + + public override void Append(StringBuilder sb, int indent) + { + sb.Append($"{Variable} IN ( "); + bool first = true; + foreach (var value in Values) + { + if ( !first ) + sb.Append(", "); + else + first = false; + + value.Append(sb, indent); + } + sb.Append($" )"); + } + + public override JsonNode? AsJson() => + new JsonObject( + [ + new( + "$in", + new JsonArray + { + new JsonNode?[]{ JsonValue.Create(Variable) } + .Concat(Values.Select(x => x.AsJson()) + ) + } + ) + ] + ); + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionNull.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionNull.cs new file mode 100644 index 0000000..331b250 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionNull.cs @@ -0,0 +1,29 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionNull : AstExpression +{ + public override void Append(StringBuilder sb, int indent) + { + sb.Append("NULL"); + } + + public override JsonNode? AsJson() => null; //JsonValue.Create("$null"); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionNumber.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionNumber.cs new file mode 100644 index 0000000..a376c43 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionNumber.cs @@ -0,0 +1,67 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionNumber : AstExpression +{ + public AstExpressionNumber(string value) + { + var d = double.Parse(value); + if ( d == Math.Floor(d) ) + { + LongValue = (long)d; + IsLong = true; + } + else + DoubleValue = d; + } + + + public AstExpressionNumber(long value) + { + LongValue = value; + IsLong = true; + } + + public AstExpressionNumber(double value) + { + DoubleValue = value; + } + + public bool IsLong { get;} + + public long LongValue { get; } + public double DoubleValue { get; } + + public override void Append(StringBuilder sb, int indent) + { + if (IsLong) + sb.Append(LongValue); + else + sb.Append(DoubleValue); + } + + public override JsonNode? AsJson() + { + if (IsLong) + return JsonValue.Create(LongValue); + else + return JsonValue.Create(DoubleValue); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs new file mode 100644 index 0000000..705fb04 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs @@ -0,0 +1,290 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics.CodeAnalysis; + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionOperation : AstExpression +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum OperationType + { + AND, + OR, + EQ, + NEQ, + GT, + GTE, + LT, + LTE, + PLUS, + MINUS, + DIVIDE, + MULTIPLY + } + + public static OperationType GetOperationType(string op) => op switch + { + "$and" => OperationType.AND, + "AND" => OperationType.AND, + "$or" => OperationType.OR, + "OR" => OperationType.OR, + "$eq" => OperationType.EQ, + "==" => OperationType.EQ, + "$ne" => OperationType.NEQ, + "<>" => OperationType.NEQ, + "!=" => OperationType.NEQ, + "$gt" => OperationType.GT, + ">" => OperationType.GT, + "$gte" => OperationType.GTE, + ">=" => OperationType.GTE, + "$lt" => OperationType.LT, + "<" => OperationType.LT, + "$lte" => OperationType.LTE, + "<=" => OperationType.LTE, + "$add" => OperationType.PLUS, + "+" => OperationType.PLUS, + "$subtract" => OperationType.MINUS, + "-" => OperationType.MINUS, + "$divide" => OperationType.DIVIDE, + "/" => OperationType.DIVIDE, + "$multiply" => OperationType.MULTIPLY, + "*" => OperationType.MULTIPLY, + _ => throw new($"Invalid operator '{op}'") + }; + + public static string GetOperationStr(OperationType op) => op switch + { + OperationType.AND => "AND", + OperationType.OR => "OR", + OperationType.EQ => "==", + OperationType.NEQ => "!=", + OperationType.GT => ">", + OperationType.GTE => ">=", + OperationType.LT => "<", + OperationType.LTE => "<=", + OperationType.PLUS => "+", + OperationType.MINUS => "-", + OperationType.DIVIDE => "/", + OperationType.MULTIPLY => "*", + _ => throw new($"Invalid operator '{op}'") + }; + + public static string GetOperationCall(OperationType op) => op switch + { + OperationType.AND => "$and", + OperationType.OR => "$or", + OperationType.EQ => "$eq", + OperationType.NEQ => "$ne", + OperationType.GT => "$gt", + OperationType.GTE => "$gte", + OperationType.LT => "$lt", + OperationType.LTE => "$lte", + OperationType.PLUS => "$add", + OperationType.MINUS => "$subtract", + OperationType.DIVIDE => "$divide", + OperationType.MULTIPLY => "$multiply", + _ => throw new($"Invalid operator '{op}'") + }; + + private static HashSet _condition = + [ + OperationType.EQ, + OperationType.NEQ, + OperationType.LT, + OperationType.LTE, + OperationType.GT, + OperationType.GTE + ]; + + private static HashSet _groupable = + [ + OperationType.AND, + OperationType.OR, + OperationType.PLUS, + OperationType.MULTIPLY + ]; + + + public AstExpressionOperation(string op, params IEnumerable args) + : this(GetOperationType(op), args) + { + } + + public AstExpressionOperation(OperationType op, params IEnumerable args) + { + Operator = op; + foreach (var arg in args) + Add(arg); + } + + public OperationType Operator { get; } + public string OperatorStr => GetOperationStr(Operator); + public IReadOnlyList Args => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + var first = true; + foreach (var arg in Args) + { + if (first) + { + first = false; + } + else + { + if (Operator == OperationType.AND || Operator == OperationType.OR) + { + sb.AppendLine(); + sb.Append($"{Spaces(indent)}{OperatorStr} "); + } + else + { + sb.Append($" {OperatorStr} "); + } + } + arg.Append(sb, indent); + } + } + + public override JsonNode? AsJson() + { + var functionArgs = Args; + var withinFunction = GetProperty(AstExpressionFunctionCall.WithinFunctionProperty, false); + // special case - equality operator + if (_condition.Contains(Operator) && !withinFunction) + { + // for root level = use this form: { field : value } + if (Operator == OperationType.EQ + && functionArgs is [ + AstExpressionVariable varArg, + { } condArg and (AstExpressionNumber or AstExpressionString or AstExpressionVariable) + ] + ) + { + var op = new JsonObject( + [ + new( + varArg.Name, + condArg.AsJson() + ) + ]); + + return op; + } + + if (Operator == OperationType.EQ + && functionArgs is [ + { } condArg1 and (AstExpressionNumber or AstExpressionString or AstExpressionVariable), + AstExpressionVariable varArg1 + ] + ) + { + var op = new JsonObject( + [ + new( + varArg1.Name, + condArg1.AsJson() + ) + ]); + + return op; + } + + if (functionArgs is + [ + AstExpressionVariable varArg2, + { } condArg2 + ]) + { + // for other conditions use { field : { $op : value } } + var op1 = new JsonObject( + [ + new( + varArg2.Name, + new JsonObject([ + new( + GetOperationCall(Operator), + condArg2.AsJson() + ) + ]) + ) + ] + ); + return op1; + } + } + + var funcArgs = Args; + + // make one for expressions like a+b+c+d + if (_groupable.Contains(Operator)) + { + var op = CreateFunctionCall(funcArgs); + return op; + } + + // make separate calls for expressions like a-b-c-d + var revArgs = funcArgs.Reverse().ToList(); + var currentFunc = CreateFunctionCall(revArgs[1], revArgs[0]); + for (var i = 2; i < revArgs.Count; i++) + currentFunc = ChainFunctionCall(revArgs[i], currentFunc); + + return currentFunc; + + JsonObject CreateFunctionCall(params IReadOnlyList a) + { + var args = new JsonArray(); + foreach (var v in a.Select(x => x.AsJson())) + { + args.Add(v); + } + + var op = new JsonObject( + [ + new( + GetOperationCall(Operator), + args + ) + ] + ); + return op; + } + + JsonObject ChainFunctionCall(AstExpression arg1, JsonObject arg2) + { + JsonArray args = + [ + arg1.AsJson(), + arg2 + ]; + + var op = new JsonObject( + [ + new( + GetOperationCall(Operator), + args + ) + ] + ); + return op; + } + + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionProjection.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionProjection.cs new file mode 100644 index 0000000..490e48b --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionProjection.cs @@ -0,0 +1,39 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionProjection : AstExpression +{ + public AstExpressionProjection() {} + public AstExpressionProjection(string fieldName, JsonNode json ) + { + Name = fieldName; + Projection = json; + } + + public string Name { get; internal set{ field = PreprocessFieldName(value); }} = ""; + public JsonNode Projection { get; internal set;} = JsonValue.Create(""); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append($"{Name} IS {Projection.ToJsonString()}"); + } + + public override JsonNode? AsJson() => new JsonObject([new(Name, Projection)]); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionString.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionString.cs new file mode 100644 index 0000000..2f10bc4 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionString.cs @@ -0,0 +1,33 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionString(string _value) : AstExpression +{ + public string Value => _value.Trim('\"'); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append($"\"{Value}\""); + } + + public override JsonNode? AsJson() => JsonValue.Create(Value); + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionUnary.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionUnary.cs new file mode 100644 index 0000000..f336b7f --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionUnary.cs @@ -0,0 +1,107 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstExpressionUnary : AstExpression +{ + public enum OperationType + { + NOT, + MINUS, + PLUS + + } + + public AstExpressionUnary(OperationType op, AstExpression arg1) + { + Operator = op; + Add(arg1); + } + + public OperationType Operator { get; } + public string OperatorStr => Operator switch + { + OperationType.NOT => "NOT", + OperationType.MINUS => "-", + OperationType.PLUS => "+", + _ => throw new($"Invalid operator '{Operator}'") + }; + + public AstExpression Arg1 => Children.OfType().First(); + + public override void Append(StringBuilder sb, int indent) + { + switch (Operator) + { + case OperationType.MINUS: + sb.Append("- "); + break; + case OperationType.PLUS: + sb.Append("+ "); + break; + case OperationType.NOT: + sb.Append("NOT "); + break; + } + Arg1.Append(sb, indent); + } + + public override JsonNode? AsJson() + { + switch (Operator) + { + case OperationType.PLUS: + return Arg1.AsJson(); + case OperationType.MINUS: + { + var left = JsonValue.Create(0); + var right = Arg1.AsJson(); + + var op = new JsonObject( + [ + new( + "$subtract", + new JsonArray { new []{left,right} } + ) + ] + ); + + return op; + + } + case OperationType.NOT: + { + var right = Arg1.AsJson(); + var op = new JsonObject( + [ + new( + "$not", + new JsonArray { new []{right} } + ) + ] + ); + + return op; + } + default: + throw new($"Invalid operator '{Operator}'"); + } + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstExpressionVariable.cs b/Rms.Risk.Mango.Language/Ast/AstExpressionVariable.cs new file mode 100644 index 0000000..1071df9 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstExpressionVariable.cs @@ -0,0 +1,30 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public partial class AstExpressionVariable(string _name) : AstExpression +{ + public string Name => PreprocessFieldName(_name); + + public override void Append(StringBuilder sb, int indent) + =>AppendField(sb, Name); + + public override JsonNode? AsJson() => JsonValue.Create($"${Name}"); + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/Ast/AstFunctionArgument.cs b/Rms.Risk.Mango.Language/Ast/AstFunctionArgument.cs new file mode 100644 index 0000000..9c8d711 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstFunctionArgument.cs @@ -0,0 +1,47 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstFunctionArgument : AstExpression +{ + public AstFunctionArgument() + { + } + + public AstFunctionArgument(string? name, AstExpression value) + { + Name = name!; + Add(value); + } + + public string Name { get; internal set{ field = PreprocessFieldName(value); } } = ""; + public AstExpression Value => Children.OfType().First(); + + public override void Append(StringBuilder sb, int indent) + { + if (!string.IsNullOrEmpty(Name)) + sb.Append($"{Name}: "); + Value.Append(sb, indent); + } + + public override JsonNode? AsJson() + => string.IsNullOrWhiteSpace(Name) + ? Value.AsJson() + : new JsonObject([new KeyValuePair(Name, Value.AsJson())]); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstLet.cs b/Rms.Risk.Mango.Language/Ast/AstLet.cs new file mode 100644 index 0000000..1f7f3e3 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstLet.cs @@ -0,0 +1,44 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public abstract class AstLet : AstNodeBase +{ + public AstLet(string? name = null) + { + Name = name; + } + + public string? Name { get; internal set{ field = PreprocessFieldName(value); } } + public abstract void AddToJson(JsonObject res, bool simplifyTargetNames = false); + public abstract void AddToJson(JsonArray res, bool simplifyTargetNames = false); + + public static JsonObject AsJson(IEnumerable lets, bool simplifyTargetNames = false) + { + var res = new JsonObject(); + + foreach (var field in lets) + field.AddToJson(res); + + return res; + } + + public sealed override JsonNode? AsJson() + => throw new NotImplementedException("Use AddToJson instead"); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstLetArray.cs b/Rms.Risk.Mango.Language/Ast/AstLetArray.cs new file mode 100644 index 0000000..d6f2184 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstLetArray.cs @@ -0,0 +1,130 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstLetArray : AstLet +{ + public AstLetArray() + { + } + + public AstLetArray(string? name, IEnumerable fields, bool isArray) + { + Name = name?.Trim('\"');//PreprocessFieldName(name); + IsArray = isArray; + + foreach (var field in fields) + Add(field); + } + + public bool IsArray { get; internal set; } + + public IReadOnlyList Fields => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + var openingBracket = IsArray ? "[" : "{"; + var closingBracket = IsArray ? "]" : "}"; + + sb.AppendLine($"{Spaces(indent)}{openingBracket}"); + var first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + field.Append(sb, indent + 1); + } + sb.AppendLine(); + + if ( !string.IsNullOrWhiteSpace(Name) ) + { + sb.Append($"{Spaces(indent)}{closingBracket} AS "); + AppendFieldName(sb, Name); + } + else + { + sb.Append($"{Spaces(indent)}{closingBracket}"); + } + } + + public override void AddToJson(JsonObject res, bool simplifyTargetNames = false) + { + if ( string.IsNullOrWhiteSpace(Name)) + throw new ("Array name must be specified"); + + var body = new JsonArray(); + foreach (var field in Fields) + { + field.AddToJson(body, simplifyTargetNames); + } + + res.Add(Name!, body); + } + + public override void AddToJson(JsonArray res, bool simplifyTargetNames = false) + { + if (IsArray) + { + var body = new JsonArray(); + + if (!string.IsNullOrWhiteSpace(Name)) + { + foreach (var field in Fields) + { + field.AddToJson(body, simplifyTargetNames); + } + + res.Add(new JsonObject([new(Name, body)])); + return; + } + + foreach (var field in Fields) + { + field.AddToJson(body, simplifyTargetNames); + } + res.Add(body); + } + else + { + // this is an object + var body = new JsonObject(); + + if (!string.IsNullOrWhiteSpace(Name)) + { + foreach (var field in Fields) + { + field.AddToJson(body, simplifyTargetNames); + } + + res.Add(new JsonObject([new(Name, body)])); + return; + } + + foreach (var field in Fields) + { + field.AddToJson(body, simplifyTargetNames); + } + res.Add(body); + } + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstLetExpression.cs b/Rms.Risk.Mango.Language/Ast/AstLetExpression.cs new file mode 100644 index 0000000..d2b81c4 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstLetExpression.cs @@ -0,0 +1,75 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstLetExpression : AstLet +{ + public AstLetExpression(AstExpression expression, string? name = null) + { + Add(expression); + if ( expression is not AstExpressionVariable ev || ev.Name != PreprocessFieldName(name)) + { + Name = name?.Trim('\"').Trim('\'');//PreprocessFieldName(name); + if ( string.IsNullOrWhiteSpace(Name)) + Name = null; + } + } + + public AstExpression Expression => Children.OfType().First(); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append(Spaces(indent)); + Expression.Append(sb, indent); + + if ( !string.IsNullOrWhiteSpace(Name) ) + { + sb.Append(" AS "); + AppendFieldName(sb, Name); + } + } + + public override void AddToJson(JsonObject res, bool simplifyTargetNames = false) + { + if ( !string.IsNullOrWhiteSpace(Name) ) + res.Add(Name, Expression.AsJson()); + else + { + var name = PreprocessFieldName((Expression as AstExpressionVariable)?.Name); + if ( string.IsNullOrWhiteSpace(name) ) + throw new ($"{Expression} must have a name"); +// res.Add(name, simplifyTargetNames ? JsonValue.Create(1) : JsonValue.Create($"${name}")); + res.Add(name, JsonValue.Create($"${name}")); + } + } + + public override void AddToJson(JsonArray res, bool simplifyTargetNames = false) + { + if ( !string.IsNullOrWhiteSpace(Name) ) + res.Add( new JsonObject([ new(Name, Expression.AsJson())])); + else + { + var name = PreprocessFieldName((Expression as AstExpressionVariable)?.Name); + if ( string.IsNullOrWhiteSpace(name) ) + throw new ($"{Expression} must have a name"); +// res.Add( new JsonObject([ new(name, simplifyTargetNames ? JsonValue.Create(1) : JsonValue.Create($"${name}"))])); + res.Add( new JsonObject([ new(name, JsonValue.Create($"${name}"))])); + } + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstNamedPipeline.cs b/Rms.Risk.Mango.Language/Ast/AstNamedPipeline.cs new file mode 100644 index 0000000..44fd01f --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstNamedPipeline.cs @@ -0,0 +1,46 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstNamedPipeline : AstNodeBase +{ + public AstNamedPipeline(string name, AstPipeline pipeline) + { + Name = name; + Add(pipeline); + } + + public string Name { get; internal set => field = PreprocessFieldName(value); } = ""; + public AstPipeline? Pipeline => Children.OfType().FirstOrDefault(); + + public override void Append(StringBuilder sb, int indent) + { + if (Pipeline == null) + throw new($"Pipeline is mandatory: {Name}"); + + sb.Append($"{Spaces(indent)}"); + AppendField(sb, Name); + sb.AppendLine(" PIPELINE {"); + Pipeline.Append(sb, indent + 1); + sb.Append($"{Spaces(indent)}}}"); + } + + public override JsonNode? AsJson() + => new JsonObject() { new(Name, Pipeline?.AsJson() ?? throw new($"Pipeline is mandatory: {Name}")) }; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/Ast/AstNodeBase.cs b/Rms.Risk.Mango.Language/Ast/AstNodeBase.cs new file mode 100644 index 0000000..ea5d3e2 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstNodeBase.cs @@ -0,0 +1,100 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text.RegularExpressions; + +namespace Rms.Risk.Mango.Language.Ast; + +public abstract partial class AstNodeBase +{ + private readonly Lazy> _children = new(); + private readonly Lazy> _properties = new(); + + public bool Empty => Count == 0; + public int Count => _children.IsValueCreated ? _children.Value.Count : 0; + public IReadOnlyList Children => _children.Value; + public AstNodeBase? Parent { get; set; } = null; + + + public void Add(AstNodeBase child) + { + _children.Value.Add(child); + child.Parent = this; + } + + public void SetProperty(string name, object value) => _properties.Value[name] = value; + + public T? GetProperty(string name, T? defaultValue = default(T), bool checkParent = true ) + { + if (_properties.IsValueCreated && _properties.Value.TryGetValue(name, out var v)) + return (T)v; + + if (Parent == null || !checkParent) + return defaultValue; + + return Parent.GetProperty(name); + } + + public override string ToString() => AsText(); + + public string AsText() + { + var sb = new StringBuilder(); + Append(sb, 0); + return sb.ToString(); + } + + public abstract void Append(StringBuilder sb, int indent); + public abstract JsonNode? AsJson(); + + protected static string Spaces(int indent) + { + var sb = new StringBuilder(); + for (int i = 0; i < indent; i++) + sb.Append(" "); + return sb.ToString(); + } + + [GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]+$")] + private static partial Regex SimpleVarNameRegex(); + + [GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_.]+$")] + private static partial Regex ComplexVarNameRegex(); + + protected static string PreprocessFieldName(string? name) + => name?.Trim('\"').Trim('\'').TrimStart('$') ?? ""; + + protected static void AppendField(StringBuilder sb, string name) + { + if ( SimpleVarNameRegex().IsMatch(name) ) + sb.Append(name); + else if ( ComplexVarNameRegex().IsMatch(name) ) + sb.Append($"${name}"); + else + sb.Append($"\'{name}'"); + } + + protected static void AppendFieldName(StringBuilder sb, string name) + { + if ( SimpleVarNameRegex().IsMatch(name) ) + sb.Append(name); + else + sb.Append($"\"{name}\""); + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstPipeline.cs b/Rms.Risk.Mango.Language/Ast/AstPipeline.cs new file mode 100644 index 0000000..18f71e6 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstPipeline.cs @@ -0,0 +1,46 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstPipeline : AstNodeBase +{ + public static readonly AstPipeline None = new(); + + public AstPipeline() + { + } + + public AstPipeline(IEnumerable stages) + { + foreach (var stage in stages) + Add(stage); + } + + public IReadOnlyList Stages => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + foreach (var stage in Stages) + stage.Append(sb, indent); + } + + public override JsonNode? AsJson() => new JsonArray([.. Stages.Select(x => x.AsJson())]); +} diff --git a/Rms.Risk.Mango.Language/Ast/AstSortField.cs b/Rms.Risk.Mango.Language/Ast/AstSortField.cs new file mode 100644 index 0000000..d1f656f --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstSortField.cs @@ -0,0 +1,49 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstSortField : AstNodeBase +{ + public enum SortOrder { Ascending, Descending } + + public AstSortField(){} + public AstSortField( string name, SortOrder order ) + { + Name = name; + Order = order; + } + + public string Name { get; internal set{ field = PreprocessFieldName(value); } } = ""; + public SortOrder Order { get; internal set; } = SortOrder.Ascending; + + public override void Append(StringBuilder sb, int indent) + { + sb.Append(Spaces(indent)); + AppendField(sb, Name); + + if ( Order != SortOrder.Ascending ) + sb.Append(" DESC"); + } + + public override JsonNode? AsJson() + { + var val = JsonValue.Create(Order == SortOrder.Ascending ? 1 : -1); + return new JsonObject([new(Name, val)]); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStage.cs b/Rms.Risk.Mango.Language/Ast/AstStage.cs new file mode 100644 index 0000000..00f64e9 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStage.cs @@ -0,0 +1,40 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public abstract class AstStage : AstNodeBase +{ + public JsonObject? Options { get; internal set; } + + protected JsonNode? ApplyOptions(JsonNode? json ) + { + if ( json is not JsonObject jo || Options == null) + return json; + + // apply overrides from Options object to jo and return the result + var stage = jo.ElementAt(0).Value as JsonObject; + if (stage == null) + return json; + + foreach (var (key, value) in Options) + stage![key] = value?.DeepClone(); + + return jo; + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageAddFields.cs b/Rms.Risk.Mango.Language/Ast/AstStageAddFields.cs new file mode 100644 index 0000000..fb63f45 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageAddFields.cs @@ -0,0 +1,72 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageAddFields : AstStage +{ + public AstStageAddFields(IEnumerable fields) + { + foreach (var field in fields) + Add((AstNodeBase)field); + } + + public IReadOnlyList Fields => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}ADD"); + + var first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + if ( field is AstLetExpression let) + { + if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name)) + { + sb.Append($"{Spaces(indent + 1)}"); + ev.Append(sb, indent + 1); + } + else + let.Append(sb, indent + 1); + } + else + ((AstNodeBase)field).Append(sb, indent + 1); + } + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + + var fields = new JsonObject(); + + foreach (var field in Fields) + { + field.AddToJson(fields); + } + + var stage = new JsonObject([ new("$addFields", fields)]); + return ApplyOptions(stage); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageBucket.cs b/Rms.Risk.Mango.Language/Ast/AstStageBucket.cs new file mode 100644 index 0000000..6b555f9 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageBucket.cs @@ -0,0 +1,236 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Globalization; + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageBucket : AstStage +{ + private readonly List _buckets = []; + public bool Auto { get; internal set; } + public int NumberOfBuckets { get; internal set; } + public string? Granularity { get; internal set; } + + public IReadOnlyList Fields => Children.OfType().ToList(); + + public IReadOnlyList Buckets => _buckets; + + public void AddBucket(string bucket) => _buckets.Add(bucket); + public void AddBucket(double bucket) => _buckets.Add(bucket.ToString(CultureInfo.InvariantCulture)); + public void AddBucket(long bucket) => _buckets.Add(bucket.ToString()); + + public string? DefaultBucket { get; internal set; } + public AstExpression GroupBy { get; internal set; } = null!; + + public override void Append(StringBuilder sb, int indent) + { + if (Auto) + AppendAuto(sb, indent); + else + AppendPlain(sb, indent); + } + + private void AppendPlain(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}BUCKET"); + + sb.Append($"{Spaces(indent+1)}"); + GroupBy.Append(sb, indent+1); + sb.AppendLine(); + + sb.Append($"{Spaces(indent+1)}BOUNDARIES "); + + var first = true; + foreach (var bucket in Buckets) + { + if ( first ) + first = false; + else + sb.Append(", "); + + if (double.TryParse(bucket, out _)) + sb.Append(bucket); + else + sb.Append($"\"{bucket}\""); + } + sb.AppendLine(); + + if (!string.IsNullOrWhiteSpace(DefaultBucket)) + { + sb.Append($"{Spaces(indent+1)}DEFAULT "); + + if (double.TryParse(DefaultBucket, out _)) + sb.Append(DefaultBucket); + else + sb.Append($"\"{DefaultBucket}\""); + sb.AppendLine(); + } + + var fields = Fields; + if (fields.Count > 0) + { + sb.AppendLine($"{Spaces(indent + 1)}LET"); + first = true; + foreach (var field in Fields) + { + if (!first) + sb.AppendLine(","); + else + first = false; + + if (field is AstLetExpression let) + { + if ((let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name)) + { + sb.Append($"{Spaces(indent + 2)}"); + ev.Append(sb, indent + 2); + } + else + let.Append(sb, indent + 2); + } + else + ((AstNodeBase)field).Append(sb, indent + 2); + } + sb.AppendLine(); + } + } + + private void AppendAuto(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}BUCKET AUTO"); + + sb.Append($"{Spaces(indent+1)}"); + GroupBy.Append(sb, indent+1); + sb.AppendLine(); + + sb.AppendLine($"{Spaces(indent+1)}BUCKETS {NumberOfBuckets}"); + if ( !string.IsNullOrWhiteSpace(Granularity) ) + sb.AppendLine($"{Spaces(indent+1)}GRANULARITY \"{Granularity}\""); + + var fields = Fields; + if (fields.Count > 0) + { + sb.AppendLine($"{Spaces(indent + 1)}LET"); + var first = true; + foreach (var field in Fields) + { + if (!first) + sb.AppendLine(","); + else + first = false; + + if (field is AstLetExpression let) + { + if ((let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name)) + { + sb.Append($"{Spaces(indent + 2)}"); + ev.Append(sb, indent + 2); + } + else + let.Append(sb, indent + 2); + } + else + ((AstNodeBase)field).Append(sb, indent + 2); + } + sb.AppendLine(); + } + } + + public override JsonNode? AsJson() + => Auto + ? AsJsonAuto() + : AsJsonPlain() + ; + + public JsonNode? AsJsonPlain() + { + var body = new JsonObject + { + ["groupBy"] = GroupBy.AsJson() + }; + + var bounds = new JsonArray(); + foreach (var bucket in Buckets) + { + if (long.TryParse(bucket, out var l)) + bounds.Add(l); + else if (double.TryParse(bucket, out var d)) + bounds.Add(d); + else + bounds.Add(bucket); + + } + body["boundaries"] = bounds; + + if (!string.IsNullOrWhiteSpace(DefaultBucket)) + { + if (long.TryParse(DefaultBucket, out var l)) + body["default"] = JsonValue.Create(l); + else if (double.TryParse(DefaultBucket, out var d)) + body["default"] = JsonValue.Create(d); + else + body["default"] = JsonValue.Create(DefaultBucket); + + } + + var fields = Fields; + if (fields.Count > 0) + { + var let = new JsonObject(); + + foreach (var field in Fields) + { + field.AddToJson(let); + } + + body["output"] = let; + } + + var stage = new JsonObject([ new("$bucket", body)]); + return ApplyOptions(stage); + } + + public JsonNode? AsJsonAuto() + { + var body = new JsonObject + { + ["groupBy"] = GroupBy.AsJson(), + ["buckets"] = NumberOfBuckets + }; + + if ( !string.IsNullOrWhiteSpace(Granularity) ) + body["granularity"] = Granularity; + + var fields = Fields; + if (fields.Count > 0) + { + var let = new JsonObject(); + + foreach (var field in Fields) + { + field.AddToJson(let); + } + + body["output"] = let; + } + + var stage = new JsonObject([ new("$bucketAuto", body)]); + return ApplyOptions(stage); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/Ast/AstStageDo.cs b/Rms.Risk.Mango.Language/Ast/AstStageDo.cs new file mode 100644 index 0000000..0b01087 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageDo.cs @@ -0,0 +1,55 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageDo : AstStage +{ + public AstStageDo() + { + + } + + public AstStageDo(JsonNode json) + { + Json = json; + } + + + public JsonNode? Json { get; set; } + + public override void Append(StringBuilder sb, int indent) + { + if ( Json == null ) + return; + + sb.AppendLine($"{Spaces(indent)}DO "); + var json = JsonSerializer.Serialize(Json, PrettyPrint) + .Replace("\r", "") + .Split("\n"); + sb.AppendJoin("\n", json.Select( x => $"{Spaces(indent)}{x}")); + sb.AppendLine(); + } + + public override JsonNode? AsJson() => Json; + + private static JsonSerializerOptions PrettyPrint = new() + { + WriteIndented = true + }; +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageFacet.cs b/Rms.Risk.Mango.Language/Ast/AstStageFacet.cs new file mode 100644 index 0000000..3fee6fe --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageFacet.cs @@ -0,0 +1,64 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageFacet : AstStage +{ + public IReadOnlyList Pipelines => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}FACET"); + + var first = true; + foreach (var field in Pipelines) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + field.Append(sb, indent + 1); + } + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + var stage = new JsonObject + { + { + "$facet", + new JsonObject + { + } + } + }; + + var body = (JsonObject)stage.ElementAt(0).Value!; + + foreach (var pipeline in Pipelines) + { + body.Add(pipeline.Name, pipeline.Pipeline!.AsJson()); + } + + return ApplyOptions(stage); + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageGroupBy.cs b/Rms.Risk.Mango.Language/Ast/AstStageGroupBy.cs new file mode 100644 index 0000000..b81b78b --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageGroupBy.cs @@ -0,0 +1,105 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageGroupBy : AstStage +{ + private List _id = []; + + public AstStageGroupBy(IEnumerable id, IEnumerable fields) + { + foreach (var x in fields) + Add(x); + foreach (var x in id) + { + _id.Add(x); + x.Parent = this; + } + } + + public void AddId(AstLet field) => _id.Add(field); + + public IReadOnlyList Id => _id; + public IReadOnlyList Fields => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}GROUP BY"); + + var first = true; + foreach (var field in Id) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + field.Append(sb, indent + 1); + } + sb.AppendLine(); + + first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + { + first = false; + sb.AppendLine($"{Spaces(indent+1)}LET"); + } + field.Append(sb, indent + 2); + } + + if ( !first ) + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + var body = new JsonObject(); + + var _id = Id.OfType().ToList(); + // _id can be a single field or an expression + if (_id.Count == 1 && string.IsNullOrWhiteSpace(_id[0].Name) ) + body.Add("_id", _id[0].Expression.AsJson()); + + // alternative method for object-style _id + if (body.Count == 0) + { + var idFields = new JsonObject(); + foreach (var field in _id) + field.AddToJson(idFields); + body.Add("_id", idFields); + } + + var fields = Fields.OfType().ToList(); + + foreach (var field in fields) + field.AddToJson(body); + + var stage = new JsonObject + { + { "$group", body } + }; + + return ApplyOptions(stage); + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageJoin.cs b/Rms.Risk.Mango.Language/Ast/AstStageJoin.cs new file mode 100644 index 0000000..95d4d80 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageJoin.cs @@ -0,0 +1,124 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageJoin : AstStage +{ + public AstStageJoin() + { + + } + + public AstStageJoin(string collection, string asField, IEnumerable on, IEnumerable let, AstPipeline? pipeline = null) + { + Init(collection, asField, on, let, pipeline); + } + + public void Init(string collection, string asField, IEnumerable on, IEnumerable let, AstPipeline? pipeline = null) + { + Collection = collection.Trim('\"'); + AsField = PreprocessFieldName(asField); + foreach(var x in on) + Add(x); + foreach(var x in let) + Add(x); + if (pipeline != null) + Add(pipeline); + } + + public string Collection { get; private set;} = ""; + public string AsField { get; private set; } = ""; + + public IReadOnlyList On => Children.OfType().ToList(); + public IReadOnlyList Let => Children.OfType().ToList(); + public AstPipeline? Pipeline => Children.OfType().FirstOrDefault(); + + public override void Append(StringBuilder sb, int indent) + { + sb.Append($"{Spaces(indent)}JOIN \"{Collection}\" AS "); + AppendField(sb, AsField); + sb.AppendLine($" ON"); + + var first = true; + foreach (var field in On) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + field.Append(sb, indent + 1); + } + sb.AppendLine(); + + if ( Let.Count > 0 ) + { + sb.AppendLine($"{Spaces(indent+1)}LET"); + foreach (var field in Let) + { + field.Append(sb, indent + 2); + } + sb.AppendLine(); + } + + if( Pipeline != null ) + { + sb.AppendLine($"{Spaces(indent + 1)}PIPELINE {{"); + Pipeline.Append(sb, indent + 2); + sb.AppendLine($"{Spaces(indent + 1)}}}"); + } + } + + public override JsonNode? AsJson() + { + var eqv = On.First(); + + var stage = new JsonObject + { + { + "$lookup", + new JsonObject + { + {"from", Collection}, + {"localField", JsonValue.Create(eqv.Left.Name)}, + {"foreignField", JsonValue.Create(eqv.Right.Name)} + } + } + }; + + var body = (JsonObject)stage.ElementAt(0).Value!; + + if (!string.IsNullOrWhiteSpace(AsField)) + { + body.Add( new("as", JsonValue.Create(AsField)) ); + } + + if ( Let?.Count > 0 ) + { + var fields =AstLet.AsJson(Let); + body.Add( new("let", fields) ); + } + + if ( Pipeline != null ) + body.Add( new("pipeline", Pipeline.AsJson()) ); + + return ApplyOptions(stage); + } + +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageProject.cs b/Rms.Risk.Mango.Language/Ast/AstStageProject.cs new file mode 100644 index 0000000..66ba534 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageProject.cs @@ -0,0 +1,131 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageProject : AstStage +{ + public AstStageProject() {} + + public AstStageProject(IEnumerable id, IEnumerable fields, bool exclude = false) + { + _id.AddRange(id); + Exclude = exclude; + foreach (var field in fields.OfType()) + Add(field); + } + + public void AddId(AstLet field) + { + _id.Add(field); + } + + private List _id = []; + + public IReadOnlyList IdFields => _id; + public IReadOnlyList Fields => Children.OfType().ToList(); + + public bool Exclude { get; internal set; } + + public override void Append(StringBuilder sb, int indent) + { + bool first; + if ( Exclude || IdFields.Count == 0) + sb.AppendLine($"{Spaces(indent)}PROJECT{(Exclude ? " EXCLUDE" : "")}"); + else + { + sb.AppendLine($"{Spaces(indent)}PROJECT ID {{"); + first = true; + foreach (var field in IdFields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + field.Append(sb, indent + 2); + } + sb.AppendLine(); + sb.AppendLine($"{Spaces(indent+1)}}}"); + } + + first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + if ( field is AstLetExpression let) + { + if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name)) + { + sb.Append($"{Spaces(indent + 1)}"); + ev.Append(sb, indent + 1); + } + else + let.Append(sb, indent + 1); + } + else + field.Append(sb, indent + 1); + } + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + + var fields = new JsonObject(); + if ( IdFields.Count > 0 ) + { + var idFields = new JsonObject(); + + foreach (var field in IdFields) + { + if ( field is AstLetExpression let) + AddFields(let, idFields); + } + + fields.Add("_id", idFields); + } + + foreach (var field in Fields) + { + if ( field is AstLetExpression let) + AddFields(let, fields); + } + + var stage = new JsonObject([ new("$project", fields)]); + return ApplyOptions(stage); + } + + private void AddFields(AstLetExpression field, JsonObject fields) + { + var name = field.Name; + if ( string.IsNullOrWhiteSpace(name) ) + name = (field.Expression as AstExpressionVariable)?.Name; + + if ( string.IsNullOrWhiteSpace(name) ) + throw new ($"{field.Expression} must have a name"); + + if ( (field.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(field.Name)) + fields.Add(ev.Name, JsonValue.Create(Exclude ? 0 : 1)); + else + fields.Add(name, field.Expression.AsJson()); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageReplace.cs b/Rms.Risk.Mango.Language/Ast/AstStageReplace.cs new file mode 100644 index 0000000..4adcb80 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageReplace.cs @@ -0,0 +1,128 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageReplace : AstStage +{ + public AstStageReplace() {} + + public AstStageReplace(IEnumerable id, IEnumerable fields) + { + _id.AddRange(id); + foreach (var field in fields.OfType()) + Add(field); + } + + public void AddId(AstLet field) + { + _id.Add(field); + } + + private List _id = []; + + public IReadOnlyList IdFields => _id; + public IReadOnlyList Fields => Children.OfType().ToList(); + + + public override void Append(StringBuilder sb, int indent) + { + bool first; + + if ( IdFields.Count == 0) + sb.AppendLine($"{Spaces(indent)}REPLACE"); + else + { + sb.AppendLine($"{Spaces(indent)}REPLACE ID {{"); + + first = true; + foreach (var field in IdFields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + field.Append(sb, indent + 2); + } + sb.AppendLine(); + sb.AppendLine($"{Spaces(indent+1)}}}"); + } + + first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + if ( field is AstLetExpression let) + { + if ( (let.Expression is AstExpressionVariable ev) && string.IsNullOrWhiteSpace(let.Name)) + { + sb.Append($"{Spaces(indent + 1)}"); + ev.Append(sb, indent + 1); + } + else + let.Append(sb, indent + 1); + } + else + field.Append(sb, indent + 1); + } + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + + var fields = new JsonObject(); + if ( IdFields.Count > 0 ) + { + var idFields = new JsonObject(); + + foreach (var field in IdFields) + { + if ( field is AstLetExpression let) + AddFields(let, idFields); + } + + fields.Add("_id", idFields); + } + + foreach (var field in Fields) + { + if ( field is AstLetExpression let) + AddFields(let, fields); + } + + var stage = new JsonObject([ new("$replaceWith", fields)]); + return ApplyOptions(stage); + } + + private void AddFields(AstLetExpression field, JsonObject fields) + { + var name = field.Name; + if ( string.IsNullOrWhiteSpace(name) ) + name = (field.Expression as AstExpressionVariable)?.Name; + + if ( string.IsNullOrWhiteSpace(name) ) + throw new ($"{field.Expression} must have a name"); + + fields.Add(name, field.Expression.AsJson()); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageSortBy.cs b/Rms.Risk.Mango.Language/Ast/AstStageSortBy.cs new file mode 100644 index 0000000..b8e1376 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageSortBy.cs @@ -0,0 +1,61 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageSortBy : AstStage +{ + public AstStageSortBy() + { + } + + public AstStageSortBy(IEnumerable fields) + { + foreach (var field in fields) + Add(field); + } + + public IReadOnlyList Fields => Children.OfType().ToList(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}SORT BY"); + var first = true; + foreach (var field in Fields) + { + if ( !first ) + sb.AppendLine(","); + else + first = false; + + sb.Append(Spaces(indent + 1)); + field.Append(sb, indent + 1); + } + sb.AppendLine(); + } + + public override JsonNode? AsJson() + { + var fields = new JsonObject(); + foreach (var field in Fields) + fields.Add(field.Name, JsonValue.Create(field.Order == AstSortField.SortOrder.Descending ? -1 : 1)); + + var stage = new JsonObject([ new("$sort", fields)]); + return ApplyOptions(stage); + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageUnwind.cs b/Rms.Risk.Mango.Language/Ast/AstStageUnwind.cs new file mode 100644 index 0000000..1d62763 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageUnwind.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageUnwind : AstStage +{ + public AstStageUnwind() {} + public AstStageUnwind(string fieldName, string? indexName = null) + { + Name = fieldName; + Index = indexName!; + } + + public string Name { get; internal set { field = PreprocessFieldName(value); }} = ""; + public string? Index { get; internal set { field = PreprocessFieldName(value); }} + + public override void Append(StringBuilder sb, int indent) + { + if (string.IsNullOrWhiteSpace(Index)) + sb.AppendLine($"{Spaces(indent)}UNWIND {Name}"); + else + sb.AppendLine($"{Spaces(indent)}UNWIND {Name} INDEX {Index}"); + } + + public override JsonNode? AsJson() + { + if ( string.IsNullOrWhiteSpace(Index) ) + { + var res = new JsonObject + {{ + "$unwind", + new JsonObject + { + { "path", JsonValue.Create($"${Name}") } + } + }}; + return ApplyOptions(res); + } + else + { + var res = new JsonObject + {{ + "$unwind", + new JsonObject + { + { "path", JsonValue.Create($"${Name}") }, + { "includeArrayIndex", JsonValue.Create(Index) } + } + }}; + return ApplyOptions(res); + } + } +} diff --git a/Rms.Risk.Mango.Language/Ast/AstStageWhere.cs b/Rms.Risk.Mango.Language/Ast/AstStageWhere.cs new file mode 100644 index 0000000..a604577 --- /dev/null +++ b/Rms.Risk.Mango.Language/Ast/AstStageWhere.cs @@ -0,0 +1,44 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Rms.Risk.Mango.Language.Ast; + +public class AstStageWhere : AstStage +{ + public AstStageWhere() + { + } + + public AstStageWhere(AstExpression expression) + { + Add(expression); + } + + public AstExpression Expression => Children.OfType().First(); + + public override void Append(StringBuilder sb, int indent) + { + sb.AppendLine($"{Spaces(indent)}WHERE"); + sb.Append(Spaces(indent + 1)); + Expression.Append(sb, indent + 1); + sb.AppendLine(); + } + + public override JsonNode? AsJson() => ApplyOptions(new JsonObject([ new("$match", Expression.AsJson())])); +} diff --git a/Rms.Risk.Mango.Language/JsonGrammar.g4 b/Rms.Risk.Mango.Language/JsonGrammar.g4 new file mode 100644 index 0000000..e9ddeb6 --- /dev/null +++ b/Rms.Risk.Mango.Language/JsonGrammar.g4 @@ -0,0 +1,83 @@ +// +// dbMango +// +// Copyright 2025 Deutsche Bank AG +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +grammar JsonGrammar; + +json + : object + | array + ; + +value + : object + | array + | 'true' + | 'false' + | 'null' + | NUMBER + | STRING + | VARIABLE + ; + +object + : '{' pair (',' pair)* '}' + | '{' '}' + ; + +pair + : object_name ':' value + ; + +object_name + : VARIABLE + | STRING + ; + +array + : '[' value (',' value)* ']' + | '[' ']' + ; + +// ---------------------- LEXER ---------------------------- + +// Fragments +fragment DIGIT : [0-9]; +fragment INT : '-'? DIGIT+; +fragment EXPONENT : [Ee] [+-]? DIGIT+; +fragment FLOAT : INT ('.' DIGIT+)? EXPONENT?; + +//fragment VERY_COMPLEX_VAR_FRAGMENT : '\'' [^']+ '\''; + +fragment VAR_FRAGMENT : ([a-zA-Z_] [a-zA-Z0-9_]*) | ('$' [a-zA-Z_] [a-zA-Z0-9_.]*) | ('\'' (ESC | ~['\\])* '\''); +fragment STRING_FRAGMENT : ('"' (ESC | ~["\\])* '"') | ('""'); +fragment ESC : '\\' (["'\\/bfnrt] | UNICODE); +fragment UNICODE : 'u' HEX HEX HEX HEX; +fragment HEX : [0-9a-fA-F]; + +// Tokens + +NUMBER : INT | FLOAT; +STRING : STRING_FRAGMENT; +VARIABLE : VAR_FRAGMENT; + +WS : [ \t\r\n]+ -> skip; + + +COMMENT + : ('/*' .*? '*/' | '//' .*? '\n') -> skip + ; diff --git a/Rms.Risk.Mango.Language/LanguageParser.cs b/Rms.Risk.Mango.Language/LanguageParser.cs new file mode 100644 index 0000000..2860506 --- /dev/null +++ b/Rms.Risk.Mango.Language/LanguageParser.cs @@ -0,0 +1,70 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; + +namespace Rms.Risk.Mango.Language; + +public static class LanguageParser +{ + public static AstAggregation ParseScriptToAST(string input) + { + var str = new AntlrInputStream(input); + var lexer = new MongoAggregationForHumansLexer(str); + var tokens = new CommonTokenStream(lexer); + var parser = new MongoAggregationForHumansParser(tokens); + + parser.AddErrorListener(new ErrorListener("parser")); + lexer.AddErrorListener(new ErrorListener("lexer")); + + var tree = parser.file(); + + var astListener = new MongoGrammarListener(); + var walker = new ParseTreeWalker(); + + walker.Walk(astListener, tree); + + return astListener.Aggregate ?? throw new ("No Aggregation parsed"); + } + + public static AstAggregation ParseAggregationJsonToAST(string collection, string json) + => AggregationPipelineParser.Parse(collection, json); + + public static AstAggregation ParseAggregationJsonToAST(string collection, JsonArray json) + => AggregationPipelineParser.Parse(collection, json); + + public static void ParseJsonForFun(string input) + { + var str = new AntlrInputStream(input); + var lexer = new JsonGrammarLexer(str); + var tokens = new CommonTokenStream(lexer); + var parser = new JsonGrammarParser(tokens); + + parser.AddErrorListener(new ErrorListener("parser")); + lexer.AddErrorListener(new ErrorListener("lexer")); + + var tree = parser.json(); + + var astListener = new JsonGrammarBaseListener(); + var walker = new ParseTreeWalker(); + + walker.Walk(astListener, tree); + } + +} diff --git a/Rms.Risk.Mango.Language/MongoAggregationForHumans.g4 b/Rms.Risk.Mango.Language/MongoAggregationForHumans.g4 new file mode 100644 index 0000000..3d0a9b2 --- /dev/null +++ b/Rms.Risk.Mango.Language/MongoAggregationForHumans.g4 @@ -0,0 +1,192 @@ +// +// dbMango +// +// Copyright 2025 Deutsche Bank AG +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +grammar MongoAggregationForHumans; +import JsonGrammar; + +file + : 'FROM' STRING pipeline_def + ; + +pipeline_def + : 'PIPELINE' '{' stages_list '}' + ; + + stages_list + : stage_def + | stages_list stage_def + ; + +stage_def + : match_def ('OPTIONS' json)? + | bucket_def ('OPTIONS' json)? + | facet_def ('OPTIONS' json)? + | addfields_def ('OPTIONS' json)? + | project_def ('OPTIONS' json)? + | group_by_def ('OPTIONS' json)? + | sort_def ('OPTIONS' json)? + | join_def ('OPTIONS' json)? + | unwind_def ('OPTIONS' json)? + | replace_def ('OPTIONS' json)? + | do_def ('OPTIONS' json)? + ; + +match_def: 'WHERE' expression + ; + +bucket_def + : 'BUCKET' expression 'BOUNDARIES' NUMBER (',' NUMBER)* ('DEFAULT' defaultBucket=(NUMBER | STRING))? ('LET' let_list)? # BucketPlain + | 'BUCKET' 'AUTO' expression 'BUCKETS' NUMBER ('GRANULARITY' STRING)? ('LET' let_list)? # BucketAuto + ; + +facet_def + : 'FACET' VARIABLE pipeline_def (',' VARIABLE pipeline_def)* + ; + +addfields_def + : 'ADD' let_list + ; + +project_def + : 'PROJECT' ('ID' '{' id_list=let_list '}')? data_list=let_list # ProjectInclude + | 'PROJECT' 'EXCLUDE' var_list # ProjectExclude + ; + +replace_def + : 'REPLACE' 'ID' '{' id_list=let_list '}' data_list=let_list + ; + + +group_by_def: 'GROUP' 'BY' id_list=let_list ('LET' data_list=let_list)? + ; + +sort_def: 'SORT' 'BY' sort_var_list + ; + +join_def: 'JOIN' STRING 'AS' (VARIABLE | STRING) 'ON' equivalence_list ('LET' let_list )? (pipeline_def)? + ; + +unwind_def: 'UNWIND' VARIABLE ('INDEX' VARIABLE)? + ; + +do_def: 'DO' json + ; + +equivalence_list + : left=VARIABLE '==' right=VARIABLE #VarEquivalence + | equivalence_list ',' equivalence_list #EquivalenceList + ; + +sort_var_list + : VARIABLE (ASC | DESC)? + | sort_var_list ',' sort_var_list + ; + +var_list + : VARIABLE + | var_list ',' var_list + ; + +let_list + : let_list_item + | let_list ',' let_list + ; + +let_list_item + : expression ('AS' (VARIABLE | STRING))? # LetExpressionAs + | '[' let_list ']' ('AS' (VARIABLE | STRING))? # LetArray + | '{' let_list '}' ('AS' (VARIABLE | STRING))? # LetObject + ; + +expression + : comparizon_expression ( (AND | OR) comparizon_expression )* + ; + +comparizon_expression + : additive_expression ( (EQ | NEQ | LT | LTE | GT | GTE) additive_expression )* + ; + +additive_expression + : multiplicative_expression ( (PLUS | MINUS) multiplicative_expression )* + ; + +multiplicative_expression + : unary_expression ( (MUL | DIV) unary_expression )* + ; + +unary_expression + : (PLUS | MINUS | NOT) unary_expression # UnaryExpression + | brackets_expression # PrimaryExpression + ; + +brackets_expression + : atom # AtomExpression + | VARIABLE NOT? 'IN' '(' expression (',' expression)* ')' # InExpression + | VARIABLE '(' (unnamed_args_list | named_args_list ) ')' # FuncExpression + | VARIABLE 'IS' json # ProjectionExpression + | VARIABLE NOT? 'EXISTS' # ExistsExpression + | '(' expression ')' # BracketsExpression + ; + +named_args_list + : left=VARIABLE ':' expression + | left=VARIABLE ':' expression_array + | named_args_list ',' named_args_list + ; + +unnamed_args_list + : expression + | expression_array + | unnamed_args_list ',' unnamed_args_list + ; + +expression_array + : '[' expression_array_item (',' expression_array_item)* ']' + | '[' ']' + ; + +expression_array_item + : expression + | expression_array + ; + + +atom + : STRING + | NUMBER + | 'true' + | 'false' + | 'null' + | VARIABLE + ; + +AND: 'AND' | '&&'; +OR: 'OR' | '||'; +NOT: 'NOT' | '!'; +EQ: '=='; +NEQ: '<>' | '!='; +GT: '>'; +GTE: '>='; +LT: '<'; +LTE: '<='; +ASC: 'ASC'; +DESC: 'DESC'; +MUL: '*'; +DIV: '/'; +PLUS: '+'; +MINUS: '-'; diff --git a/Rms.Risk.Mango.Language/Parsers/AggregationPipelineParser.cs b/Rms.Risk.Mango.Language/Parsers/AggregationPipelineParser.cs new file mode 100644 index 0000000..be2ed1f --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/AggregationPipelineParser.cs @@ -0,0 +1,685 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Parsers; + +internal static class AggregationPipelineParser +{ + public static AstAggregation Parse(string collection, string json) + { + var arr = (JsonArray?)JsonNode.Parse(json); + if ( arr == null ) + throw new ("Json array expected"); + return Parse(collection, arr); + } + + public static AstAggregation Parse(string collection, JsonArray json) + { + var agg = new AstAggregation(collection); + var pipeline = new AstPipeline(); + + foreach (var stageJson in json ) + { + if ( stageJson is not JsonObject jo ) + throw new ("Json object expected as stage"); + + var stage = ParseStage(jo); + pipeline.Add(stage); + } + + agg.Add(pipeline); + return agg; + } + + private static AstStage ParseStage(JsonObject json) + { + var name = json.ElementAt(0).Key; + + if (json.ElementAt(0).Value is JsonValue jv) + { + if (name == "$unwind") + return ParseUnwind(jv); + else + return new AstStageDo(json); + } + + if (json.ElementAt(0).Value is not JsonObject body) + throw new($"Json object expected as body of {name} stage"); + + switch (name) + { + case "$addFields": return ParseAddFields(body); + case "$bucket": return ParseBucket(body); + case "$bucketAuto": return ParseBucketAuto(body); + case "$facet": return ParseFacet(body); + case "$project": return ParseProject(body); + case "$match": return ParseMatch(body); + case "$group": return ParseGroup(body); + case "$sort": return ParseSort(body); + case "$unwind": return ParseUnwind(body); + case "$lookup": return ParseLookup(body); + case "$replaceWith": return ParseReplaceWith(body); + // case "$merge": return ParseMerge(body); + // case "$out": return ParseOut(body); + // case "$limit": return ParseLimit(body); + // case "$skip": return ParseSkip(body); + // case "$count": return ParseCount(body); + default: + return new AstStageDo(json); + + } + } + + private static AstStage ParseLookup(JsonObject body) + { + if ( !body.TryGetPropertyValue("from", out var collection) + || !body.TryGetPropertyValue("localField", out var local) + || !body.TryGetPropertyValue("foreignField", out var foreign) + || !body.TryGetPropertyValue("as", out var asField) + ) + throw new($"Invalid lookup stage: {body.ToJsonString()}"); + + var eqv = new AstEquivalence( new(local!.GetValue()), new(foreign!.GetValue()) ); + return new AstStageJoin(collection!.GetValue(), asField!.GetValue(), [eqv], [], null); + } + + private static AstStage ParseReplaceRoot(JsonObject body) + { + throw new NotImplementedException(); + } + + private static AstStage ParseUnwind(JsonObject body) + { + var path = body.ElementAt(0).Value?.GetValue() ?? throw new($"Expected path: {body}"); + string? index = null; + if (body.TryGetPropertyValue("includeArrayIndex", out var indexNode)) + index= indexNode!.GetValue(); + + return new AstStageUnwind(path, index); + } + + private static AstStage ParseUnwind(JsonValue body) + { + var path = body.GetValue() ?? throw new($"Expected path: {body}"); + return new AstStageUnwind(path); + } + + private static AstStage ParseMerge(JsonObject body) + { + throw new NotImplementedException(); + } + + private static AstStage ParseSort(JsonObject body) + { + var order = new List(); + foreach (var field in body) + { + var name = field.Key; + var sortOrder = field.Value?.GetValue() ?? 1; + + order.Add(new(name, sortOrder != -1 ? AstSortField.SortOrder.Ascending : AstSortField.SortOrder.Descending)); + } + return new AstStageSortBy(order); + } + + private static AstStage ParseGroup(JsonObject body) + { + var fields = new List(); + var id = new List(); + + foreach (var field in body) + { + if ( field.Key == "_id") + { + if (field.Value is JsonValue jv && jv.GetValueKind() == JsonValueKind.String) + { + var let = new AstLetExpression(new AstExpressionVariable(jv.GetValue())); + id.Add(let); + } + else + { + var idObj = field.Value as JsonObject ?? + throw new($"_id must be an object: {field.Value?.ToJsonString()}"); + + foreach (var idField in idObj) + { + var (let, _) = ParseLet(idField); + id.Add(let); + } + } + } + else + { + var (let, _) = ParseLet(field); + fields.Add(let); + } + } + + var stage = new AstStageGroupBy(id, fields); + return stage; + } + + private static AstStage ParseMatch(JsonObject body) + { + // special case: there is no logical function at the top + if ( body.Count == 1 ) + { + var field = body.ElementAt(0); + if ( !field.Key.StartsWith("$") && field.Value is JsonObject jo ) + { + var expr = ParseLogicalFuncArgument(jo); + var eq = new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(field.Key), expr); + return new AstStageWhere(eq); + } + } + var expression = ParseExpression(body); + return new AstStageWhere(expression); + } + + private static HashSet _operations = [ + "$and", + "$or", + "$eq", + "$ne", + "$gt", + "$gte", + "$lt", + "$lte", + "$add", + "$subtract", + "$divide", + "$multiply" + ]; + + private static HashSet _projectionLogicalOperations = [ + "$eq", + "$ne", + "$gt", + "$gte", + "$lt", + "$lte" + ]; + + + private static AstExpression ParseExpression(JsonNode? json) + { + switch (json) + { + case null: return new AstExpressionNull(); + case JsonArray ja: + throw new ($"Unexpected array {ja}"); + case JsonObject jo: + { + if (_operations.Contains(jo.ElementAt(0).Key)) + return ParseOperation(jo); + else + return ParseFunctionCall(jo); + } + case JsonValue jv: + { + switch (jv.GetValueKind()) + { + case JsonValueKind.String: + if (jv.GetValue().StartsWith('$') && !jv.GetValue().StartsWith("$$")) + return new AstExpressionVariable(jv.GetValue()); + else + return new AstExpressionString(jv.GetValue()); + case JsonValueKind.Number: + if (jv.TryGetValue(out var l)) + return new AstExpressionNumber(l); + if (jv.TryGetValue(out var i)) + return new AstExpressionNumber((long)i); + if (jv.TryGetValue(out var d)) + return new AstExpressionNumber(d); + throw new($"Invalid number {jv}"); + case JsonValueKind.True: return new AstExpressionBool(true); + case JsonValueKind.False: return new AstExpressionBool(false); + case JsonValueKind.Null: return new AstExpressionNull(); + default: throw new($"Invalid json value {jv}"); + } + } + } + throw new($"Invalid json expression {json}"); + } + + private static AstExpressionOperation? TryParseProjectionOperation(JsonNode? json) + { + if ( json is not JsonObject jo ) + return null; + + if ( jo.Count != 1 ) + return null; + + var fieldName = jo.ElementAt(0).Key; + if ( fieldName.StartsWith("$") ) + return null; + + var operationFunc = jo.ElementAt(0).Value as JsonObject; + if ( operationFunc?.Count != 1 ) + return null; + + var operationName = operationFunc.ElementAt(0).Key; + if ( !_projectionLogicalOperations.Contains(operationName) ) + return null; + + // HACK: { field: { $gt: {} } is equivalent to { field: { $exists: true } } + if ( operationName == "$gt" && operationFunc.ElementAt(0).Value is JsonObject { Count: 0 } ) + { + return new( + AstExpressionOperation.OperationType.EQ, + new AstExpressionVariable(fieldName), + new AstExpressionFunctionCall( + "exists", + [new (null, new AstExpressionBool(true))])); + } + + var operationParam = ParseExpression(operationFunc.ElementAt(0).Value); + + return new(operationName, new AstExpressionVariable(fieldName), operationParam); + } + + private static AstExpression ParseOperation(JsonObject json) + { + var funcName = json.ElementAt(0).Key; + if (!funcName.StartsWith("$")) + throw new ($"Operation name {funcName} must start with $"); + + if ( json.ElementAt(0).Value is not JsonArray funcParams || funcParams.Count == 0 ) + throw new($"Operation {funcName} parameters must be an array: {json?.ToJsonString()}"); + + if ( funcName == "$and" || funcName == "$or") + return ParseLogicalOperation(funcName, funcParams); + + if ( funcParams.Count != 2 ) + throw new($"Operation {funcName} must have 2 parameters"); + + var arg1 = ParseExpression(funcParams[0]); + var arg2 = ParseExpression(funcParams[1]); + + return new AstExpressionOperation(funcName, arg1, arg2); + } + + private static AstExpression ParseLogicalOperation(string funcName, JsonArray funcParams) + { + var args = funcParams.Select(ParseLogicalFuncArgument).ToList(); + if ( args.Count == 1 ) + return args[0]; + if ( args.Count == 2) + { + var isLogical0 = (args[0] as AstExpressionOperation) is { Operator: AstExpressionOperation.OperationType.AND } or { Operator: AstExpressionOperation.OperationType.OR }; + var isLogical1 = (args[1] as AstExpressionOperation) is { Operator: AstExpressionOperation.OperationType.AND } or { Operator: AstExpressionOperation.OperationType.OR }; + + var arg0 = isLogical0 ? new AstExpressionBrackets(args[0]) : args[0]; + var arg1 = isLogical1 ? new AstExpressionBrackets(args[1]) : args[1]; + return new AstExpressionOperation(funcName, arg0, arg1); + } + return new AstExpressionBrackets(Join(funcName, args)); + } + + private static AstExpressionOperation Join( string funcName, List args) + { + if ( args.Count < 2 ) + throw new($"Expecting at least 3 parameters for joining {funcName} operations"); + if ( args.Count == 2 ) + return new(funcName, args[0], args[1]); + + var op = new AstExpressionOperation(funcName, args[0], Join(funcName, [.. args.Skip(1)])); + return op; + } + + private static AstExpressionFunctionCall ParseFunctionCall(JsonObject json) + { + var funcName = json.ElementAt(0).Key; + if (!funcName.StartsWith("$")) + throw new ($"Function name \"{funcName}\" must start with $: {json?.ToJsonString()}"); + + var funcParams = json.ElementAt(0).Value; + + List namedParams = []; + List unnamedParams = []; + + if (funcParams is JsonValue jv) + unnamedParams.Add(new (null, ParseExpression(jv))); + if (funcParams is JsonArray ja) + unnamedParams.AddRange(ja.Select(x => new AstFunctionArgument(null, ParseExpression(x)))); + if (funcParams is JsonObject jo) + { + foreach ( var arg in jo) + { + if ( arg.Value is JsonArray arrArg ) + { + var arrayMembers = new List(); + foreach (var member in arrArg) + arrayMembers.Add(ParseExpression(member)); + + namedParams.Add(new(arg.Key, new AstExpressionArray(arrayMembers))); + } + else + { + if (arg.Key.StartsWith("$") ) + { + var funcObj = new JsonObject([new(arg.Key, arg.Value?.DeepClone())]); + unnamedParams.Add(new(null, ParseFunctionCall(funcObj))); + continue; + } + + if (arg.Value is JsonObject inner) + { + if (inner.Count == 1 && inner.ElementAt(0).Key.StartsWith('$') ) + { + //var funcObj = new JsonObject([new(inner.ElementAt(0).Key, inner.ElementAt(0).Value?.DeepClone())]); + namedParams.Add(new(arg.Key, ParseExpression(inner))); + continue; + + } + } + + var expr = ParseExpression(arg.Value); + if ( expr is AstExpressionFunctionCall ) + unnamedParams.Add(new (null, expr)); + else + namedParams.Add(new(arg.Key, expr)); + } + } + } + + return new(funcName, unnamedParams.Concat( namedParams )); + } + + /// + /// Special case for $and and $or - their arguments can be like { aaa : 1} which means "a == 1". + /// + private static AstExpression ParseLogicalFuncArgument(JsonNode? json) + { + if (json is JsonObject jo && !jo.ElementAt(0).Key.StartsWith('$')) + { + var projectionOperation = TryParseProjectionOperation(json); + if ( projectionOperation != null ) + return projectionOperation; + + if ( jo.ElementAt(0).Value is JsonValue ) + { + // simple equality + var right = ParseExpression(jo.ElementAt(0).Value); + return new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(jo.ElementAt(0).Key), right); + } + else if ( jo.ElementAt(0).Value is JsonObject joRight ) + { + // projection https://www.mongodb.com/docs/manual/reference/operator/query/ + // only $exists is supported. + if (joRight.ElementAt(0).Key == "$exists" && joRight.ElementAt(0).Value is JsonValue) + return new AstExpressionExists(jo.ElementAt(0).Key, joRight.ElementAt(0).Value!.GetValue()); + + // full range of projections is not supported. + //return new AstExpressionProjection(jo.ElementAt(0).Key, joRight); + var right = ParseExpression(jo.ElementAt(0).Value); + return new AstExpressionOperation(AstExpressionOperation.OperationType.EQ, new AstExpressionVariable(jo.ElementAt(0).Key), right); + } + } + return ParseExpression(json); + } + + private static AstStage ParseProject(JsonObject body) + { + var fields = new List(); + var idFields = new List(); + + AstLet? let; + + var exclude = false; + foreach (var field in body) + { + if ( field is { Key: "_id", Value: JsonObject idObj }) + { + foreach (var idField in idObj) + { + (let, _) = ParseLet(idField); + idFields.Add(let); + } + exclude = false; + continue; + } + + bool exc; + + (let, exc) = ParseLet(field); + exclude = exc; + fields.Add(let); + } + + var stage = new AstStageProject(idFields, fields, exclude); + return stage; + } + + private static AstStage ParseReplaceWith(JsonObject body) + { + var fields = new List(); + var idFields = new List(); + + AstLet? let; + + foreach (var field in body) + { + if ( field is { Key: "_id", Value: JsonObject idObj }) + { + foreach (var idField in idObj) + { + (let, _) = ParseLet(idField); + idFields.Add(let); + } + continue; + } + + (let, _) = ParseLet(field); + fields.Add(let); + } + + var stage = new AstStageReplace(idFields, fields); + return stage; + } + + private static (AstLet, bool) ParseLet(KeyValuePair field) + { + var exclude = false; + + if ( field.Value is JsonArray ja ) + return (ParseArrayProjection(field.Key, ja),false); + if ( field.Value is JsonObject { Count: > 0 } jo && !jo.ElementAt(0).Key.StartsWith('$') ) + return (ParseObjectProjection(field.Key, jo),false); + + var expression = ParseExpression(field.Value); + AstLet let; + + // if any argument is like aaa : 0 this means PROJECT EXCLUDE aaa + if (expression is AstExpressionNumber { IsLong: true } en) + { + if (en.LongValue == 0) + exclude = true; + let = new AstLetExpression(new AstExpressionVariable(field.Key)); + } + else + let = new AstLetExpression(expression, field.Key); + return (let, exclude); + } + + private static AstLetArray ParseArrayProjection(string name, JsonArray ja) + { + var fields = new List(); + + foreach ( var o in ja.OfType() ) + { + var singleLet = ParseLet(new("", o)).Item1; + fields.Add(singleLet); + } + var res = new AstLetArray(name, fields, true); + return res; + } + + private static AstLetArray ParseObjectProjection(string name, JsonObject jo) + { + var fields = new List(); + + foreach (var pair in jo) + { + var singleLet = ParseLet(pair).Item1; + fields.Add(singleLet); + } + var res = new AstLetArray(name, fields, false); + return res; + } + + private static AstStage ParseAddFields(JsonObject body) + { + var fields = new List(); + + var exclude = false; + foreach (var field in body) + { + if ( field.Value is JsonArray ja ) + { + var arrayProjection = ParseArrayProjection(field.Key, ja); + fields.Add(arrayProjection); + } + else + { + var (let, exc) = ParseLet(field); + exclude = exc; + fields.Add(let); + } + } + + var stage = new AstStageAddFields(fields); + return stage; + } + + private static AstStage ParseBucket(JsonObject body) + { + var stage = new AstStageBucket(); + + foreach (var field in body) + { + switch (field) + { + case { Key: "groupBy", }: + stage.GroupBy = ParseExpression(field.Value); + break; + case { Key: "default", Value: JsonValue val }: + stage.DefaultBucket = val.ToString(); + break; + case { Key: "boundaries", Value: JsonArray arr }: + { + foreach (var v in arr.Select(x => x?.ToString()).Where(x => x != null)) + stage.AddBucket(v!.Trim('"')); + break; + } + case { Key: "output", Value: JsonObject output }: + { + foreach (var let in output) + { + if (let.Value is JsonArray ja) + { + var arrayProjection = ParseArrayProjection(let.Key, ja); + stage.Add(arrayProjection); + } + else + { + var (let1, _) = ParseLet(let); + stage.Add(let1); + } + } + + break; + } + } + } + + return stage; + } + + private static AstStage ParseBucketAuto(JsonObject body) + { + var stage = new AstStageBucket() + { + Auto = true + }; + + foreach (var field in body) + { + switch (field) + { + case { Key: "groupBy", }: + stage.GroupBy = ParseExpression(field.Value); + break; + case { Key: "buckets", Value: JsonValue val }: + stage.NumberOfBuckets = val.GetValue(); + break; + case { Key: "granularity", Value: JsonValue val }: + stage.Granularity = val.ToString(); + break; + case { Key: "output", Value: JsonObject output }: + { + foreach (var let in output) + { + if (let.Value is JsonArray ja) + { + var arrayProjection = ParseArrayProjection(let.Key, ja); + stage.Add(arrayProjection); + } + else + { + var (let1, _) = ParseLet(let); + stage.Add(let1); + } + } + + break; + } + } + } + + return stage; + } + + private static AstStage ParseFacet(JsonObject body) + { + var stage = new AstStageFacet(); + + foreach (var (name, value) in body) + { + var pipeLine = ParsePipeline(value as JsonArray); + + stage.Add( new AstNamedPipeline(name, new (pipeLine))); + } + + return stage; + } + + private static List ParsePipeline(JsonArray? json) + { + var pipeline = new List(); + if (json == null) + return pipeline; + + foreach ( var stageJson in json.OfType()) + { + var stage = ParseStage(stageJson); + pipeline.Add(stage); + } + return pipeline; + } +} diff --git a/Rms.Risk.Mango.Language/Parsers/ErrorListener.cs b/Rms.Risk.Mango.Language/Parsers/ErrorListener.cs new file mode 100644 index 0000000..9dfd37f --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/ErrorListener.cs @@ -0,0 +1,36 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Antlr4.Runtime; + +namespace Rms.Risk.Mango.Language.Parsers; + +internal class ErrorListener(string component) : IAntlrErrorListener +{ + private string Component { get; } = component; + + public void SyntaxError( TextWriter output, + IRecognizer recognizer, + T offendingSymbol, + int line, + int charPositionInLine, + string msg, + RecognitionException e + ) => + throw new SyntaxErrorException(Component, line, charPositionInLine, msg, offendingSymbol!, e); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/Parsers/JsonListenerHelper.cs b/Rms.Risk.Mango.Language/Parsers/JsonListenerHelper.cs new file mode 100644 index 0000000..a6bd3d8 --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/JsonListenerHelper.cs @@ -0,0 +1,109 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Parsers; + +public static class JsonListenerHelper +{ + public static JsonNode Convert(MongoAggregationForHumansParser.JsonContext context) + { + if (context.@object() != null) + { + return ConvertToJsonObject(context.@object()); + } + else if (context.array() != null) + { + return ConvertToJsonArray(context.array()); + } + throw new($"Invalid json: {context?.GetType().Name}: {context}"); + } + + private static JsonNode? ConvertToJsonValue(MongoAggregationForHumansParser.ValueContext context) + { + if (context.VARIABLE() != null) + { + return JsonValue.Create(context.VARIABLE().GetText().Trim('"')); + } + else if (context.STRING() != null) + { + return JsonValue.Create(context.STRING().GetText().Trim('"')); + } + else if (context.NUMBER() != null) + { + var d = double.Parse(context.NUMBER().GetText()); + if ( d == Math.Floor(d) ) + { + if ( d >= int.MinValue || d <= int.MaxValue ) + return JsonValue.Create((int)d); + return JsonValue.Create((long)d); + } + else + return JsonValue.Create(d); + } + else if (context.GetText() == "true") + { + return JsonValue.Create(true); + } + else if (context.GetText() == "false") + { + return JsonValue.Create(false); + } + else if ( context.@object() != null ) + { + return ConvertToJsonObject(context.@object()); + } + else if ( context.array() != null ) + { + return ConvertToJsonArray(context.array()); + } + else if (context.GetText() == "null") + { + return null; + } + throw new($"Invalid json value: {context?.GetType().Name}: {context}"); + } + + private static JsonArray ConvertToJsonArray(MongoAggregationForHumansParser.ArrayContext context) + { + var array = new JsonArray(); + foreach (var value in context.value()) + { + array.Add(ConvertToJsonValue(value)); + } + return array; + } + + private static JsonObject ConvertToJsonObject(MongoAggregationForHumansParser.ObjectContext context) + { + var pairs = new JsonObject(); + foreach (var pair in context.pair()) + { + pairs.Add(ConvertToJsonPair(pair)); + } + return pairs; + } + + private static KeyValuePair ConvertToJsonPair(MongoAggregationForHumansParser.PairContext context) + { + var name = context.object_name().GetText().Trim('"'); + var value = ConvertToJsonValue(context.value()); + return new(name, value); + } + + +} diff --git a/Rms.Risk.Mango.Language/Parsers/ListenerHelper.cs b/Rms.Risk.Mango.Language/Parsers/ListenerHelper.cs new file mode 100644 index 0000000..d17811a --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/ListenerHelper.cs @@ -0,0 +1,346 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Parser =Rms.Risk.Mango.Language.MongoAggregationForHumansParser; + +namespace Rms.Risk.Mango.Language.Parsers; + +internal static class ListenerHelper +{ + public static List BuildLet(Parser.Let_listContext? context) + { + var lets = new List(); + if ( context == null ) + return lets; + + foreach (var let in context.let_list().SelectMany(BuildLet)) + { + lets.Add(let); + } + if (context.let_list_item() != null) + { + var let = BuildSingleLet(context.let_list_item()); + lets.Add(let); + } + return lets; + + static AstLet BuildSingleLet(Parser.Let_list_itemContext let) + { + switch (let) + { + //case Parser.LetVariableContext vc: + // return new AstLetExpression(new AstExpressionVariable(vc.VARIABLE().GetText())); + case Parser.LetExpressionAsContext ec: + { + var expression = BuildAst(ec.expression()); + + var name = ec.STRING()?.GetText()?.Trim('"'); + if (string.IsNullOrWhiteSpace(name)) + name = ec.VARIABLE()?.GetText(); + + if (string.IsNullOrWhiteSpace(name) && expression is AstExpressionString se && se.Value.StartsWith("$")) + expression = new AstExpressionVariable(se.Value[1..]); + + if (string.IsNullOrWhiteSpace(name) && expression is not AstExpressionVariable) + throw new($"Only plain variable allowed to be unnamed. Please add 'AS'. Got {ec.GetText()} ({expression.GetType()})"); + return new AstLetExpression(expression, name); + } + + case Parser.LetArrayContext ac: + return ParseLetArray(ac); + case Parser.LetObjectContext oc: + return ParseLetObject(oc); + default: + throw new($"Invalid let: {let.GetType().Name}: {let.GetText()}"); + } + } + } + + private static AstLetArray ParseLetObject(Parser.LetObjectContext oc) + { + var fields = new List(); + foreach (var field in oc.let_list().let_list()) + { + var x = BuildLet(field); + fields.AddRange(x); + } + return new (oc.VARIABLE()?.GetText(), fields, false); + } + + private static AstLetArray ParseLetArray(Parser.LetArrayContext ac) + { + var fields = new List(); + foreach (var field in ac.let_list().let_list()) + { + var x = BuildLet(field); + fields.AddRange(x); + } + return new ((ac.VARIABLE() ?? ac.STRING())?.GetText(), fields, true); + } + + public static List BuildSortFieldList(Parser.Sort_var_listContext context) + { + var lets = new List(); + foreach (var let in context.sort_var_list().SelectMany(BuildSortFieldList)) + { + lets.Add(let); + } + if (context.VARIABLE() != null) + { + var let = new AstSortField(context.VARIABLE().GetText(), AstSortField.SortOrder.Ascending); + lets.Add(let); + } + return lets; + } + + public static List BuildVarList(Parser.Var_listContext context) + { + var lets = new List(); + foreach (var let in context.var_list().SelectMany(BuildVarList)) + { + lets.Add(let); + } + if (context.VARIABLE() != null) + { + var let = new AstLetExpression(new AstExpressionVariable(context.VARIABLE().GetText())); + lets.Add(let); + } + return lets; + } + + public static List BuildEquivalence(Parser.Equivalence_listContext context) + { + var eqs = new List(); + if (context is Parser.VarEquivalenceContext vc) + { + var left = new AstExpressionVariable(vc.left.Text); + var right = new AstExpressionVariable(vc.right.Text); + eqs.Add(new(left, right)); + } + if (context is Parser.EquivalenceListContext ec) + { + foreach (var eq in ec.equivalence_list().SelectMany(BuildEquivalence)) + { + eqs.Add(eq); + } + } + return eqs; + } + + public static List BuildUnnamedArgumentsList(Parser.Unnamed_args_listContext? context) + { + var arguments = new List(); + foreach (var exp in context?.unnamed_args_list()?.SelectMany(BuildUnnamedArgumentsList) ?? []) + { + arguments.Add(exp); + } + if (context?.expression() != null) + { + var exp = BuildAst(context.expression()); + arguments.Add(new("", exp)); + } + else if (context?.expression_array() != null) + { + var exp = BuildArray(context.expression_array()); + arguments.Add(new("", exp)); + } + return arguments; + } + + public static AstExpressionArray BuildArray(Parser.Expression_arrayContext context) + { + var fields = new List(); + foreach (var field in context.expression_array_item()) + { + if ( field.expression() != null) + { + var exp = BuildAst(field.expression()); + fields.Add(new("", exp)); + } + else if ( field.expression_array() != null) + { + var exp = BuildArray(field.expression_array()); + fields.Add(new("", exp)); + } + } + return new AstExpressionArray(fields); + } + + public static List BuildNamedArgumentsList(Parser.Named_args_listContext? context) + { + var arguments = new List(); + if (context == null) + return arguments; + + arguments.AddRange(context.named_args_list()?.SelectMany(BuildNamedArgumentsList) ?? []); + if (context.expression() != null) + { + var exp = BuildAst(context.expression()); + arguments.Add(new(context.VARIABLE().GetText(), exp)); + } + else if (context.expression_array() != null) + { + var exp = BuildArray(context.expression_array()); + arguments.Add(new(context.VARIABLE().GetText(), exp)); + } + return arguments; + } + + public static AstExpression BuildAst(Parser.ExpressionContext context) + { + if ( context.comparizon_expression().Length == 1 ) + return BuildAst(context.comparizon_expression()[0]); + + var op = context.AND().Length > 0 + ? AstExpressionOperation.OperationType.AND + : context.OR().Length > 0 + ? AstExpressionOperation.OperationType.OR + : throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}"); + + var list = new List(); + foreach (var exp in context.comparizon_expression()) + { + list.Add(BuildAst(exp)); + } + return new AstExpressionOperation(op, list); + } + + public static AstExpression BuildAst(Parser.Comparizon_expressionContext context) + { + if ( context.additive_expression().Length == 1 ) + return BuildAst(context.additive_expression()[0]); + + var op = + context.LT().Length > 0 + ? AstExpressionOperation.OperationType.LT + : context.GT().Length > 0 + ? AstExpressionOperation.OperationType.GT + : context.EQ().Length > 0 + ? AstExpressionOperation.OperationType.EQ + : context.NEQ().Length > 0 + ? AstExpressionOperation.OperationType.NEQ + : context.LTE().Length > 0 + ? AstExpressionOperation.OperationType.LTE + : context.GTE().Length > 0 + ? AstExpressionOperation.OperationType.GTE + : throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}"); + + var list = new List(); + foreach (var exp in context.additive_expression()) + { + list.Add(BuildAst(exp)); + } + return new AstExpressionOperation(op, list); + } + + public static AstExpression BuildAst(Parser.Additive_expressionContext context) + { + if ( context.multiplicative_expression().Length == 1 ) + return BuildAst(context.multiplicative_expression()[0]); + + var op = + context.PLUS().Length > 0 + ? AstExpressionOperation.OperationType.PLUS + : context.MINUS().Length > 0 + ? AstExpressionOperation.OperationType.MINUS + : throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}"); + + var list = new List(); + foreach (var exp in context.multiplicative_expression()) + { + list.Add(BuildAst(exp)); + } + return new AstExpressionOperation(op, list); + } + + public static AstExpression BuildAst(Parser.Multiplicative_expressionContext context) + { + if ( context.unary_expression().Length == 1 ) + return BuildAst(context.unary_expression()[0]); + + var op = + context.MUL().Length > 0 + ? AstExpressionOperation.OperationType.MULTIPLY + : context.DIV().Length > 0 + ? AstExpressionOperation.OperationType.DIVIDE + : throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}"); + + var list = new List(); + foreach (var exp in context.unary_expression()) + { + list.Add(BuildAst(exp)); + } + return new AstExpressionOperation(op, list); + } + + public static AstExpression BuildAst(Parser.Unary_expressionContext context) + { + var expr = context switch + { + Parser.UnaryExpressionContext unary => + new AstExpressionUnary( + unary.NOT() != null + ? AstExpressionUnary.OperationType.NOT + : unary.MINUS() != null + ? AstExpressionUnary.OperationType.MINUS + : unary.PLUS() != null + ? AstExpressionUnary.OperationType.PLUS + : throw new($"Invalid operation: {context?.GetType().Name}: {context?.GetText()}") + , BuildAst(unary.unary_expression())), + Parser.PrimaryExpressionContext brackets => BuildAst(brackets.brackets_expression()), + _ => throw new($"Invalid expression: {context?.GetType().Name}: {context?.GetText()}") + }; + + return expr; + } + + public static AstExpression BuildAst(Parser.Brackets_expressionContext context) + { + var expr = context switch + { + Parser.AtomExpressionContext atom => BuildAtom(atom.atom()), + Parser.BracketsExpressionContext brackets => BuildAst(brackets.expression()), + Parser.InExpressionContext @in => new AstExpressionIn(@in.VARIABLE().GetText(), @in.NOT() != null, @in.expression().Select(BuildAst)), + Parser.FuncExpressionContext func => new AstExpressionFunctionCall(func.VARIABLE().GetText(), BuildUnnamedArgumentsList(func.unnamed_args_list()).Concat( BuildNamedArgumentsList(func.named_args_list())) ), + Parser.ProjectionExpressionContext proj => new AstExpressionProjection(proj.VARIABLE().GetText(), JsonListenerHelper.Convert(proj.json())), + Parser.ExistsExpressionContext exists => new AstExpressionExists(exists.VARIABLE().GetText(), exists.NOT() == null), + + _ => throw new($"Invalid expression: {context?.GetType().Name}: {context}") + }; + + return expr; + + static AstExpression BuildAtom(Parser.AtomContext atom) + { + if (atom.STRING() != null) + return new AstExpressionString(atom.STRING().GetText()); + else if (atom.NUMBER() != null) + return new AstExpressionNumber(atom.NUMBER().GetText()); + else if (atom.GetText() == "true") + return new AstExpressionBool(true); + else if (atom.GetText() == "false") + return new AstExpressionBool(false); + else if (atom.GetText() == "null") + return new AstExpressionNull(); + else if (atom.VARIABLE() != null) + return new AstExpressionVariable(atom.VARIABLE().GetText()); + throw new($"Invalid atom: {atom?.GetType().Name}: {atom}"); + } + + } +} diff --git a/Rms.Risk.Mango.Language/Parsers/MongoGrammarListener.cs b/Rms.Risk.Mango.Language/Parsers/MongoGrammarListener.cs new file mode 100644 index 0000000..416d41f --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/MongoGrammarListener.cs @@ -0,0 +1,363 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Antlr4.Runtime.Misc; +using Parser=Rms.Risk.Mango.Language.MongoAggregationForHumansParser; + +namespace Rms.Risk.Mango.Language.Parsers; + +public class MongoGrammarListener : MongoAggregationForHumansBaseListener +{ + private AstPipeline _currentPipeline = AstPipeline.None; + + private Stack _pipelines = new(); + private Stack _stage = new(); + + public AstAggregation? Aggregate { get; private set;} + + private void Clear() + { + _currentPipeline = AstPipeline.None; + _pipelines.Clear(); + _stage.Clear(); + } + + public override void EnterFile(Parser.FileContext context) + { + Clear(); + var collection = context.STRING().GetText(); + Aggregate = new(collection); + } + + public override void ExitFile(Parser.FileContext context) + { + Aggregate!.Add(_currentPipeline); + + if ( ReferenceEquals( _currentPipeline, AstPipeline.None ) ) + throw new("Invalid _currentPipeline"); + + if ( _pipelines.Count != 1 ) + throw new($"Invalid _pipelines Count={_pipelines.Count}"); + if ( _stage.Count != 0 ) + throw new($"Invalid _stage Count={_stage.Count}"); + } + + public override void EnterPipeline_def(MongoAggregationForHumansParser.Pipeline_defContext context) + { + _pipelines.Push(_currentPipeline); + _currentPipeline = new(); + + // stages are responsible for popping it back + } + + public override void ExitStage_def(Parser.Stage_defContext context) + { + var stage = _currentPipeline.Stages.Last(); + if ( context.json() != null ) + stage.Options = (JsonObject)JsonListenerHelper.Convert(context.json()); + } + + public override void EnterMatch_def(Parser.Match_defContext context) + { + var stage = new AstStageWhere(); + _stage.Push(stage); + } + + public override void ExitMatch_def(Parser.Match_defContext context) + { + var stage = _stage.Pop(); + var expression = ListenerHelper.BuildAst(context.expression()); + stage.Add(expression); + _currentPipeline.Add(stage); + } + + public override void EnterBucketPlain(Parser.BucketPlainContext context) + { + var stage = new AstStageBucket(); + _stage.Push(stage); + } + + public override void ExitBucketPlain(Parser.BucketPlainContext context) + { + var stage = (AstStageBucket)_stage.Pop(); + + stage.GroupBy = ListenerHelper.BuildAst(context.expression()); + + foreach (var bound in context.NUMBER().Select(x => x.GetText()).Where(x => !string.IsNullOrWhiteSpace(x))) + { + stage.AddBucket(bound.Trim('"')); + } + + if (context.defaultBucket != null) + { + stage.DefaultBucket = context.defaultBucket.Text.Trim('"'); + } + + var fields = ListenerHelper.BuildLet(context.let_list()); + foreach (var field in fields) + stage.Add(field); + _currentPipeline.Add(stage); + } + + public override void EnterBucketAuto(Parser.BucketAutoContext context) + { + var stage = new AstStageBucket() + { + Auto = true + }; + _stage.Push(stage); + } + + public override void ExitBucketAuto(Parser.BucketAutoContext context) + { + var stage = (AstStageBucket)_stage.Pop(); + + stage.GroupBy = ListenerHelper.BuildAst(context.expression()); + stage.NumberOfBuckets = int.Parse(context.NUMBER().GetText()); + stage.Granularity = context.STRING()?.GetText().Trim('"'); + + var fields = ListenerHelper.BuildLet(context.let_list()); + foreach (var field in fields) + stage.Add(field); + + _currentPipeline.Add(stage); + } + + public override void EnterAddfields_def(Parser.Addfields_defContext context) + { + var stage = new AstStageAddFields([]); + _stage.Push(stage); + } + + public override void ExitAddfields_def(Parser.Addfields_defContext context) + { + var stage = (AstStageAddFields)_stage.Pop(); + var fields = ListenerHelper.BuildLet(context.let_list()); + foreach (var field in fields) + stage.Add(field); + _currentPipeline.Add(stage); + } + + public override void EnterProjectInclude(Parser.ProjectIncludeContext context) + { + var stage = new AstStageProject(); + _stage.Push(stage); + } + + public override void ExitProjectInclude(Parser.ProjectIncludeContext context) + { + var stage = (AstStageProject)_stage.Pop(); + + var idLetList = context.id_list; + var letList = context.data_list; + + var idFields = ListenerHelper.BuildLet(idLetList); + foreach (var field in idFields) + stage.AddId(field); + + var fields = ListenerHelper.BuildLet(letList); + foreach (var field in fields) + stage.Add(field); + _currentPipeline.Add(stage); + } + + public override void EnterProjectExclude(Parser.ProjectExcludeContext context) + { + var stage = new AstStageProject(); + _stage.Push(stage); + } + + public override void ExitProjectExclude(Parser.ProjectExcludeContext context) + { + var stage = (AstStageProject)_stage.Pop(); + stage.Exclude = true; + var fields = ListenerHelper.BuildVarList(context.var_list()); + foreach (var field in fields) + stage.Add(field); + _currentPipeline.Add(stage); + } + + public override void EnterReplace_def(Parser.Replace_defContext context) + { + var stage = new AstStageReplace(); + _stage.Push(stage); + } + + public override void ExitReplace_def(Parser.Replace_defContext context) + { + var stage = (AstStageReplace)_stage.Pop(); + + var idLetList = context.id_list; + var letList = context.data_list; + + var idFields = ListenerHelper.BuildLet(idLetList); + foreach (var field in idFields) + stage.AddId(field); + + var fields = ListenerHelper.BuildLet(letList); + foreach (var field in fields) + stage.Add(field); + _currentPipeline.Add(stage); + } + + public override void EnterGroup_by_def(Parser.Group_by_defContext context) + { + var stage = new AstStageGroupBy([], []); + _stage.Push(stage); + } + + public override void ExitGroup_by_def(Parser.Group_by_defContext context) + { + var stage = (AstStageGroupBy)_stage.Pop(); + var id = ListenerHelper.BuildLet(context.id_list); + var fields = ListenerHelper.BuildLet(context.data_list); + + foreach (var field in fields) + stage.Add(field); + foreach (var x in id) + stage.AddId(x); + + _currentPipeline.Add(stage); + } + + public override void EnterSort_def(Parser.Sort_defContext context) + { + var stage = new AstStageSortBy(); + _stage.Push(stage); + } + + public override void ExitSort_def(Parser.Sort_defContext context) + { + var stage = (AstStageSortBy)_stage.Pop(); + foreach (var field in ListenerHelper.BuildSortFieldList(context.sort_var_list())) + stage.Add(field); + + _currentPipeline.Add(stage); + } + + public override void EnterUnwind_def(Parser.Unwind_defContext context) + { + var stage = new AstStageUnwind(); + _stage.Push(stage); + } + + public override void ExitUnwind_def(Parser.Unwind_defContext context) + { + var stage = (AstStageUnwind)_stage.Pop(); + stage.Name = context.VARIABLE()[0].GetText(); + + stage.Index = context.VARIABLE().Length > 1 ? context.VARIABLE()[1].GetText() : null; + + + _currentPipeline.Add(stage); + } + + public override void EnterJoin_def(Parser.Join_defContext context) + { + var stage = new AstStageJoin(); + _stage.Push(stage); + } + + public override void ExitJoin_def(Parser.Join_defContext context) + { + var stage = (AstStageJoin)_stage.Pop(); + + var pipelineExists = context.pipeline_def() != null; + + AstPipeline pipeline; + if (pipelineExists) + { + pipeline = _currentPipeline; + _currentPipeline = _pipelines.Pop(); + } + else + pipeline = AstPipeline.None; + + var on = ListenerHelper.BuildEquivalence(context.equivalence_list()); + var collection = context.STRING()[0].GetText(); + + var asField = + context.VARIABLE()?.GetText() + ?? context.STRING()[1].GetText() + ?? throw new ($"AS clause required for join: {context.GetText()}"); + + var let = ListenerHelper.BuildLet(context.let_list()); + + stage.Init(collection, asField, on, let, !pipelineExists || pipeline.Count == 0 ? null : pipeline); + _currentPipeline.Add(stage); + } + + public override void EnterFacet_def(Parser.Facet_defContext context) + { + var stage = new AstStageFacet(); + _stage.Push(stage); + } + + public override void ExitFacet_def(Parser.Facet_defContext context) + { + var stage = (AstStageFacet)_stage.Pop(); + var pipelines = new List{_currentPipeline}; + + if (context.pipeline_def().Length != context.VARIABLE().Length) + throw new($"context.pipeline_def().Length={context.pipeline_def().Length} != context.VARIABLE().Length={context.VARIABLE().Length}"); + + if (_pipelines.Count <= context.pipeline_def().Length) + throw new($"_pipelines.Count={_pipelines.Count} but context.pipeline_def().Length={context.pipeline_def().Length}"); + + for (var i = 0; i < context.pipeline_def().Length-1; i++) + { + pipelines.Insert(0, _pipelines.Pop()); + } + + for (var i = 0; i < pipelines.Count; i++) + { + var name = context.VARIABLE(i).GetText(); + var pipeline = pipelines[i]; + var namedPipeline = new AstNamedPipeline(name, pipeline); + + stage.Add( namedPipeline ); + } + + if (_pipelines.Count < 1) + throw new($"Invalid _pipelines Count={_pipelines.Count}"); + + _currentPipeline = _pipelines.Pop(); + + _currentPipeline.Add(stage); + } + + public override void EnterDo_def([NotNull] Parser.Do_defContext context) + { + var stage = new AstStageDo(); + _stage.Push(stage); + } + + public override void ExitDo_def([NotNull] Parser.Do_defContext context) + { + var stage = (AstStageDo)_stage.Pop(); + + var json = JsonListenerHelper.Convert(context.json()); + stage.Json = json; + + _currentPipeline.Add(stage); + } + + + +} + diff --git a/Rms.Risk.Mango.Language/Parsers/SyntaxErrorException.cs b/Rms.Risk.Mango.Language/Parsers/SyntaxErrorException.cs new file mode 100644 index 0000000..06b3084 --- /dev/null +++ b/Rms.Risk.Mango.Language/Parsers/SyntaxErrorException.cs @@ -0,0 +1,76 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Language.Parsers; + +/// +/// Script parsing syntax error +/// +public class SyntaxErrorException : Exception +{ + /// + /// Line number + /// + public int Line { get; } + /// + /// Position within the line + /// + public int Position { get; } + /// + /// Offending symbol + /// + public object? Symbol { get; } + + /// + /// Source of error + /// + public string Component { get; } + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + public SyntaxErrorException( string component, int line, int pos, string message, object? symbol = null, Exception? innerException = null ) + : base($"Syntax error in {component}: line {line} position {pos}: {message}", innerException) + { + Line = line; + Position = pos; + Symbol = symbol; + Component = component; + } + + /// + /// Constructor + /// + /// + /// + /// + public SyntaxErrorException( string component, string message, Exception? innerException = null ) + : base($"Syntax error in {component}: {message}", innerException) + { + Line = 0; + Position = 0; + Symbol = null; + Component = component; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/README.md b/Rms.Risk.Mango.Language/README.md new file mode 100644 index 0000000..c0874a8 --- /dev/null +++ b/Rms.Risk.Mango.Language/README.md @@ -0,0 +1,39 @@ +# AFH language + +See [documentation](../Rms.Risk.Mango/wwwroot/docs/afh-overview.md). + +# Compiling + +Unfortunately there is something is wrong with `Antlr4` tooling. According to https://github.com/kaby76/Antlr4BuildTasks/blob/master/Readme.md the +project is setup correctly however we are experiencing the following error: + +> ANT02: Went through the complete probe list looking for an Antlr4 tool jar, but could not find anything + +To workaround it you can use one of the options: + +1. To download it automatically during the build, uncomment this section within Rms.Risk.Mango.Language.csproj : + ``` + + ``` + +2. Download `jar` https://www.antlr.org/download.html (direct link https://www.antlr.org/download/antlr-4.13.2-complete.jar) + and put it into `$(SolutionDirectory)Resources/Antlr4` folder. Create one if needed. + +Now it should work. \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/Rms.Risk.Mango.Language.csproj b/Rms.Risk.Mango.Language/Rms.Risk.Mango.Language.csproj new file mode 100644 index 0000000..86e34b5 --- /dev/null +++ b/Rms.Risk.Mango.Language/Rms.Risk.Mango.Language.csproj @@ -0,0 +1,45 @@ + + + CS1584,CS1658,CS1591 + + + + + + + Rms.Risk.Mango.Language + $(SolutionDirectory)tools/Antlr4/antlr4-4.13.1-complete.jar + $(JAVA_HOME)/bin/java.exe + $(JAVA_HOME)/bin/java + + + Rms.Risk.Mango.Language + $(SolutionDirectory)tools/Antlr4/antlr4-4.13.1-complete.jar + $(JAVA_HOME)/bin/java.exe + $(JAVA_HOME)/bin/java + + + + + + + + + \ No newline at end of file diff --git a/Rms.Risk.Mango.Language/_imports.cs b/Rms.Risk.Mango.Language/_imports.cs new file mode 100644 index 0000000..c740561 --- /dev/null +++ b/Rms.Risk.Mango.Language/_imports.cs @@ -0,0 +1,23 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Nodes; +global using Rms.Risk.Mango.Language.Ast; +global using Rms.Risk.Mango.Language.Parsers; \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs new file mode 100644 index 0000000..b4923a4 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs @@ -0,0 +1,331 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics; +using Newtonsoft.Json; +using System.Text.RegularExpressions; +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core; + +[BsonSerializer( typeof(ArrayBasedPivotDataSerializer))] +[JsonConverter( typeof(ArrayBasedPivotDataJsonSerializer))] +public class ArrayBasedPivotData : IPivotedData +{ + private readonly List _headers; + private List<(string OrigHeader, string DisplayHeader)>? _headersMap; + private readonly List _realData = []; + private readonly Dictionary _columnTypesCache = []; + private readonly List _displayHeaders = []; + private int[]? _columnMap; // displayPos -> physicalPos in _headers + + + public static readonly ArrayBasedPivotData NoData = new(["No data"]) { Id = "no_data" }; + + + public IReadOnlyCollection Headers + { + get + { + if ( _displayHeaders.Count > 0 && _displayHeaders.Count == _headers.Count) + return _displayHeaders; + + if ( !(_columnMap?.Length > 0) ) + return _headers; + + _displayHeaders.Clear(); + + _displayHeaders.AddRange( + _columnMap + .Select((physicalPos, displayPos) => (physicalPos, displayPos)) + .OrderBy(x => x.displayPos) + .Select( x => _headers[x.physicalPos]) + ); + +// _displayHeaders.AddRange( +// _headers +// .Select((x, i) => (Name: x, Order: Array.IndexOf(_columnMap, i, 0))) +// .OrderBy(x => x.Order) +// .Select(x => x.Name) +// .ToList() +// ); + + return _displayHeaders; + } + } + + public IReadOnlyCollection<(string OrigHeader, string DesplayHeader)> HeadersMap => _headersMap ?? []; + + public void UpdateHeaders(Func changeColumnName) + { + // make a copy or original headers + if ( _headersMap != null ) + throw new ApplicationException("UpdateHeaders can only be called once"); + _headersMap = []; + + for (var i = 0; i < _headers.Count; i++ ) + { + var newName = changeColumnName(_headers[i]); + _headersMap.Add((OrigHeader:_headers[i], DisplayHeader: newName)); + _headers[i] = newName; + } + + _displayHeaders.Clear(); + } + + public string Id { get; set; } = ""; + public DateTime ExpireAt { get; set; } + + public int Count => _realData.Count; + + public object? Get(int displayCol, int row) => this[displayCol, row]; + + public object? this[int displayCol, int row] + { + get + { + if (row >= _realData.Count) + throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}"); + + var physicalCol = _columnMap != null && displayCol < _columnMap.Length + ? _columnMap[displayCol] + : displayCol; + + return physicalCol >= _realData[row].Length + ? null + : _realData[row][physicalCol]; + } + set + { + if (row >= _realData.Count) + throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}"); + + var physicalCol = _columnMap != null && displayCol < _columnMap.Length + ? _columnMap[displayCol] + : displayCol; + + if (physicalCol >= _realData[row].Length) + throw new ArgumentException($"Col={row} is greater than Count={_realData[row].Length} or Row={row}"); + _realData[row][physicalCol] = value; + } + } + + public ArrayBasedPivotData(IEnumerable headers) + { + _headers = headers.ToList(); + } + + public ArrayBasedPivotData(IPivotedData other) + : this( other.Headers ) + { + var len = other.Headers.Count; + var o = new object?[len]; + + for ( var row = 0; row < other.Count; row++) + { + for (var col = 0; col < len; col++) + { + o[col] = other.Get(col, row); + } + Add(o); + } + } + + private ArrayBasedPivotData( + IEnumerable headers, + int[]? columnMap + ) + { + _headers = headers.ToList(); + _columnMap = columnMap == null ? null : [.. columnMap]; // make a copy + } + + public void Add( IEnumerable data ) + { + var row = new object?[_headers.Count]; + var i = 0; + foreach ( var o in data ) + { + if ( i >= _headers.Count ) + throw new ArgumentException($"Length of supplied data must be at least {_headers.Count}", nameof(data)); + row[i++] = o; + } + + _realData.Add( row ); + } + + public void AddHeader(string header) + { + _headers.Add(header); + _displayHeaders.Clear(); + _columnMap = null; + } + + public bool Contains( string header ) => _headers.Any( x => x == header ); + + public Type GetColumnType(int displayCol) + { + if ( _columnTypesCache.TryGetValue( displayCol, out var t ) ) + return t; + t = DetectColumnType( displayCol ); + _columnTypesCache[displayCol] = t; + return t; + } + + private Type DetectColumnType(int displayCol) + { + var guessDouble = 0; + var guessLong = 0; + var guessInt = 0; + var guessDec = 0; + + for ( var i = 0; i < Math.Min( 200, Count); i++ ) + { + var o = this[displayCol, i]; + if (o == null) + continue; + + if ( o is double ) + guessDouble += 1; + else if (o is int) + guessInt += 1; + else if (o is long) + guessLong += 1; + else if (o is decimal) + guessDec += 1; + } + + if (guessDec > guessDouble && guessDec > guessLong && guessDec > guessInt) + return typeof(decimal); + if (guessDouble > guessLong && guessDouble > guessInt) + return typeof(double); + if (guessLong > guessInt) + return typeof(long); + if (guessInt > 0) + return typeof(int); + + return typeof(string); + } + + public void ReorderColumns( IReadOnlyCollection columnsOrder ) + { + _displayHeaders.Clear(); + + if ( !(columnsOrder?.Count > 0) ) + { + _columnMap = []; + return; + } + + var src = new List( _headers ); + var colMap = new List(); // position = display col / value = real col number + + foreach ( var regex in columnsOrder.Select( x => new Regex( x, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline ) ).ToArray() ) + { + var alpha = new List(); // for alpha sorting + foreach ( var name in src ) + { + var m = regex.Match( name ); + if ( !m.Success ) + continue; + alpha.Add( name ); + } + + alpha.Sort(); + + foreach ( var name in alpha ) + { + src.Remove( name ); + colMap.Add( _headers.IndexOf( name ) ); + } + } + + foreach (var name in src) // something what is not fall under any regex + colMap.Add(_headers.IndexOf(name)); + + Debug.Assert( colMap.Count == _headers.Count ); + + _columnMap = colMap.ToArray(); // set column mapping + } + + /// + /// Create filtered copy of data. Filter func must return true if you want row to remain in the output copy. + /// + /// + /// + public IPivotedData Filter(Func filter) + { + var res = new ArrayBasedPivotData(_headers, _columnMap ); + for( var i = 0; i < _realData.Count; i++ ) + { + if (!filter(i)) + continue; + res.Add(_realData[i]); + } + + return res; + } + + /// + /// Create filtered copy of data. Resulting columns present in the data in order of appearance in newColumns. If column is missing it's omitted. + /// + public IPivotedData FilterColumns(IReadOnlyCollection newColumns, Func? filter = null) + { + var newColMap = new List<(string, int)>(); + foreach (var colName in newColumns) + { + var index = _headers.IndexOf(colName); + if ( index < 0 ) + continue; + newColMap.Add((colName, index)); + } + + var res = new ArrayBasedPivotData( newColMap.Select(x => x.Item1).ToList() ); + for( var i = 0; i < _realData.Count; i++ ) + { + if (filter != null && !filter(i)) + continue; + + var newData = new object?[newColMap.Count]; + foreach (var (oldIndex, newIndex) in newColMap.Select((x,pos) => (OldIndex: x.Item2, NewIndex: pos))) + { + newData[newIndex] = _realData[i].Length <= oldIndex + ? null + : _realData[i][oldIndex] + ; + } + res.Add(newData); + } + + return res; + } + private class RowComparer(IPivotedData pivot, Func, int> comparer) : IComparer + { + private readonly Dictionary _columnMap = pivot.Headers.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i); + private readonly Func, int> _comparer = comparer; + + public int Compare( object?[]? x, object?[]? y ) + => x == null ? -1 : y == null ? 1 : _comparer( x, y, _columnMap ); + } + + /// + /// Custom sort for real data. Be careful with views. + /// + /// + public void Sort( Func, int> comparer ) => _realData.Sort(new RowComparer( this, comparer )); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataJsonSerializer.cs b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataJsonSerializer.cs new file mode 100644 index 0000000..c8e359c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataJsonSerializer.cs @@ -0,0 +1,400 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Runtime.Serialization; +#if MSJSON +using System.Text.Json; +using System.Text.Json.Serialization; +#else +using Newtonsoft.Json; +#endif +namespace Rms.Risk.Mango.Pivot.Core; +#if MSJSON + public class ArrayBasedPivotDataJsonSerializer : JsonConverter + { + public override void Write( + Utf8JsonWriter writer, + ArrayBasedPivotData value, + JsonSerializerOptions options) + { + if ( string.IsNullOrWhiteSpace( value.Id ) ) + throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" ); + var headers = value.Headers.OrderBy(x => x.Value).Select(x => x.Key).ToList(); + if ( headers.Count == 0 ) + throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance"); + + writer.WriteStartObject(); + + writer.WritePropertyName("_id"); + writer.WriteStringValue(value.Id); + writer.WritePropertyName("ExpireAt"); + writer.WriteStringValue(value.ExpireAt.ToString("O")); + + writer.WritePropertyName("Headers"); + + writer.WriteStartArray(); + foreach ( var header in headers ) + writer.WriteStringValue( header ); + writer.WriteEndArray(); + + writer.WritePropertyName( "Data" ); + writer.WriteStartArray(); + + for ( var row = 0; row < value.Count; row ++ ) + { + writer.WriteStartArray(); + + for ( var col = 0; col < headers.Count; col++ ) + { + var data = value.Get( col, row ); + if ( data == null ) + { + writer.WriteNullValue(); + continue; + } + + var t = data.GetType(); + if (t == typeof(double)) + writer.WriteNumberValue((double)data); + else if (t == typeof(string)) + writer.WriteStringValue((string)data); + else if (t == typeof(int)) + writer.WriteNumberValue((int)data); + else if (t == typeof(long)) + writer.WriteNumberValue((long)data); + else if (t == typeof(DateTime)) + writer.WriteStringValue(((DateTime)data).ToString("O")); + else if (t == typeof(bool)) + writer.WriteBooleanValue((bool)data); + else + writer.WriteNullValue(); + } + writer.WriteEndArray(); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } + + private static void Expect( Utf8JsonReader reader, JsonTokenType expected ) + { + if ( !reader.Read() ) + throw new SerializationException(); + if ( reader.TokenType != expected ) + throw new SerializationException($"Expected {expected} but got {reader.TokenType}"); + } + + private static void ReadStartArray( Utf8JsonReader reader ) => Expect( reader, JsonTokenType.StartArray ); + private static void ReadEndProperty( Utf8JsonReader reader ) => Expect( reader, JsonTokenType.EndObject ); + + private static void ReadProperty( Utf8JsonReader reader, string name ) + { + Expect( reader, JsonTokenType.PropertyName ); + var n = reader.GetString(); + if ( n != name ) + throw new SerializationException($"Expected \"{name}\" but got \"{n}\""); + } + + + public override ArrayBasedPivotData Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + ReadProperty(reader, "_id"); + var id = reader.GetString(); + ReadProperty(reader, "ExpireAt"); + var expireAt = DateTime.Parse(reader.GetString()); + + ReadProperty(reader, "Headers"); + + ReadStartArray(reader); + var headers = new List(); + + while ( reader.Read() ) + { + var tokenType = reader.TokenType; + if ( tokenType == JsonTokenType.EndArray ) + break; + headers.Add( reader.GetString() ); + } + + var data = new ArrayBasedPivotData( headers ) + { + Id = id, + ExpireAt = expireAt + }; + + ReadProperty(reader, "Data"); + + while ( reader.Read() ) + { + var tokenType = reader.TokenType; + if ( tokenType == JsonTokenType.EndArray ) + break; + + if ( tokenType != JsonTokenType.StartArray ) + continue; + + var val = new object[headers.Count]; + var col = 0; + + while ( reader.Read() ) + { + tokenType = reader.TokenType; + if ( tokenType == JsonTokenType.StartArray ) + continue; + if ( tokenType == JsonTokenType.EndArray ) + break; + + switch ( tokenType ) + { + case JsonTokenType.Comment: + case JsonTokenType.None: + break; + // case JsonTokenType.Float: + // case JsonTokenType.Boolean: + // case JsonTokenType.Date: + // case JsonTokenType.Integer: + // case JsonTokenType.Bytes: + case JsonTokenType.String: + val[col] = reader.GetString(); + break; + case JsonTokenType.Number: + var s = reader.GetString(); + // since doubles are more than 90% of the data make it quick + if ( s.IndexOf(".", StringComparison.Ordinal) >= 0 && double.TryParse( s, out var d ) ) + val[col] = d; + if ( int.TryParse( s, out var i ) ) + val[col] = i; + if ( long.TryParse( s, out var l ) ) + val[col] = l; + if ( double.TryParse( s, out d ) ) + val[col] = d; + break; + case JsonTokenType.Null: + val[col] = null; + break; + // case JsonToken.StartObject: + // case JsonToken.StartArray: + // case JsonToken.StartConstructor: + // case JsonToken.PropertyName: + // case JsonToken.Raw: + // case JsonToken.Undefined: + // case JsonToken.EndObject: + // case JsonToken.EndConstructor: + default: + throw new SerializationException($"Unexpected token {tokenType}"); + } + + col += 1; + } + + data.Add( val ); + } + + ReadEndProperty( reader ); + return data; + } + + public override bool CanConvert(Type objectType) + { + return typeof(ArrayBasedPivotData) == objectType; + } + } +#else +public class ArrayBasedPivotDataJsonSerializer : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? val, JsonSerializer serializer) + { + if ( val is not ArrayBasedPivotData value ) + throw new ArgumentNullException( nameof(value), "ArrayBasedPivotData instance is null" ); + if ( string.IsNullOrWhiteSpace( value.Id ) ) + throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" ); + var headers = value.Headers; + if ( headers.Count == 0 ) + throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance"); + + writer.WriteStartObject(); + + writer.WritePropertyName("_id"); + writer.WriteValue(value.Id); + writer.WritePropertyName("ExpireAt"); + writer.WriteValue(value.ExpireAt); + + writer.WritePropertyName("Headers"); + + writer.WriteStartArray(); + foreach ( var header in headers ) + writer.WriteValue( header ); + writer.WriteEndArray(); + + writer.WritePropertyName( "Data" ); + writer.WriteStartArray(); + + for ( var row = 0; row < value.Count; row ++ ) + { + writer.WriteStartArray(); + + for ( var col = 0; col < headers.Count; col++ ) + { + var data = value.Get( col, row ); + if ( data == null ) + { + writer.WriteNull(); + continue; + } + + var t = data.GetType(); + if (t == typeof(double)) + writer.WriteValue((double)data); + else if (t == typeof(string)) + writer.WriteValue((string)data); + else if (t == typeof(int)) + writer.WriteValue((int)data); + else if (t == typeof(long)) + writer.WriteValue((long)data); + else if (t == typeof(DateTime)) + writer.WriteValue((DateTime)data); + else if (t == typeof(bool)) + writer.WriteValue((bool)data); + else + writer.WriteNull(); + } + writer.WriteEndArray(); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } + + private static void Expect( JsonReader reader, JsonToken expected ) + { + if ( !reader.Read() ) + throw new SerializationException($"Expected {expected} but got none"); + if ( reader.TokenType != expected ) + throw new SerializationException($"Expected {expected} but got {reader.TokenType}"); + } + + private static void ReadStartArray( JsonReader reader ) => Expect( reader, JsonToken.StartArray ); + private static void ReadEndProperty( JsonReader reader ) => Expect( reader, JsonToken.EndObject ); + + private static void ReadProperty( JsonReader reader, string name ) + { + Expect( reader, JsonToken.PropertyName ); + if ( reader.ValueType != typeof(string)) + throw new SerializationException($"Expected {name} but got \"{reader.Value}\" ({reader.ValueType})"); + var n = reader.Value as string; + if ( n != name ) + throw new SerializationException($"Expected \"{name}\" but got \"{n}\""); + } + + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + ReadProperty(reader, "_id"); + var id = reader.ReadAsString(); + ReadProperty(reader, "ExpireAt"); + var expireAt = reader.ReadAsDateTime(); + + ReadProperty(reader, "Headers"); + + ReadStartArray(reader); + var headers = new List(); + + while ( reader.Read() ) + { + var tokenType = reader.TokenType; + if ( tokenType == JsonToken.EndArray ) + break; + headers.Add( (string)reader.Value! ); + } + + var data = new ArrayBasedPivotData( headers ) + { + Id = id ?? "", + ExpireAt = expireAt ?? default(DateTime) + }; + + if (data.Id == "no_data") + return data; + + ReadProperty(reader, "Data"); + + while ( reader.Read() ) + { + var tokenType = reader.TokenType; + if ( tokenType == JsonToken.EndArray ) + break; + + if ( tokenType != JsonToken.StartArray ) + continue; + + var val = new object[headers.Count]; + var col = 0; + + while ( reader.Read() ) + { + tokenType = reader.TokenType; + if ( tokenType == JsonToken.StartArray ) + continue; + if ( tokenType == JsonToken.EndArray ) + break; + + switch ( tokenType ) + { + case JsonToken.Comment: + case JsonToken.None: + break; + case JsonToken.Float: + case JsonToken.String: + case JsonToken.Boolean: + case JsonToken.Date: + case JsonToken.Null: + case JsonToken.Integer: + case JsonToken.Bytes: + val[col] = reader.Value!; + break; + // case JsonToken.StartObject: + // case JsonToken.StartArray: + // case JsonToken.StartConstructor: + // case JsonToken.PropertyName: + // case JsonToken.Raw: + // case JsonToken.Undefined: + // case JsonToken.EndObject: + // case JsonToken.EndConstructor: + default: + throw new SerializationException($"Unexpected token {tokenType}"); + } + + col += 1; + } + + data.Add( val ); + } + + ReadEndProperty( reader ); + return data; + } + + public override bool CanRead => true; + + public override bool CanConvert(Type objectType) => typeof(ArrayBasedPivotData) == objectType; +} + +#endif \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataSerializer.cs b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataSerializer.cs new file mode 100644 index 0000000..4514b05 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotDataSerializer.cs @@ -0,0 +1,157 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Runtime.Serialization; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace Rms.Risk.Mango.Pivot.Core; + +internal class ArrayBasedPivotDataSerializer : SerializerBase +{ + public static readonly DateTime UnixEpoch = new( + 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static ulong ToMillisecondsSinceUnixEpoch(DateTime dateTimeUtc) => (ulong)(dateTimeUtc - UnixEpoch).TotalMilliseconds; + + public override void Serialize( BsonSerializationContext context, BsonSerializationArgs args, ArrayBasedPivotData value ) + { + if ( value == null ) + throw new ArgumentNullException( nameof(value), "ArrayBasedPivotData instance is null" ); + if ( string.IsNullOrWhiteSpace( value.Id ) ) + throw new SerializationException( "Id is not set for ArrayBasedPivotData instance" ); + var headers = value.Headers; + if ( headers.Count == 0 ) + throw new SerializationException("Headers count is zero for ArrayBasedPivotData instance"); + + + context.Writer.WriteStartDocument(); + context.Writer.WriteName("_id"); + context.Writer.WriteString(value.Id); + context.Writer.WriteName("ExpireAt"); + context.Writer.WriteDateTime((long)ToMillisecondsSinceUnixEpoch(value.ExpireAt)); + + context.Writer.WriteName("Headers"); + + context.Writer.WriteStartArray(); + foreach ( var header in headers ) + context.Writer.WriteString( header ); + context.Writer.WriteEndArray(); + + context.Writer.WriteName( "data" ); + context.Writer.WriteStartArray(); + + for ( var row = 0; row < value.Count; row ++ ) + { + context.Writer.WriteStartArray(); + + for ( var col = 0; col < headers.Count; col++ ) + { + var data = value.Get( col, row ); + if ( data == null ) + { + context.Writer.WriteNull(); + continue; + } + + var t = data.GetType(); + if (t == typeof(double)) + context.Writer.WriteDouble((double)data); + else if (t == typeof(string)) + context.Writer.WriteString((string)data); + else if (t == typeof(int)) + context.Writer.WriteInt32((int)data); + else if (t == typeof(long)) + context.Writer.WriteInt64((long)data); + else if (t == typeof(DateTime)) + context.Writer.WriteDateTime((long)ToMillisecondsSinceUnixEpoch((DateTime)data)); + else if (t == typeof(bool)) + context.Writer.WriteBoolean((bool)data); + else + context.Writer.WriteNull(); + } + context.Writer.WriteEndArray(); + } + + context.Writer.WriteEndArray(); + context.Writer.WriteEndDocument(); + } + + public override ArrayBasedPivotData Deserialize( BsonDeserializationContext context, BsonDeserializationArgs args ) + { + context.Reader.ReadStartDocument(); + + ExpectName(context, "_id"); + var id = context.Reader.ReadString(); + ExpectName(context, "ExpireAt"); + context.Reader.ReadDateTime(); + + ExpectName(context, "Headers"); + + context.Reader.ReadStartArray(); + var headers = new List(); + while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) + headers.Add( context.Reader.ReadString() ); + context.Reader.ReadEndArray(); + + var data = new ArrayBasedPivotData( headers ) { Id = id }; + + ExpectName(context, "data"); + + context.Reader.ReadStartArray(); + + while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) // rows + { + context.Reader.ReadStartArray(); + + var val = new object[headers.Count]; + var col = 0; + while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) // columns + { + var v = BsonValueSerializer.Instance.Deserialize(context); + + try + { + val[col] = BsonTypeMapper.MapToDotNetValue(v); + } + catch ( Exception ) + { + v[col] = null; + } + + col += 1; + } + context.Reader.ReadEndArray(); + + data.Add( val ); + } + + context.Reader.ReadEndArray(); + context.Reader.ReadEndDocument(); + return data; + } + + private static void ExpectName( BsonDeserializationContext context, string elementExpected ) + { + var name = context.Reader.ReadName(); + if ( name != elementExpected ) + throw new SerializationException( $"{elementExpected} expected, but got {name}" ); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Highlighting.cs b/Rms.Risk.Mango.Pivot.Core/Highlighting.cs new file mode 100644 index 0000000..45935a5 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Highlighting.cs @@ -0,0 +1,37 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core; + +[BsonIgnoreExtraElements] +public class Highlighting +{ + public enum HighlightingMode + { + Off = 0, + Breaks = 1, + HeatMap = 2 + } + + public HighlightingMode Mode { get; set; } = HighlightingMode.Off; + + public double MinBound { get; set; } = double.MinValue; + public double MaxBound { get; set; } = double.MaxValue; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/IPivotTableDataSource.cs b/Rms.Risk.Mango.Pivot.Core/IPivotTableDataSource.cs new file mode 100644 index 0000000..fd27d2e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/IPivotTableDataSource.cs @@ -0,0 +1,266 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Newtonsoft.Json; +using Rms.Risk.Mango.Pivot.Core.Models; +using System.Drawing; +using System.Text.RegularExpressions; +using static Rms.Risk.Mango.Pivot.Core.IPivotTableDataSource; + +namespace Rms.Risk.Mango.Pivot.Core; + +public class PivotColumnDescriptor +{ + [JsonIgnore] public Regex NameRegex { get; private set; } = null!; + public Color Background { get; set; } + public Color AlternateBackground { get; set; } + public string Format { get; set; } = ""; + public bool ShowTotals { get; set; } = true; + + public string NameRegexString + { + get; + set + { + field = value; + NameRegex = new( + value, + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | + RegexOptions.Singleline + ); + } + } = ""; +} + +public enum PivotFieldPurpose +{ + Data = 0, + PrimaryKey1 = 1, + PrimaryKey2 = 2, + Key = 3, + Info = 4, + Hidden = 5 +} + +public class PivotFieldDescriptor +{ + public string Name { get; set; } = ""; + public PivotFieldPurpose Purpose { get; set; } + + [JsonIgnore] public Type Type { get; set; } = typeof(object); + + public string TypeString + { + get => Type.Name; + set => Type = Type.GetType( "System."+value ) ?? throw new ("Type not found: System."+value); + } +} + +public enum CollectionType +{ + All, + NoMeta, + HaveMeta +} + +public class GroupedPivot : ICloneable +{ + public required string Text { get; init; } + public bool IsGroup { get; init; } + public required PivotDefinition Pivot { get; init; } + + public override string ToString() => Text; + + object ICloneable.Clone() => Clone(); + + public GroupedPivot Clone() => + new() + { + Text = Text, + IsGroup = IsGroup, + Pivot = Pivot.Clone() + }; +} + +public class GroupedCollection +{ + public required string DataSourcePrefix { get; init; } + public string CollectionNameWithPrefix => $"{DataSourcePrefix}: {CollectionNameWithoutPrefix}"; + public required string CollectionNameWithoutPrefix { get; init; } + public bool IsGroup { get; init; } + public PivotColumnDescriptor[] ColumnDescriptors { get; set; } = []; + public HashSet DataFields { get; set; } = []; + public HashSet KeyFields { get; set; } = []; + public DateTime[] Cobs { get; set; } = []; + public string[] Departments { get; set; } = []; + public List Pivots { get; set; } = []; + public Dictionary FieldTypes { get; set; } = []; + + public override string ToString() => CollectionNameWithPrefix; + + public void CopyFrom(GroupedCollection other) + { + if ( other == null ) throw new ArgumentNullException(nameof(other)); + + ColumnDescriptors = other.ColumnDescriptors; + DataFields = other.DataFields; + KeyFields = other.KeyFields; + Cobs = other.Cobs; + Departments = other.Departments; + Pivots = other.Pivots; + FieldTypes = other.FieldTypes; + } + + public List GetDrilldownKeyFields(PivotFieldPurpose purpose) => + FieldTypes + .Where( x => x.Value.Purpose == purpose ) + .Select(x => x.Key) + .OrderBy(x => x) + .ToList(); +} + +public interface IPivotTableDataSource +{ + string SourceId { get; } + string Prefix { get; } + + string User { get; set; } + + Task> GetAllMeta(bool force = false, CancellationToken token = default); + + /// + /// Get drilldown formula for the given column + /// + /// + /// + /// Value to be compared with. Only records not matching this value will be shown. + /// If true "name = value", if false "name != value" + /// + /// + Task GetDrilldownAsync(string collectionName, string name, string value = "\"\"", bool equals = false, CancellationToken token = default ); + + /// + /// Aggregate data + /// + /// + /// Pivot definition + /// Extra $match stage + /// Skip cached results + /// + /// + /// + /// Pivoted data + Task PivotAsync( + string collectionName, + PivotDefinition def, + FilterExpressionTree.ExpressionGroup? extraFilter, + bool skipCache, + string? userName = null, + int maxFetchSize = -1, + CancellationToken token = default + ); + + public enum PivotType + { + Predefined, + User, + UserAndPredefined, + All + } + + Task UpdatePredefinedPivotsAsync(string collectionName, + IEnumerable pivots, + bool predefined = false, + string? userName = null, + CancellationToken token = default); + + Task UpdatePivotAsync(string collectionName, + PivotDefinition pivot, + string? userName = null, + CancellationToken token = default) + => UpdatePredefinedPivotsAsync(collectionName, [pivot], pivot.IsPredefined, userName, token); + + /// + /// Preprocess def to get proper query text. + /// Should perform all sort of postprocessing appied to the query def. + /// + /// mongo collection Name + /// Pivot definition + /// Extra $match stage + /// + /// Processed query + Task GetQueryTextAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default ); + + /// + /// Get a single document + /// + /// mongo collection Name + /// Primary key fields in no particular order + /// Extra $match stage + /// + /// Json string + Task GetDocumentAsync(string collectionName, KeyValuePair [] keys, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default); + + /// + /// Get a single document + /// + /// mongo collection Name + /// Filter selecting a single document + /// + /// Json string + Task GetDocumentAsync(string collectionName, FilterExpressionTree.ExpressionGroup filterText, CancellationToken token = default); + + /// + /// Delete pivot from the collection. Works for user pivots only. + /// + /// mongo collection Name + /// Pivot to delete + /// Group pivot belongs to. Currently only "User Pivots" + /// User who owns the pivot + /// + /// + Task DeletePivotAsync(string collectionName, string pivotName, string groupName, string userName, CancellationToken token = default); +} + +/// +/// Low level metadata access interface. Should not be used directly in the application. Intended to be used internal caches only. +/// Applications should use instead. +/// +public interface IPivotTableDataSourceMetaProvider +{ + string SourceId { get; } + string Prefix { get; } + + Task GetCollectionsAsync(CollectionType includeMeta = CollectionType.All, CancellationToken token = default); + Task GetDepartmentsAsync(string collectionName, CancellationToken token = default); + Task<(string, string)[]> GetDesksWithDepartmentAsync(string collectionName, CancellationToken token = default); + Task GetKeyFieldsAsync(string collectionName, CancellationToken token = default); + Task GetDrilldownKeyFieldsAsync(string collectionName, PivotFieldPurpose keyLevel, CancellationToken token = default); + Task GetDataFieldsAsync(string collectionName, CancellationToken token = default); + Task GetColumnDescriptorsAsync(string collectionName, CancellationToken token = default); + Task GetCobDatesAsync(string collectionName, bool force = false,CancellationToken token = default); + + /// + /// Get all field types including keys, data and calculated fields + /// + /// Field types + Dictionary GetFieldTypes(string collectionName); + + Task> GetPivotsAsync(string collectionName, PivotType pivotType, string? userName = null, CancellationToken token = default); + +} diff --git a/Rms.Risk.Mango.Pivot.Core/IPivotedData.cs b/Rms.Risk.Mango.Pivot.Core/IPivotedData.cs new file mode 100644 index 0000000..be6d227 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/IPivotedData.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core; + +public interface IPivotedData +{ + + public string Id { get; } + + /// + /// name : column + /// + IReadOnlyCollection Headers { get; } + + /// + /// Get column positions. Beware that duplicate column will be missing. + /// Only first unique occurrence of column name will be counted. + /// + Dictionary GetColumnPositions() + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // this is safer than calling ToDictionary as it handles duplicate headers + foreach ( var (key, pos) in Headers.Select((x, i) => (Key: x, Value: i)) ) + { + dict.TryAdd(key, pos); + } + + return dict; + } + + /// + /// Number of rows + /// + int Count { get; } + + /// + /// Get element at (col, row) + /// + /// + /// + /// + object? Get( int col, int row ); + + Type GetColumnType( int col ); + + /// + /// Create filtered copy of data. Filter func must return true if you want row to remain in the output copy. + /// + /// + /// + IPivotedData Filter(Func filter); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/CalcFieldDef.cs b/Rms.Risk.Mango.Pivot.Core/Models/CalcFieldDef.cs new file mode 100644 index 0000000..f745518 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/CalcFieldDef.cs @@ -0,0 +1,35 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +[BsonIgnoreExtraElements] +public class CalcFieldDef(string formula, string drillDown, string[]? lookupDef, string aggregationOperator) : ICloneable +{ + public string Formula { get; set; } = formula; + public string DrillDown { get; set; } = drillDown; + public string[] LookupDef { get; set; } = lookupDef ?? []; + public string AggregationOperator { get; set; } = aggregationOperator; + + public override string ToString() => $"{AggregationOperator} {Formula}"; + + object ICloneable. Clone() => Clone(); + public CalcFieldDef Clone() => (CalcFieldDef)MemberwiseClone(); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/CollStatsModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/CollStatsModel.cs new file mode 100644 index 0000000..bad04df --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/CollStatsModel.cs @@ -0,0 +1,89 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class CollStatsModel +{ + public bool Sharded { get; set; } + public bool Capped { get; set; } + public CollStatsData WiredTiger { get; set; } = new CollStatsData(); + public Dictionary IndexDetails { get; set; } = new(); + + // New fields added based on the provided JSON structure + public string Ns { get; set; } = string.Empty; + public long Count { get; set; } + public long Size { get; set; } + public long StorageSize { get; set; } + public long TotalIndexSize { get; set; } + public long TotalSize { get; set; } + public Dictionary IndexSizes { get; set; } = new(); + public double AvgObjSize { get; set; } + public long MaxSize { get; set; } + public int NIndexes { get; set; } + public int ScaleFactor { get; set; } + public int NChunks { get; set; } + + public static CollStatsModel FromJson(string json) + { + try + { + var options = new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + AllowTrailingCommas = true + }; + return System.Text.Json.JsonSerializer.Deserialize(json, options) ?? new(); + } + catch + { + // Log the exception if necessary + return new (); + } + } +} + +public class CollStatsData +{ + public Metadata Metadata { get; set; } = new(); + public string CreationString { get; set; } = ""; + public string Type { get; set; } = ""; + public string Uri { get; set; } = ""; + public Dictionary? LSM { get; set; } + public Dictionary? Autocommit { get; set; } + public Dictionary? Backup { get; set; } + public Dictionary? BlockManager { get; set; } + public Dictionary? Btree { get; set; } + public Dictionary? Cache { get; set; } + public Dictionary? CacheWalk { get; set; } + public Dictionary? Checkpoint { get; set; } + public Dictionary? Compression { get; set; } + public Dictionary? Cursor { get; set; } + public Dictionary? Reconciliation { get; set; } + public Dictionary? Session { get; set; } + public Dictionary? Transaction { get; set; } +} + +public class Metadata +{ + public int FormatVersion { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/ColorConverter.cs b/Rms.Risk.Mango.Pivot.Core/Models/ColorConverter.cs new file mode 100644 index 0000000..6936f8d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/ColorConverter.cs @@ -0,0 +1,47 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; +using System.Drawing; +using System.Globalization; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +internal class ColorConverter +{ + private static readonly ConcurrentDictionary _colorConverter = new(); + + public static Color ConvertFromString( string colorStr ) + { + if ( _colorConverter.TryGetValue( colorStr, out var color ) ) + return color; + + if ( colorStr.StartsWith( "#" ) ) + { + color = Color.FromArgb( int.Parse( colorStr[1..], NumberStyles.HexNumber ) ); + _colorConverter.TryAdd( colorStr, color ); + return color; + } + + color = Color.FromName( colorStr ); + if ( color.IsEmpty ) + color = Color.DimGray; + _colorConverter.TryAdd( colorStr, color ); + return color; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/DatabaseStatsModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/DatabaseStatsModel.cs new file mode 100644 index 0000000..a6b7e89 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/DatabaseStatsModel.cs @@ -0,0 +1,93 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Services.Models; + +public class DatabaseStatsModel +{ + public Dictionary Raw { get; set; } = new(); + + public static DatabaseStatsModel FromBson(BsonDocument res) + { + var model = new DatabaseStatsModel(); + + // Check if "raw" exists and is a document + if (res.Contains("raw") && res["raw"].IsBsonDocument) + { + var rawDoc = res["raw"].AsBsonDocument; + foreach (var element in rawDoc.Elements) + { + // Each element's value is a BsonDocument representing DatabaseStatsRaw + if (element.Value.IsBsonDocument) + { + var statsRaw = DatabaseStatsRaw.FromBson(element.Value.AsBsonDocument); + model.Raw[element.Name] = statsRaw; + } + } + } + + return model; + } +} + +public class DatabaseStatsRaw +{ + public static DatabaseStatsRaw FromBson(BsonDocument doc) => + new() + { + Db = doc.GetValue("db", "").AsString, + Collections = doc.GetValue("collections", 0).ToInt32(), + Views = doc.GetValue("views", 0).ToInt32(), + Objects = doc.GetValue("objects", 0L).ToInt64(), + AvgObjSize = doc.GetValue("avgObjSize", 0.0).ToDouble(), + DataSize = doc.GetValue("dataSize", 0.0).ToDouble(), + StorageSize = doc.GetValue("storageSize", 0.0).ToDouble(), + FreeStorageSize = doc.GetValue("freeStorageSize", 0.0).ToDouble(), + Indexes = doc.GetValue("indexes", 0).ToInt32(), + IndexSize = doc.GetValue("indexSize", 0.0).ToDouble(), + IndexFreeStorageSize = doc.GetValue("indexFreeStorageSize", 0.0).ToDouble(), + TotalSize = doc.GetValue("totalSize", 0.0).ToDouble(), + TotalFreeStorageSize = doc.GetValue("totalFreeStorageSize", 0.0).ToDouble(), + ScaleFactor = doc.GetValue("scaleFactor", 0).ToInt32(), + FsUsedSize = doc.GetValue("fsUsedSize", 0.0).ToDouble(), + FsTotalSize = doc.GetValue("fsTotalSize", 0.0).ToDouble(), + Ok = doc.GetValue("ok", 0.0).ToDouble() + }; + + public string Db { get; set; } = string.Empty; + public int Collections { get; set; } + public int Views { get; set; } + public long Objects { get; set; } + public double AvgObjSize { get; set; } + public double DataSize { get; set; } + public double StorageSize { get; set; } + public double FreeStorageSize { get; set; } + public int Indexes { get; set; } + public double IndexSize { get; set; } + public double IndexFreeStorageSize { get; set; } + public double TotalSize { get; set; } + public double TotalFreeStorageSize { get; set; } + public int ScaleFactor { get; set; } + public double FsUsedSize { get; set; } + public double FsTotalSize { get; set; } + public double Ok { get; set; } + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/DrilldownSupport.cs b/Rms.Risk.Mango.Pivot.Core/Models/DrilldownSupport.cs new file mode 100644 index 0000000..1ab8ecb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/DrilldownSupport.cs @@ -0,0 +1,379 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Reflection; +using log4net; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +/// +/// Implements drilldown functionality. +/// +// ReSharper disable once InconsistentNaming +public class DrilldownSupport(List _collections) +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public const string DefaultDrilldownField = ""; + + public Func[], Task> ShowDocument { get; set; } = _ => Task.CompletedTask; + public Func MessageBoxShow { get; set; } = _ => Task.CompletedTask; + public Func> MessageBoxShowYesNo { get; set; } = (_,_) => Task.FromResult(false); + public Func ShowException { get; set; } = _ => Task.CompletedTask; + public Func GetPivotDefinition { get; set; } = (_,_) => null; + public Func?>> GetCustomDrilldown { get; set; } = (_,_) => Task.FromResult?>(null); + + private GroupedCollection GetCollection(string name) => _collections.FirstOrDefault( x => x.CollectionNameWithPrefix == name ) ?? throw new ApplicationException($"Collection=\"{name}\" is not found"); + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public static bool ShowAllKeysInDrilldowns { get; set; } + + /// + /// Determines collection and pivot that needs to be executed. + /// Returned pivot contains he filter required to show only rows of the original set that + /// made contribution to the number shown in column/>. + /// + /// Data source + /// Source collection name + /// Column name as displayed to drilldown to + /// Display name to real column name map + /// All columns currently shown. Names must be resolvable via getValue + /// Currently shown pivot definition + /// Get value of any column for the current row + /// List of all data fields that can potentially be shown using current pivot + /// List of all key fields that can potentially be shown using current pivot + /// Collection name and pivot definition implementing drilldown for supplied field + public async Task?> Drilldown( + IPivotTableDataSource source, + string collectionName, + string displayName, + Dictionary displayToRealNameMap, + string [] allColumns, + PivotDefinition current, + Func getValue, + IReadOnlySet allDataFields, + IReadOnlySet allKeyFields + ) + { + try + { + var fields = GetPrimaryKey2KeyFields(collectionName, getValue, allColumns).ToArray(); + if ( fields.Length == GetCollection(collectionName).FieldTypes.Count( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 ) ) + { + // all keys already shown. now ony one document can be selected. + // show the detailed single document view instead of report + + await ShowDocument( fields! ); + return null; + } + + var realName = Rename(displayName, displayToRealNameMap); + + var pivotTuple = await GetCustomDrilldown( collectionName, realName ); + + pivotTuple ??= await TryCustomDrilldown(source, collectionName, current, getValue, realName, allColumns); + + if ( pivotTuple != null ) + return pivotTuple; + + // aggregation drilldown + + // there is no reason to check this for map/reduce reports as they always contains unique column names + if ( !allDataFields.Contains( realName ) && !allKeyFields.Contains( realName ) ) + { + await MessageBoxShow( $"Drilldown is not supported for Column=\"{realName}\" DisplayedAs=\"{displayName}\"" ); + return null; + } + + // make inverted dictionary + var realToDisplayNameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach ( var item in displayToRealNameMap ) + realToDisplayNameMap[item.Value] = item.Key; + + pivotTuple = await DrilldownInternal(source, collectionName, getValue, displayName, realName, realToDisplayNameMap, current, allKeyFields); + return pivotTuple; + } + catch (Exception ex) + { + await ShowException(ex); + return null; + } + } + + private static string Rename(string name, Dictionary map) + { + if ( !map.TryGetValue(name, out var value) || value == null ) + value = name; + return value; + } + + private async Task?> DrilldownInternal( + IPivotTableDataSource dataSource, + string collectionName, + Func getValue, + string displayName, + string realName, + Dictionary realToDisplayNameMap, + PivotDefinition source, + IReadOnlySet allKeys) + { + try + { + var value = getValue(displayName); + var isString = value is string; + var isDate = value is DateTime; + var isSql = collectionName.StartsWith("BFG: "); + + var keyFields = source.KeyFields.ToArray(); + var dataFields = source.DataFields.ToArray(); + + var current = source.Clone(); + + current.Group = PivotDefinition.CurrentPivotGroup; + + var keyFilter = ""; + + var fieldTypes = GetCollection(collectionName).FieldTypes; + + foreach ( var keyColumn in keyFields ) + { + var v = getValue( Rename(keyColumn.Replace( ".", " " ), realToDisplayNameMap) ); + var val = v == null + ? "null" + : $"\"{v}\""; + + if ( fieldTypes.TryGetValue( keyColumn, out var desc ) ) + { + if ( desc.Type == typeof(DateTime) && v?.GetType() == typeof(DateTime) ) + val = isSql ? $"'{v:yyyy-MM-dd}'" : $"ISODate(\"{v:yyyy-MM-ddTHH:mm:ss.fff}Z\")"; + else if ( desc.Type != typeof(string) ) + val = val.Trim('\"'); + } + + if ( keyFilter != "" ) + keyFilter += GetFilterConcatenationString(collectionName); + + //keyFilter += $"{{ \"{keyColumn}\": {val} }}"; + keyFilter += await dataSource.GetDrilldownAsync(collectionName, keyColumn, val, true ); + } + + var nullValue = isString + ? "\"\"" + : isDate + ? "null" + : "0.0"; + + if ( keyFilter != "" ) + keyFilter += GetFilterConcatenationString(collectionName); + + keyFilter += await dataSource.GetDrilldownAsync(collectionName, realName, nullValue ); + current.DrilldownFilter = keyFilter; + + var keys1 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey1); + var keys2 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey2); + + var keys = (keys1.All(x => keyFields.Contains(x)) ? keys2 : keys1 ) + .Concat( + ShowAllKeysInDrilldowns + ? allKeys + : keyFields ) + .Distinct() + .Where( allKeys.Contains ) + ; + + current.KeyFields = keys.ToArray(); + current.DataFields = dataFields; + + _log.Debug( $"Drilldown Pivot=\"{source.Name}\" Filter:\n{current.Filter} DrilldownFilter:\n{current.DrilldownFilter}"); + + return Tuple.Create(collectionName, current); + } + catch ( Exception ex ) + { + await ShowException( ex ); + return null; + } + } + + private static string GetFilterConcatenationString(string collectionName) => collectionName.StartsWith("Forge:") ? "," : "\n\tAND "; + + private async Task?> TryCustomDrilldown( + IPivotTableDataSource dataSource, + string collectionName, + PivotDefinition? origPivot, + Func getValue, + string column, + string[] allHeaders) + { + // try to find custom drilldown + + var rec = origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == column ) + ?? origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField ) + ; + if ( rec == null ) + return null; + + // detect other keys shown + var keyHeaders = GetCollection(collectionName).KeyFields; + var keys = allHeaders + .Where( x => keyHeaders.Contains( x ) ) + .Select( x => new KeyValuePair(x, getValue( x )?.ToString() ?? "") ) + ; + + var basePivot = GetPivotDefinition( rec.DrilldownPivot, collectionName ); + + if ( basePivot != null ) + { + var pivot = await SameCollectionDrilldown( dataSource, collectionName, origPivot, getValue, column, allHeaders, basePivot, rec, keys ); + return Tuple.Create(collectionName, pivot); + } + + var path = rec.DrilldownPivot.Split( '/' ); + if ( path.Length == 2 ) + { + if ( !await MessageBoxShowYesNo( + $"Do you want to start separate instance and run \"{path[1]}\" for collection \"{path[0]}\" in order to drill down to \"{column}\"?", + "Drill down" ) ) + { + return null; + } + + var tuple = await SeparateCollectionDrilldown( origPivot, getValue, column, allHeaders, keys, path[0], path[1] ); + return tuple; + } + + await MessageBoxShow( $"DrilldownPivot=\"{rec.DrilldownPivot}\" is not found. Column=\"{column}\"" ); + return null; + } + + private async Task?> SeparateCollectionDrilldown( + PivotDefinition? orig, + Func getValue, + string column, + string [] allHeaders, + IEnumerable> keys, + string destCollection, + string destPivotName + ) + { + var destPivot = GetPivotDefinition(destPivotName, destCollection)?.Clone(); + + var rec = orig?.Drilldown.FirstOrDefault( x => x.ColumnName == column ) + ?? orig?.Drilldown.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField ) + ; + + if (rec == null || destPivot == null) + { + await MessageBoxShow($"Drilldown is not configured for \"{column}\""); + return null; + } + + destPivot.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys + + PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition + + return Tuple.Create(destCollection, destPivot); + } + + private Task SameCollectionDrilldown( + IPivotTableDataSource dataSource, + string collectionName, + PivotDefinition? origPivot, + Func getValue, + string column, + string [] allHeaders, + PivotDefinition basePivot, + PivotDefinition.DrilldownDef rec, + IEnumerable> keys + ) + { + var drill = basePivot.Clone(); + + drill.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys + + PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition + + drill.Name = PivotDefinition.CurrentPivotName; + drill.Group = PivotDefinition.CurrentPivotGroup; + + if ( !string.IsNullOrWhiteSpace( rec.AppendToBeforeGrouping ) ) + { + // append to "Before Grouping" + if ( !string.IsNullOrWhiteSpace( drill.BeforeGrouping ) ) + drill.BeforeGrouping += ",\n"; + drill.BeforeGrouping += PrepareDrilldownCondition( allHeaders, rec.AppendToBeforeGrouping, column, getValue ); + } + + var primaryKeys1 = GetCollection(collectionName).FieldTypes.Values + .Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey1 ) + .Select( x => x.Name ) + .ToList(); + + if ( origPivot?.KeyFields.Intersect( primaryKeys1 ).Count() != primaryKeys1.Count + ) // not all PK1 shown => add missing PK1 + { + drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) ).ToArray(); + } + else + { + // do the same for PK2 + var primaryKeys2 = GetCollection(collectionName).FieldTypes.Values + .Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey2 ) + .Select( x => x.Name ) + .ToList(); + + if ( origPivot.KeyFields.Intersect( primaryKeys2 ).Count() != primaryKeys2.Count + ) // not all PK2 selected => add missing PK1 and PK2 + { + drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) ) + .ToArray(); + drill.KeyFields = drill.KeyFields.Concat( primaryKeys2.Where( x => !drill.KeyFields.Contains( x ) ) ) + .ToArray(); + } + } + + return Task.FromResult(drill); + } + + + private IEnumerable> GetPrimaryKey2KeyFields( + string collectionName, + Func getValue, + string [] allHeaders + ) => + GetCollection(collectionName).FieldTypes + .Where( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 ) + .Select( x => x.Key ) + .Where( allHeaders.Contains ) + .Select( key => new KeyValuePair( key, getValue( key ) ) ); + + private static string PrepareDrilldownCondition( + string [] allHeaders, + string cond, + string columnName, + Func getValue + ) + { + cond = allHeaders + .Aggregate( + cond, + (current, header) => current.Replace($"<{header}>", getValue(header)?.ToString()) + ); + + cond = cond.Replace("", columnName); + return cond; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/ExpiringConcurrentDictionary.cs b/Rms.Risk.Mango.Pivot.Core/Models/ExpiringConcurrentDictionary.cs new file mode 100644 index 0000000..8be762c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/ExpiringConcurrentDictionary.cs @@ -0,0 +1,209 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +/// +/// A thread-safe dictionary with elements that expire after a specified duration. +/// Accessing an element renews its expiration time. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the dictionary. +public class ExpiringConcurrentDictionary : IDisposable where TValue : class where TKey : notnull +{ + private readonly ConcurrentDictionary _dictionary = new(); + private readonly TimeSpan _expirationDuration; + private readonly bool _shouldDispose; + private readonly Func? _elementFactory; + private readonly Timer _cleanupTimer; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The duration after which elements expire. + /// The factory method to create elements for missing keys. + /// The interval at which expired elements are removed. + /// Indicates whether elements should be disposed when removed. + public ExpiringConcurrentDictionary(Func elementFactory, TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true) + : this(expirationDuration, cleanupInterval, shouldDispose) + { + _elementFactory = elementFactory ?? throw new ArgumentNullException(nameof(elementFactory)); + } + + /// + /// Initializes a new instance of the class. + /// Element factory must be provided later via . + /// + /// The duration after which elements expire. + /// The interval at which expired elements are removed. + /// Indicates whether elements should be disposed when removed. + public ExpiringConcurrentDictionary(TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true) + { + _expirationDuration = expirationDuration; + _shouldDispose = shouldDispose; + _elementFactory = null; + _cleanupTimer = new(_ => RemoveExpiredElements(), null, cleanupInterval, cleanupInterval); + } + + ~ExpiringConcurrentDictionary() + { + Dispose(false); + } + + /// + /// Gets or adds an element by key. If the element exists and is not expired, its expiration is renewed. + /// If the element does not exist or is expired, a new element is created using the factory method. + /// + /// The key of the element. + /// The factory method to create the element if it does not exist or is expired. + /// The element associated with the key. + public TValue GetOrAdd(TKey key, Func? elementFactory = null) + { + while (true) + { + var now = DateTime.UtcNow; + if (_dictionary.TryGetValue(key, out var entry)) + { + if (entry.Expiration > now) + { + // Renew expiration and return the value + _dictionary[key] = (entry.Value, now.Add(_expirationDuration)); + return entry.Value; + } + else + { + // Remove expired entry + RemoveEntry(key, entry.Value); + } + } + + // Create a new value using the factory method + var factory = elementFactory ?? _elementFactory; + if (factory == null) + { + throw new InvalidOperationException("Element factory is not specified."); + } + var newValue = factory(key); + var newEntry = (newValue, now.Add(_expirationDuration)); + if (_dictionary.TryAdd(key, newEntry)) + { + return newValue; + } + } + } + + public void Clear() + { + var values = _dictionary.Values.ToList(); + _dictionary.Clear(); + + foreach (var entry in values) + { + DisposeIfNecessary(entry.Value); + } + } + + /// + /// Removes expired elements from the dictionary. + /// If elements implement , they are disposed upon removal. + /// + public void RemoveExpiredElements() + { + var now = DateTime.UtcNow; + foreach (var key in _dictionary.Keys) + { + if (_dictionary.TryGetValue(key, out var entry) && entry.Expiration <= now) + { + RemoveEntry(key, entry.Value); + } + } + } + + /// + /// Removes an element by key. + /// If the element implements , it is disposed upon removal. + /// + /// The key of the element to remove. + /// True if the element was removed; otherwise, false. + public bool TryRemove(TKey key) + { + if (_dictionary.TryRemove(key, out var entry)) + { + DisposeIfNecessary(entry.Value); + return true; + } + return false; + } + + /// + /// Disposes the dictionary and its elements if they implement . + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _cleanupTimer.Dispose(); + foreach (var entry in _dictionary.Values) + { + DisposeIfNecessary(entry.Value); + } + _dictionary.Clear(); + } + + _disposed = true; + } + } + + private void RemoveEntry(TKey key, TValue value) + { + if (_dictionary.TryRemove(key, out _)) + { + DisposeIfNecessary(value); + } + } + + private void DisposeIfNecessary(TValue value) + { + if (_shouldDispose && value is IDisposable disposable) + { + disposable.Dispose(); + } + } + /// + /// Determines whether the dictionary contains the specified key. + /// + /// The key to locate in the dictionary. + /// True if the dictionary contains the key; otherwise, false. + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + /// Gets the count of elements in the dictionary. + /// + public int Count => _dictionary.Count; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/ExpiringObjectPool.cs b/Rms.Risk.Mango.Pivot.Core/Models/ExpiringObjectPool.cs new file mode 100644 index 0000000..1dac591 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/ExpiringObjectPool.cs @@ -0,0 +1,110 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class ExpiringObjectPool + where TValue : class + where TKey : notnull +{ + public static TimeSpan DefaultExpiryDuration = TimeSpan.FromHours(3); + public static TimeSpan DefaultCleanupInterval = TimeSpan.FromMinutes(5); + + private readonly ConcurrentDictionary _pool; + private readonly Func> _objectGenerator; + private readonly TimeSpan _expiryDuration; + private readonly Timer _cleanupTimer; + private readonly ConcurrentDictionary> _loadingTasks = new(); + + public ExpiringObjectPool(Func> objectGenerator, TimeSpan expiryDuration = default, TimeSpan cleanupInterval = default ) + { + if ( cleanupInterval == TimeSpan.Zero ) + cleanupInterval = DefaultCleanupInterval; + if (expiryDuration == TimeSpan.Zero ) + expiryDuration = DefaultExpiryDuration; + + _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator)); + _expiryDuration = expiryDuration; + _pool = new ConcurrentDictionary(); + + // Set up a timer to clean up expired objects + _cleanupTimer = new Timer(CleanupExpiredObjects, null, cleanupInterval, cleanupInterval); + } + + public void Clear() => _pool.Clear(); + + public async Task Get(TKey key, TArg arg, CancellationToken token = default) + { + if (_pool.TryGetValue(key, out var entry) && entry.Expiry > DateTime.UtcNow) + { + return entry.Item; + } + + // Check if the key is already being loaded + var loadingTask = _loadingTasks.GetOrAdd(key, _ => LoadNewItem(key, arg, token)); + + try + { + var newItem = await loadingTask; + return newItem; + } + finally + { + // Remove the task once loading is complete + _loadingTasks.TryRemove(key, out _); + } + } + + private async Task LoadNewItem(TKey key, TArg arg, CancellationToken token) + { + var newItem = await _objectGenerator(key, arg, token); + _pool[key] = (newItem, DateTime.UtcNow.Add(_expiryDuration)); + return newItem; + } + + public void ReturnObject(TKey key, TValue item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + + _pool[key] = (item, DateTime.UtcNow.Add(_expiryDuration)); + } + + private void CleanupExpiredObjects(object? state) + { + var now = DateTime.UtcNow; + + // Remove expired objects + var expiredKeys = _pool.Where(pair => pair.Value.Expiry <= now).Select(pair => pair.Key).ToList(); + foreach (var key in expiredKeys) + { + _pool.TryRemove(key, out _); + } + } + + public void Dispose() + { + _cleanupTimer.Dispose(); + } + + public void Remove(TKey key) + { + _pool.TryRemove(key, out _); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/FieldMapping.cs b/Rms.Risk.Mango.Pivot.Core/Models/FieldMapping.cs new file mode 100644 index 0000000..1768b34 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/FieldMapping.cs @@ -0,0 +1,168 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class FieldMapping +{ + public const double DrilldownTolerance = 0.000001; + + public FieldMappingData Data { get; set; } = new(); + + public int Count => Data.Fields.Count; + public bool UseMapping => Data.UseMapping; + public IEnumerable FieldNames => Data.Fields.Keys; + public IEnumerable> Fields => Data.Fields; + public IEnumerable CalculatedFields => Data.CalculatedFields.Keys; + + public override string ToString() => $"Fields={Count} CalculatedFields={Data.CalculatedFields.Count} Lookups={Data.Lookups.Count}"; + + public bool TryGetValue( string name, out SingleFieldMapping? mapping ) => Data.Fields.TryGetValue( name, out mapping ); + + public SingleFieldMapping this[ string name ] + { + get + { + if ( !Data.Fields.TryGetValue( name, out var mapping ) + && !Data.Fields.TryGetValue( name.Replace( ".", " " ), out mapping ) ) + throw new MissingFieldException( $"Field \"{name}\" is not found" ); + + return mapping; + } + + set => Data.Fields[name] = value; + } + + public bool ContainsKey( string name ) => + Data.Fields.ContainsKey( name ) + || Data.Fields.ContainsKey( name.Replace( ".", " " ) ); + + public string MapField(string name) + { + if (!Data.UseMapping) + return name; + + return !TryGetValue(name, out var m) + ? name + : $"f{m?.Id}"; + } + + public string UnmapField(string name) + { + if (!Data.UseMapping || !name.StartsWith("f")) + return name; + if (!int.TryParse(name[1..], out var id) || id <= 0) + return name; + var n = Data.Fields.FirstOrDefault(x => x.Value.Id == id).Key; + return n ?? name; + } + + public FieldMapping(bool use) + { + Data.UseMapping = use; + } + + public void MapAllFields(IList pipeline) + { + for (var i = 0; i < pipeline.Count; i++) + { + var stageName = pipeline[i].Elements.First().Name; + pipeline[i] = MapAllFields(pipeline[i], stageName == "$project" || stageName == "$match"); + } + } + + public BsonDocument MapAllFields(BsonDocument bsonDocument, bool replaceNakedNames) + { + var json = bsonDocument.ToJson(); + + json = MapAllFields(json, replaceNakedNames); + + return BsonDocument.Parse(json); + } + + public string MapAllFields(string json, bool replaceNakedNames) + { + // ordering by key.Length to resolve conflicts like "TradePV" vs "PV" + + json = Data.CalculatedFields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"${m.Key}\"", m.Value.Formula)); + if (Data.UseMapping) + json = Data.Fields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"${m.Key}\"", $"\"$f{m.Value.Id}\"")); + if ( !replaceNakedNames ) + return json; + + json = Data.CalculatedFields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"{m.Key}\"", m.Value.Formula)); + if (Data.UseMapping) + json = Data.Fields.OrderBy(x => -x.Key.Length).Aggregate(json, (current, m) => current.Replace($"\"{m.Key}\"", $"\"f{m.Value.Id}\"")); + + return json; + } + + public void ClearCalculatedFields() => Data.CalculatedFields.Clear(); + + public void AddCalculatedField(string name, string formula, string drillDown, string []? lookupDef = null, string aggregationOperator = "$sum") => Data.CalculatedFields[name] = new(formula, drillDown, lookupDef, aggregationOperator); + + public bool IsCalculated(string name) => Data.CalculatedFields.ContainsKey(name); + + public string GetDrilldown(string column, string value = "\"\"", bool equals = false) + { + if ( Data.CalculatedFields.TryGetValue(column, out var field) ) + return field.DrillDown; + + Data.Fields.TryGetValue(column, out var mapping); + var isDouble = mapping != null && (mapping.Type == typeof(double) || mapping.Type == typeof(float) || mapping.Type == typeof(decimal)); + + var name = Data.Fields.ContainsKey( column.Replace(" ", ".") ) + ? column.Replace(" ", ".") + : column; + + if (equals) + { + //TODO: this compares all double with ==. This may not be ideal, but I don't know how to implement abs(field -value) < TOLERANCE in $filter step + return $"{{ \"{name}\": {value} }}"; // all the rest including integers and strings + } + + return isDouble + ? $"{{ \"$or\" : [ {{ \"{name}\": {{ \"$lte\" : {-DrilldownTolerance} }} }}, {{ \"{name}\": {{ \"$gte\" : {DrilldownTolerance} }} }} ] }}" // double = special + : $"{{ \"{name}\": {{ \"$ne\" : {value} }} }}"; // all the rest including integers and strings + } + + public string GetLookup(string name) + { + var lookups = Data.CalculatedFields[name].LookupDef + .Where(Data.Lookups.ContainsKey) + .Distinct() + .Select(x => Data.Lookups[x]) + ; + + return IsCalculated( name ) + ? string.Join( ", " , lookups) + : "" + ; + } + + public string GetFormula(string name) => Data.CalculatedFields[name].Formula; + + public string GetAggregationOperator(string name) => + IsCalculated(name) + ? Data.CalculatedFields[name].AggregationOperator + : "$sum"; + + public void AddLookup( string elem, string stages ) => Data.Lookups[elem] = stages; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/FieldMappingData.cs b/Rms.Risk.Mango.Pivot.Core/Models/FieldMappingData.cs new file mode 100644 index 0000000..2f8de47 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/FieldMappingData.cs @@ -0,0 +1,97 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +[BsonIgnoreExtraElements] +public class FieldMappingData : ICloneable +{ + public DateTime CachedAt { get; set; } + public bool UseMapping { get; set; } + + public Dictionary Fields { get; set; } = new( StringComparer.OrdinalIgnoreCase ); + public Dictionary CalculatedFields { get; set; } = new( StringComparer.OrdinalIgnoreCase ); + public Dictionary Lookups { get; set; } = new( StringComparer.OrdinalIgnoreCase ); + + /// + /// Call after loading from Json/Bson to replace # in field names to . + /// + public void PostLoad() + { + var fields = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var calculatedFields = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var lookups = new Dictionary( StringComparer.OrdinalIgnoreCase ); + + foreach ( var x in Fields ) + fields.Add( x.Key.Replace( "#", "." ), x.Value ); + foreach ( var x in CalculatedFields ) + calculatedFields.Add( x.Key.Replace( "#", "." ), x.Value ); + foreach ( var x in Lookups ) + lookups.Add( x.Key.Replace( "#", "." ), x.Value ); + + Fields = fields; + CalculatedFields = calculatedFields; + Lookups = lookups; + } + + /// + /// Call before saving to Json/Bson to replace . in field names to # + /// + public void PreSave() + { + var now = DateTime.Now; + CachedAt = new(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc); + + var fields = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var calculatedFields = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var lookups = new Dictionary( StringComparer.OrdinalIgnoreCase ); + + foreach ( var x in Fields ) + fields.Add( x.Key.Replace( ".", "#" ), x.Value ); + foreach ( var x in CalculatedFields ) + calculatedFields.Add( x.Key.Replace( ".", "#" ), x.Value ); + foreach ( var x in Lookups ) + lookups.Add( x.Key.Replace( ".", "#" ), x.Value ); + + Fields = fields; + CalculatedFields = calculatedFields; + Lookups = lookups; + } + + object ICloneable.Clone() => Clone(); + + public FieldMappingData Clone() + { + var d = new FieldMappingData + { + CachedAt = CachedAt, + UseMapping = UseMapping + }; + + foreach ( var x in Fields ) + d.Fields.Add( x.Key, x.Value.Clone() ); + foreach ( var x in CalculatedFields ) + d.CalculatedFields.Add( x.Key, x.Value.Clone() ); + foreach ( var x in Lookups ) + d.Lookups.Add( x.Key, x.Value ); + + return d; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/FilterExpressionTree.cs b/Rms.Risk.Mango.Pivot.Core/Models/FilterExpressionTree.cs new file mode 100644 index 0000000..03cd032 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/FilterExpressionTree.cs @@ -0,0 +1,677 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.ComponentModel; +using System.Data; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using MongoDB.Bson; +using MongoDB.Bson.IO; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public static class FilterExpressionTree +{ + public const string IsoDatePrefix = "ISODate(\""; + public const string IsoDateSuffix = "\")"; + + + public interface IFilterExpression + { + List Children { get; } + IFilterExpression Clone(); + } + + public sealed class ExpressionGroup : IFilterExpression, ICloneable + { + public enum ConditionType { And, Or } + + public ConditionType Condition { get; set; } = ConditionType.And; + public List Children { get; set; } = []; + + object ICloneable.Clone() => Clone(); + + public IFilterExpression Clone() + { + var clone = new ExpressionGroup + { + Condition = Condition + }; + + clone.Children.AddRange(Children.Select(x => x.Clone())); + + return clone; + } + + public override string ToString() + { + if (IsEmpty) + return string.Empty; + + var sb = new StringBuilder(); + sb.Append("( "); + + for (var i = 0; i < Children.Count; i++) + { + sb.Append(Children[i]); + if (i < Children.Count - 1) + { + sb.Append(Condition == ConditionType.And ? " AND " : " OR "); + } + } + + sb.Append(" )"); + return sb.ToString(); + } + + public bool IsEmpty => Children.Count == 0; + + public string ToJson(Dictionary fieldTypes) => FilterExpressionTree.ToJson(this, fieldTypes); + } + + public enum FieldConditionType + { + [Description( "Contains") ] Contains, + [Description( "Starts with") ] StartsWith, + [Description( "Ends with") ] EndsWith, + [Description( "==") ] EqualTo, + [Description( "!=") ] NotEqualTo, + [Description( ">") ] GreaterThan, + [Description( "<") ] LessThan, + [Description( ">=") ] GreaterThanOrEqualTo, + [Description( "<=") ] LessThanOrEqualTo, + [Description( "Is empty") ] IsEmpty, + [Description( "Not is empty") ] NotIsEmpty, + [Description( "Is null") ] IsNull, + [Description( "Not is null") ] NotIsNull, + [Description( "Matches") ] Matches, + [Description( "Does not match") ] DoesNotMatch, + [Description( "Does not contain") ] DoesNotContain, + [Description( "Does not start with") ] DoesNotStartWith, + [Description( "Does not end with") ] DoesNotEndWith, + } + + public sealed class FieldExpression : IFilterExpression, ICloneable + { + public List Children => []; + + public FieldConditionType Condition { get; set; } = FieldConditionType.EqualTo; + + public string Field + { + get; + set + { + if (field == value) + return; + field = value.Trim(); + } + } = ""; + + public string Argument + { + get; + set + { + if (field == value) + return; + field = value.Trim(); + } + } = ""; + + object ICloneable.Clone() => Clone(); + + public IFilterExpression Clone() + { + var clone = new FieldExpression + { + Condition = Condition, + Field = Field, + Argument = Argument + }; + + return clone; + } + + public override string ToString() + { + var description = Condition.GetType() + .GetField(Condition.ToString()) + ?.GetCustomAttributes(typeof(DescriptionAttribute), false) + .FirstOrDefault() as DescriptionAttribute; + + return $"{Field} {description?.Description.ToLower() ?? Condition.ToString()} {Argument}"; + } + + } + + public static ExpressionGroup ParseJson( string filterText ) + { + if (string.IsNullOrWhiteSpace(filterText)) + { + return new(); + } + + BsonDocument? bson; + try + { + bson = BsonDocument.Parse( filterText ); + + var e = bson.Elements.First(); + if ( e.Name != "$and" ) + bson = null; + } + catch ( Exception ) + { + // ignore + bson = null; + } + + if ( bson == null ) + { + try + { + bson = BsonDocument.Parse($"{{ \"$and\" : [ {filterText } ] }}"); + } + catch ( Exception ) + { + return new(); + } + } + + try + { + if (ParseCondition( bson.Elements.First() ) is ExpressionGroup cond) + return cond; + } + catch ( Exception ) + { + // ignore + } + + return new(); + } + + public static string ToJson(IFilterExpression? filter, IDictionary fieldTypes) + { + if (filter == null || (filter is ExpressionGroup grp && (grp.Children?.Count ?? 0) == 0)) + return ""; + + var bson = MakeJsonExpression(filter, fieldTypes); + if ( bson == null ) + return ""; + + return bson.ElementCount > 0 + ? bson.ToJson(new() { Indent = true, OutputMode = JsonOutputMode.RelaxedExtendedJson }) + : ""; + } + + public static string ToSQL(ExpressionGroup parsedFilter, IDictionary fieldTypes) + { + if (parsedFilter == null || (parsedFilter.Children?.Count ?? 0) == 0) + return ""; + + var sql = MakeSqlExpression(parsedFilter, fieldTypes, 0); + return sql; + } + + private static IFilterExpression? ParseCondition(BsonElement element) + { + switch (element.Name) + { + case "$and": + case "$or": + { + var cond = new ExpressionGroup + { + Condition = element.Name == "$and" + ? ExpressionGroup.ConditionType.And + : ExpressionGroup.ConditionType.Or + }; + + foreach (var e in element.Value.AsBsonArray.Select(x => x.AsBsonDocument.First())) + { + var expr = ParseCondition(e); + if ( expr == null ) + continue; + + cond.Children.Add( expr ); + } + + return cond; + } + + + default: + { + var fieldName = element.Name; + if ( !element.Value.IsBsonDocument ) + { + var val = element.Value.ToString() ?? ""; + + if ( val.StartsWith( "/" ) && val.EndsWith( "/" )) + return ParsePropertyExpression(fieldName, "$regex", val); + return ParsePropertyExpression( fieldName, "$eq", val ); + } + + var e = element.Value.AsBsonDocument.Elements.First(); + var op = e.Name; + var arg = e.Value.ToString() ?? ""; + return ParsePropertyExpression( fieldName, op, arg ); + } + } + } + + private static IFilterExpression? ParsePropertyExpression( string property, string op, string arg ) + { + switch (op) + { + case "$ne": + return CreateBinaryExpression(property, arg, FieldConditionType.NotEqualTo); + case "$eq": + return CreateBinaryExpression(property, arg, FieldConditionType.EqualTo); + case "$lte": + return CreateBinaryExpression(property, arg, FieldConditionType.LessThanOrEqualTo); + case "$lt": + return CreateBinaryExpression(property, arg, FieldConditionType.LessThan); + case "$gte": + return CreateBinaryExpression(property, arg, FieldConditionType.GreaterThanOrEqualTo); + case "$gt": + return CreateBinaryExpression(property, arg, FieldConditionType.GreaterThan); + case "$regex": + { + arg = arg.Trim( '/' ); + var newCond = RegexToCondition( arg, out var newArg ); + + return CreateBinaryExpression( property, newArg ?? arg, newCond ); + } + default: + return null; + } + } + + private static IFilterExpression CreateBinaryExpression(string property, string arg, FieldConditionType c) + => new FieldExpression {Field = property, Condition = c, Argument = arg}; + + + private static string Shield(string s) => + s + .Replace("\\", "\\\\") // first + .Replace(".", "\\.") + .Replace("(", "\\(") + .Replace(")", "\\)") + .Replace("*", "\\*") + .Replace("?", "\\?") + .Replace("[", "\\]") + .Replace("]", "\\]"); + + private static string Unshield(string s) => + s + .Replace("\\.", ".") + .Replace("\\(", "(") + .Replace("\\)", ")") + .Replace("\\*", "*") + .Replace("\\?", "?") + .Replace("\\]", "[") + .Replace("\\]", "]") + .Replace("\\\\", "\\"); // last + + private static FieldConditionType RegexToCondition( string regex, out string? arg ) + { + arg = null; + switch ( regex ) + { + case "^$": + return FieldConditionType.IsEmpty; + case "^.+$": + return FieldConditionType.NotIsEmpty; + default: + if ( regex.StartsWith( "^.*" ) && regex.EndsWith(".*$")) + { + arg = Unshield(regex.Substring( 3, regex.Length - 3 - 3 )); + return FieldConditionType.Contains; + } + if (regex.StartsWith("^.*") && regex.EndsWith("$")) + { + arg = Unshield(regex.Substring(3, regex.Length - 3 - 1 )); + return FieldConditionType.EndsWith; + } + if (regex.StartsWith("^") && regex.EndsWith(".*$")) + { + arg = Unshield(regex.Substring(1, regex.Length - 1- 3)); + return FieldConditionType.StartsWith; + } + if (regex.StartsWith("^(?!") && regex.EndsWith(")$")) + { + arg = Unshield(regex.Substring(1, regex.Length - 4 - 2)); + return FieldConditionType.DoesNotMatch; + } + if (regex.StartsWith("^.*(?!") && regex.EndsWith(").*$")) + { + arg = Unshield(regex.Substring(1, regex.Length - 6 - 4)); + return FieldConditionType.DoesNotContain; + } + if (regex.StartsWith("^(?!") && regex.EndsWith(").*$")) + { + arg = Unshield(regex.Substring(1, regex.Length - 4 - 4)); + return FieldConditionType.DoesNotStartWith; + } + if (regex.StartsWith("^.*(?!") && regex.EndsWith(")$")) + { + arg = Unshield(regex.Substring(1, regex.Length - 6 - 2)); + return FieldConditionType.DoesNotEndWith; + } + return FieldConditionType.Matches; + } + } + + private static BsonDocument? MakeJsonExpression(IFilterExpression cond, IDictionary fieldTypes) + { + if (cond == null) + throw new InvalidExpressionException("Expected FieldExpression but got NULL" ); + + switch ( cond ) + { + case ExpressionGroup when cond.Children.Count == 0: + // empty group, return null + return null; + case ExpressionGroup when cond.Children is [FieldExpression { Condition: FieldConditionType.EqualTo }]: + // skip grouping if only one child + { + var fieldExpr = (FieldExpression)cond.Children[0]; + return [new BsonElement(fieldExpr.Field, ConvertValue(fieldExpr.Field, fieldExpr.Argument))]; + } + case ExpressionGroup group: + var items = cond.Children.Select( cond1 => MakeJsonExpression(cond1, fieldTypes) ).Where( x => x != null ).ToList(); + if ( items.Count == 0 ) + return null; + + var elem = new BsonElement(group.Condition == ExpressionGroup.ConditionType.And ? "$and" : "$or", new BsonArray(items)); + return [elem]; + } + + if ( cond is not FieldExpression propExpr ) + throw new InvalidExpressionException($"Expected FieldExpression but got {cond} ({cond.GetType()})" ); + + var prop = propExpr.Field; + var op = propExpr.Condition; + var val = propExpr.Argument; + + var arg = ConvertValue(prop, val); + + var o = ConvertCondition( op, arg.ToString() ?? "", out var regex ); + var d = new BsonDocument {new( o, regex ?? arg )}; + + return [new BsonElement(prop, d)]; + + BsonValue ConvertValue(string name, string value) + { + if (!fieldTypes.TryGetValue(name, out var propType)) + propType = typeof(string); + + BsonValue bsonValue; + if (propType == typeof(double)) + bsonValue = new BsonDouble(Convert.ToDouble(value)); + else if (propType == typeof(int)) + bsonValue = new BsonInt32(Convert.ToInt32(value)); + else if (propType == typeof(decimal)) + bsonValue = new BsonDouble(Convert.ToDouble(value)); + else if (propType == typeof(long)) + bsonValue = new BsonInt64(Convert.ToInt64(value)); + else bsonValue = propType == typeof(DateTime) || value.StartsWith(IsoDatePrefix) + ? new BsonDateTime(ConvertToDateTime(value)) + : new BsonString(value) + ; + return bsonValue; + } + } + + private const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; + + private static DateTime ConvertToDateTime(string val) + { + + if (val.StartsWith(IsoDatePrefix) && val.EndsWith(IsoDateSuffix)) + { + var inner = val.Substring(IsoDatePrefix.Length, val.Length - IsoDatePrefix.Length - IsoDateSuffix.Length); + return ConvertExact(inner); + } + + return ConvertExact(val); + + DateTime ConvertExact(string value) + { + var formats = new[] + { + "yyyy-MM-ddTHH:mm:ssZ", + "yyyy-MM-ddTHH:mm:ss.fffZ", + "yyyy-MM-ddTHH:mm:ssK", + "yyyy-MM-ddTHH:mm:ss.fffK", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss.fff", + "yyyy-MM-dd", + "yyyy/MM/ddTHH:mm:ssZ", + "yyyy/MM/ddTHH:mm:ss.fffZ", + "yyyy/MM/ddTHH:mm:ssK", + "yyyy/MM/ddTHH:mm:ss.fffK", + "yyyy/MM/dd HH:mm:ss", + "yyyy/MM/dd HH:mm:ss.fff", + "yyyy/MM/dd", + // really don't recommend these + "dd/MM/yyyy", + "dd/MM/yyyy HH:mm:ss", + "dd/MM/yyyy HH:mm:ss.fff", + "dd-MM-yyyy" + }; + + foreach ( var format in formats ) + { + if (DateTime.TryParseExact(value, format, null, DateTimeStyle, out var d)) + return DateTime.SpecifyKind(d, DateTimeKind.Utc); + } + throw new ApplicationException("Invalid date/time: " + value); + } + } + + private static string ConvertCondition(FieldConditionType op, string arg, out string? regex) + { + regex = null; + switch (op) + { + case FieldConditionType.EqualTo: + return "$eq"; + case FieldConditionType.NotEqualTo: + return "$ne"; + case FieldConditionType.LessThanOrEqualTo: + return "$lte"; + case FieldConditionType.LessThan: + return "$lt"; + case FieldConditionType.GreaterThanOrEqualTo: + return "$gte"; + case FieldConditionType.GreaterThan: + return "$gt"; + case FieldConditionType.Contains: + regex = $"^.*{Shield(arg)}.*$"; + return "$regex"; + case FieldConditionType.StartsWith: + regex = $"^{Shield(arg)}.*$"; + return "$regex"; + case FieldConditionType.EndsWith: + regex = $"^.*{Shield(arg)}$"; + return "$regex"; + case FieldConditionType.IsEmpty: + regex = "^$"; + return "$regex"; + case FieldConditionType.NotIsEmpty: + regex = "^.+$"; + return "$regex"; + case FieldConditionType.IsNull: + regex = "^$"; + return "$regex"; + case FieldConditionType.NotIsNull: + regex = "^.+$"; + return "$regex"; + case FieldConditionType.Matches: + return "$regex"; + case FieldConditionType.DoesNotMatch: + regex = $"^(?!{arg})$"; + return "$regex"; + case FieldConditionType.DoesNotContain: + // https://stackoverflow.com/a/406408 + regex = $"^((?!{Shield(arg)}).)*$"; + return "$regex"; + case FieldConditionType.DoesNotStartWith: + regex = $"^(?!{Shield(arg)}).*$"; + return "$regex"; + case FieldConditionType.DoesNotEndWith: + regex = $"^.*(?!{Shield(arg)})$"; + return "$regex"; + default: + throw new ApplicationException($"Unsupported operation {op}"); + } + } + + /// + /// https://www.codeproject.com/Tips/483763/Equivalent-function-of-mysql-real-escape-string-in + /// + /// + /// + private static string ShieldSql(string str) => + Regex.Replace(str, @"[\x00'""\b\n\r\t\cZ\\%_]", + delegate(Match match) + { + var v = match.Value; + switch (v) + { + case "\x00": // ASCII NUL (0x00) character + return "\\0"; + case "\b": // BACKSPACE character + return "\\b"; + case "\n": // NEWLINE (linefeed) character + return "\\n"; + case "\r": // CARRIAGE RETURN character + return "\\r"; + case "\t": // TAB + return "\\t"; + case "\u001A": // Ctrl-Z + return "\\Z"; + default: + return "\\" + v; + } + }); + + private static string MakeSqlExpression(IFilterExpression filter, IDictionary fieldTypes, int level) + { + var prefix = new string(' ', level); + if (filter is ExpressionGroup group) + { + var sb = new StringBuilder(); + if ( group.Condition == ExpressionGroup.ConditionType.And ) + { + sb.AppendJoin( + $"\n\t{prefix}AND ", + group.Children.Select(x => MakeSqlExpression(x, fieldTypes, level+1))); + } + else + { + sb.AppendLine($"\n\t{prefix}("); + sb.AppendJoin( + $"\n\t{prefix}OR ", + group.Children.Select(x => MakeSqlExpression(x, fieldTypes, level+1))); + sb.AppendLine($"\n\t{prefix})"); + } + return sb.ToString(); + } + + var expr = (FieldExpression) filter; + var s = $"\t{prefix}{expr.Field} {GetSqlCondition(expr, fieldTypes)}"; + return s; + } + + private static string GetSqlArg(FieldExpression expr, IDictionary fieldTypes) + { + if (expr.Argument == null) + return "''"; + + if (!fieldTypes.TryGetValue(expr.Field, out var propType)) + propType = typeof(string); + + if (propType == typeof(string)) + return $"'{ShieldSql(expr.Argument)}'"; + if (propType == typeof(double)) + return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToDouble(expr.Argument).ToString(CultureInfo.InvariantCulture); + if (propType == typeof(int)) + return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToInt32(expr.Argument).ToString(CultureInfo.InvariantCulture); + if (propType == typeof(decimal)) + return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToDouble(expr.Argument).ToString(CultureInfo.InvariantCulture); + if (propType == typeof(long)) + return string.IsNullOrWhiteSpace(expr.Argument) ? "0" : Convert.ToInt64(expr.Argument).ToString(CultureInfo.InvariantCulture); + if (propType == typeof(DateTime)) + return string.IsNullOrWhiteSpace(expr.Argument) + ? "0" + : $"timestamp( '{ToDateTime(expr):yyyy-MM-dd HH:mm:ss}' )"; // should work for Oracle and ClickHouse + if (propType == typeof(DateOnly)) + return string.IsNullOrWhiteSpace(expr.Argument) + ? "0" + : $"timestamp( '{ToDateOnly(expr):yyyy-MM-dd}' )"; // should work for Oracle and ClickHouse + return ShieldSql(expr.Argument); + } + + private static DateTime ToDateTime(FieldExpression expr) + { + try + { + return DateTime.Parse(expr.Argument, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + catch (FormatException) + { + return DateTime.ParseExact(expr.Argument, "dd-MM-yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + } + + private static DateOnly ToDateOnly(FieldExpression expr) + => DateOnly.Parse(expr.Argument, CultureInfo.InvariantCulture); + + private static string GetSqlCondition(FieldExpression expr, IDictionary fieldTypes) + { + var arg = GetSqlArg(expr, fieldTypes); + return expr.Condition switch + { + FieldConditionType.EqualTo => $"= {arg}", + FieldConditionType.NotEqualTo => $"!= {arg}", + FieldConditionType.LessThanOrEqualTo => $"<= {arg}", + FieldConditionType.LessThan => $"< {arg}", + FieldConditionType.GreaterThanOrEqualTo => $">= {arg}", + FieldConditionType.GreaterThan => $"> {arg}", + FieldConditionType.Contains => $"LIKE '%{ShieldSql(expr.Argument)}%'", + FieldConditionType.StartsWith => $"LIKE '{ShieldSql(expr.Argument)}%'", + FieldConditionType.EndsWith => $"LIKE '%{ShieldSql(expr.Argument)}'", + FieldConditionType.IsEmpty => "= ''", + FieldConditionType.NotIsEmpty => "!= ''", + FieldConditionType.IsNull => "IS NULL", + FieldConditionType.NotIsNull => "IS NOT NULL", + FieldConditionType.Matches => $"LIKE '{ShieldSql(expr.Argument)}'", + FieldConditionType.DoesNotMatch => $"NOT LIKE '{ShieldSql(expr.Argument)}'", + FieldConditionType.DoesNotContain => $"NOT LIKE '%{ShieldSql(expr.Argument)}%'", + FieldConditionType.DoesNotStartWith => $"NOT LIKE '{ShieldSql(expr.Argument)}%'", + FieldConditionType.DoesNotEndWith => $"NOT LIKE '%{ShieldSql(expr.Argument)}'", + _ => throw new ApplicationException($"Unsupported operation {expr.Condition}") + }; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/IndexInfoModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/IndexInfoModel.cs new file mode 100644 index 0000000..6d5250e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/IndexInfoModel.cs @@ -0,0 +1,92 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class IndexesInfoModel +{ + public List Indexes { get; set; } = new(); + + public static IndexesInfoModel FromBson(BsonDocument bsonDocument) + { + var model = new IndexesInfoModel(); + var indexesArray = bsonDocument["cursor"]["firstBatch"].AsBsonArray; + + foreach (var indexElement in indexesArray) + { + var indexDoc = indexElement.AsBsonDocument; + var index = new IndexInfoModel + { + Version = indexDoc.Contains("v") ? indexDoc["v"].AsInt32 : 0, + Key = indexDoc.Contains("key") && indexDoc["key"].IsBsonDocument + ? indexDoc["key"].AsBsonDocument.ToDictionary(k => k.Name, v => v.Value.ToString() ?? "") + : new Dictionary(), + Name = indexDoc.Contains("name") ? indexDoc["name"].AsString : string.Empty, + ExpireAfterSeconds = indexDoc.Contains("expireAfterSeconds") ? indexDoc["expireAfterSeconds"].ToNullableInt32() : null + }; + model.Indexes.Add(index); + } + + return model; + } + + public void CopyFrom(IndexesInfoModel source) + { + Indexes = source.Indexes.Select(index => index.Clone()).ToList(); + } + + public IndexesInfoModel Clone() + { + var clone = new IndexesInfoModel(); + clone.CopyFrom(this); + return clone; + } +} + +public class IndexInfoModel +{ + public int Version { get; set; } + public Dictionary Key { get; set; } = new(); + public string Name { get; set; } = string.Empty; + public int? ExpireAfterSeconds { get; set; } + + public void CopyFrom(IndexInfoModel source) + { + Version = source.Version; + Key = new Dictionary(source.Key); + Name = source.Name; + ExpireAfterSeconds = source.ExpireAfterSeconds; + } + + public IndexInfoModel Clone() + { + var clone = new IndexInfoModel(); + clone.CopyFrom(this); + return clone; + } +} + +public static class BsonExtensions +{ + public static int? ToNullableInt32(this BsonValue value) + { + return value.IsBsonNull ? (int?)null : value.AsInt32; + } +} diff --git a/Rms.Risk.Mango.Pivot.Core/Models/LogsModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/LogsModel.cs new file mode 100644 index 0000000..70778a8 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/LogsModel.cs @@ -0,0 +1,47 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class LogRecordModel +{ + + public DateTime Time { get; set; } + public string Severity { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public long Id { get; set; } + public string Svc { get; set; } = string.Empty; + public string Ctx { get; set; } = string.Empty; + public string Msg { get; set; } = string.Empty; + public BsonDocument? Attr { get; set; } + + public static LogRecordModel FromBson(BsonDocument doc) => + new () + { + Time = doc.Contains("t") ? doc["t"].ToUniversalTime() : default, + Severity = doc.Contains("s") ? doc["s"].AsString : string.Empty, + Category = doc.Contains("c") ? doc["c"].AsString : string.Empty, + Id = doc.Contains("id") ? doc["id"].ToInt64() : 0L, + Svc = doc.Contains("svc") ? doc["svc"].AsString : string.Empty, + Ctx = doc.Contains("ctx") ? doc["ctx"].AsString : string.Empty, + Msg = doc.Contains("msg") ? doc["msg"].AsString : string.Empty, + Attr = doc.Contains("attr") ? doc["attr"].AsBsonDocument : null + }; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/MongoDatabaseInfo.cs b/Rms.Risk.Mango.Pivot.Core/Models/MongoDatabaseInfo.cs new file mode 100644 index 0000000..df2cbd6 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/MongoDatabaseInfo.cs @@ -0,0 +1,32 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text.Json.Serialization; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class MongoDatabaseInfo +{ + [JsonPropertyName("name")] public string Name { get; set; } = ""; + + [JsonPropertyName( "sizeOnDisk")] + public decimal SizeOnDisk { get; set; } + + [JsonPropertyName( "empty")] + public bool Empty { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/MongoDbCachingHelper.cs b/Rms.Risk.Mango.Pivot.Core/Models/MongoDbCachingHelper.cs new file mode 100644 index 0000000..5ac4f49 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/MongoDbCachingHelper.cs @@ -0,0 +1,161 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public static class MongoDbCachingHelper +{ + public static async Task LoadCachedCobDatesAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default) + { + var coll = source.GetCollectionWithRetries(collectionName+"-Meta"); + var cursor = await coll.FindAsync("{ _id : \"CachedCobDates\"}", cancellationToken: token); + + while ( await cursor.MoveNextAsync(token) ) + { + var batch = cursor.Current; + foreach ( var doc in batch ) + { + if ( doc.IsBsonNull ) + return []; + + var res = doc.ToDictionary(); + + return res["CobDates"] is not object[] cobs + ? [] + : cobs.Select( x => x as string ) + .Where( x => !string.IsNullOrWhiteSpace( x ) ) + .OfType() + .ToArray() + ; + } + } + return []; + } + + private class CachedDepartmentsDoc + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [BsonId] public string Id { get; set; } = ""; + public string[] Departments { get; set; } = []; + public DateTime CachedOnUtc { get; set; } + } + + private const string CachedDepartmentsDocName = "CachedDepartments"; + + public static async Task> LoadCachedDepartmentsAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default) + { + var coll = source.GetCollectionWithRetries(collectionName+"-Meta"); + var cursor = await coll.FindAsync($"{{ _id : \"{CachedDepartmentsDocName}\"}}", cancellationToken: token); + + while ( await cursor.MoveNextAsync(token) ) + { + var batch = cursor.Current; + foreach ( var doc in batch ) + { + var expireAt = doc.CachedOnUtc + TimeSpan.FromHours(1); + var isStillValid = expireAt > DateTime.UtcNow; + + return Tuple.Create( + isStillValid, + doc.Departments.Where( x => !string.IsNullOrWhiteSpace( x ) ).ToArray() + ); + } + } + return Tuple.Create(false, Array.Empty()); + } + + public static async Task CacheDepartments(this MongoDbDataSource source, string collectionName, string [] departments, CancellationToken token = default) + { + if ( departments.Length == 0 ) + return; + + var doc = new CachedDepartmentsDoc() + { + Id = CachedDepartmentsDocName, + CachedOnUtc = DateTime.UtcNow, + Departments = departments + }; + + var coll = source.GetCollectionWithRetries(collectionName + "-Meta"); + await coll.ReplaceOneAsync( $"{{ _id : \"{CachedDepartmentsDocName}\"}}", doc, new ReplaceOptions {IsUpsert = true}, token); + } + + private class CachedDesksDoc + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [BsonId] public string Id { get; set; } = ""; + public (string, string)[] DeskAndDepartment { get; set; } = []; + public DateTime CachedOnUtc { get; set; } + } + + private const string CachedDesksDocName = "CachedDesks"; + + public static async Task> LoadCachedDesksAsync(this MongoDbDataSource source, string collectionName, CancellationToken token = default) + { + var coll = source.GetCollectionWithRetries(collectionName+"-Meta"); + var cursor = await coll.FindAsync($"{{ _id : \"{CachedDesksDocName}\"}}", cancellationToken: token); + + while ( await cursor.MoveNextAsync(token) ) + { + var batch = cursor.Current; + foreach ( var doc in batch ) + { + var expireAt = doc.CachedOnUtc + TimeSpan.FromHours(24); + var isStillValid = expireAt > DateTime.UtcNow; + + return Tuple.Create( + isStillValid, + doc.DeskAndDepartment.Where( x => !string.IsNullOrWhiteSpace( x.Item1 ) && + !string.IsNullOrWhiteSpace( x.Item2 )).ToArray() + ); + } + } + return Tuple.Create(false, Array.Empty<(string, string)>()); + } + + public static async Task CacheDesks(this MongoDbDataSource source, string collectionName, (string, string) [] desks, CancellationToken token = default) + { + if ( desks.Length == 0 ) + return; + + var doc = new CachedDesksDoc() + { + Id = CachedDesksDocName, + CachedOnUtc = DateTime.UtcNow, + DeskAndDepartment = desks + }; + + var coll = source.GetCollectionWithRetries(collectionName + "-Meta"); + await coll.ReplaceOneAsync( $"{{ _id : \"{CachedDesksDocName}\"}}", doc, new ReplaceOptions {IsUpsert = true}, token); + } + + public static async Task CacheCobDates(this MongoDbDataSource source, string collectionName, string [] cobs, CancellationToken token = default) + { + if ( cobs.Length == 0 ) + return; + + var doc = new BsonDocument(new Dictionary { ["CobDates"] = cobs}); + var coll = source.GetCollectionWithRetries(collectionName + "-Meta"); + await coll.ReplaceOneAsync( "{_id : \"CachedCobDates\"}", doc, new ReplaceOptions {IsUpsert = true}, token); + } + +} diff --git a/Rms.Risk.Mango.Pivot.Core/Models/MongoDbDataSource.cs b/Rms.Risk.Mango.Pivot.Core/Models/MongoDbDataSource.cs new file mode 100644 index 0000000..a03f2df --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/MongoDbDataSource.cs @@ -0,0 +1,1513 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +/// +/// Implementation of IPivotTableDataSource that goes direct to the mongo database +/// +public class MongoDbDataSource : IPivotTableDataSource, IPivotTableDataSourceMetaProvider +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + // ReSharper disable InconsistentNaming + public static int PivotCacheTTLHours = 1; + public static TimeSpan MetaCacheTTL = TimeSpan.FromMinutes(30); + public static int PivotMaxReturnedRows = 500_000; + public static string SourcePrefix = "Forge"; + public static string CacheCollectionName = "dbMango-Pivot-Cache"; + // ReSharper restore InconsistentNaming + + private const int MaxCachedRows = 25000; + + private readonly MongoDbConfigRecord _config; + private readonly MongoDbSettings _settings; + private readonly string _databaseInstance; + private IMongoDatabase? _database; + private readonly Lock _syncObject = new(); + private readonly List _allMeta = []; + private DateTime _allMetaValidUntil = DateTime.MinValue; + + + public string User { get; set; } = ""; + + readonly SemaphoreSlim _threadLock = new(1); + + internal IMongoDatabase Database + { + get + { + if (_database != null) + return _database; + + lock (_syncObject) + { + Thread.MemoryBarrier(); + + if (_database != null) + return _database; + + return _database = MongoDbHelper.GetDatabase(_config, _settings, _databaseInstance); + } + } + } + + public string SourceId => _config!.GetKey(_databaseInstance); + public string Prefix => SourcePrefix; + public override string ToString() => SourceId; + + private readonly ConcurrentDictionary _fieldMap = new(); + + public MongoDbDataSource(MongoDbConfigRecord config, MongoDbSettings settings, string? databaseInstance) + { + _config = config; + _settings = settings; + _databaseInstance = databaseInstance ?? config.MongoDbDatabase; + _config.Check(); + } + + private async Task InitAsync(string collectionName, bool skipCache, CancellationToken token = default ) + { + //multiple callers call this method, so ensure only 1 at a time passes through this logic + await _threadLock.WaitAsync(token); + + try + { + // Force test connection, and reset _database if disconnected + if (!MongoDbHelper.IsConnected(_database, true)) + _database = null; + + if (_fieldMap.ContainsKey(collectionName) && !skipCache) + return; + + var sw = Stopwatch.StartNew(); + + _log.Debug($"Loading field map for Collection=\"{collectionName}\" MongoDb=\"{_config?.MongoDbDatabase}\" URL=\"{_config?.MongoDbUrl}\""); + + if (await LoadCachedFieldMappingAsync(collectionName, skipCache, token)) + { + sw.Stop(); + _log.Info($"Loaded cached field map for Collection=\"{collectionName}\" FieldDefs={_fieldMap[collectionName].Count} CalcFields={_fieldMap[collectionName].CalculatedFields.Count()} Lookups={_fieldMap[collectionName].Data.Lookups.Count} MongoDb=\"{_config?.MongoDbDatabase}\" URL=\"{_config?.MongoDbUrl}\"" + + $"\tElapsed=\"{sw.Elapsed}\"" + ); + return; + } + + //no cached field mappings found, so create it and store it in the db + var swUpdateFieldMappings = Stopwatch.StartNew(); + await UpdateFieldMappings(collectionName, skipCache, token); + swUpdateFieldMappings.Stop(); + + var swUpdateLookups = Stopwatch.StartNew(); + var lookups = await UpdateLookups(collectionName, token); + swUpdateLookups.Stop(); + + var swUpdateCalculatedFields = Stopwatch.StartNew(); + await UpdateCalculatedFields(collectionName, token); + swUpdateCalculatedFields.Stop(); + + await CacheFieldMappingAsync(collectionName, token); + + sw.Stop(); + _log.Info($"Created field map for Collection=\"{collectionName}\", FieldDefs={_fieldMap[collectionName].Count}, CalcFields={_fieldMap[collectionName].CalculatedFields.Count()}, Lookups={lookups}, MongoDb=\"{_config?.MongoDbDatabase}\", URL=\"{_config?.MongoDbUrl}\",\n" + + $"ElapsedMs=\"{sw.Elapsed.TotalMilliseconds}\",\n" + + $"UpdateFieldMappingsMs=\"{swUpdateFieldMappings.Elapsed.TotalMilliseconds}\",\n" + + $"UpdateLookupsMs=\"{swUpdateLookups.Elapsed.TotalMilliseconds}\",\n" + + $"UpdateCalculatedFieldsMs=\"{swUpdateCalculatedFields.Elapsed.TotalMilliseconds}\"\n" + ); + } + finally + { + _threadLock.Release(); + } + } + + public async Task> GetAllMeta(bool force = false, CancellationToken token = default) + { + if ( force ) + { + _allMeta.Clear(); + PivotMetaCache.Clear(); + } + + if (DateTime.Now > _allMetaValidUntil ) + _allMeta.Clear(); + + if ( _allMeta.Count > 0 ) + return _allMeta; + + await this.PreloadCollections(_allMeta, User, token); + _allMetaValidUntil = DateTime.Now + MetaCacheTTL; + + return _allMeta; + } + + + private async Task LoadCachedFieldMappingAsync(string collectionName, bool skipCache, CancellationToken token = default ) + { + if ( skipCache ) + return false; + + var coll = GetCollectionWithRetries(collectionName+"-Meta"); + var cursor = await coll.FindAsync("{ _id : \"CachedFieldMapping\"}", cancellationToken: token); + + while ( await cursor.MoveNextAsync(token) ) + { + var doc = cursor.Current.FirstOrDefault(); + if ( doc == null ) + return false; + + if ( doc.CachedAt.Date != DateTime.UtcNow.Date ) // cache is too old + return false; + + doc.PostLoad(); + _fieldMap[collectionName] = new( doc.UseMapping ) { Data = doc }; + return true; + } + return false; + } + + public async Task CacheFieldMappingAsync(string collectionName, CancellationToken token = default) + { + var doc = _fieldMap[collectionName].Data.Clone(); + doc.PreSave(); + + var coll = GetCollectionWithRetries(collectionName + "-Meta"); + await coll.ReplaceOneAsync( "{_id : \"CachedFieldMapping\"}", doc, new ReplaceOptions {IsUpsert = true}, token); + } + + private async Task UpdateLookups(string collectionName, CancellationToken token = default) + { + var coll = GetCollectionWithRetries($"{collectionName }-Meta"); + var doc = (await coll.FindAsync(new BsonDocument { { "_id", "Lookups" } }, cancellationToken: token)).FirstOrDefault(); + + if (doc == null) + return 0; + + var count = 0; + foreach (var elem in doc.Elements.Where(x => !x.Name.StartsWith("_")).Select(x => x.Name)) + { + try + { + var stages = doc[elem] + .ToJson() + .Replace( "@", "$" ) + .Replace( "#", "." ) + .TrimStart(' ', '\t', '\r', '\n', '[') + .TrimEnd( ' ', '\t', '\r', '\n', ']') + ; + + _fieldMap[collectionName].AddLookup( elem, stages ); + count += 1; + } + catch + { + // ignore + } + } + return count; + } + + private async Task UpdateCalculatedFields(string collectionName, CancellationToken token = default) + { + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + var doc = (await coll.FindAsync(new BsonDocument { { "_id", "CalculatedFields" } }, cancellationToken: token)).FirstOrDefault(); + + if (doc == null) + return; + + foreach (var elem in doc.Elements.Where(x => !x.Name.StartsWith("_")).Select(x => x.Name)) + { + try + { + var e = doc[elem].AsBsonDocument; + + var formula = e.GetValue("Formula", null)?.ToJson() + .Replace("@", "$") + ; + + if (string.IsNullOrWhiteSpace(formula)) + continue; + + var drill = e.GetValue("DrillDown", null)?.ToJson() + .Replace("@", "$") + .Replace("#", ".") + ?? "" + ; + + var lookupDef = e.GetValue("LookupDef", null)?.AsBsonArray + ; + + var aggregationOperator = e.GetValue("AggregationOperator", null)?.AsString ?? "$sum"; + + _fieldMap[collectionName].AddCalculatedField(elem, formula, drill, lookupDef?.Values.Select( x => x.AsString ).ToArray(), aggregationOperator); + } + catch + { + // ignore + } + } + } + + private async Task UpdateFieldMappings(string collectionName, bool skipCache, CancellationToken token = default ) + { + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + var doc = (await coll.FindAsync(new BsonDocument { { "_id", "FieldsMap" } }, cancellationToken: token)).FirstOrDefault(); + + if (doc == null) + { + _fieldMap[collectionName] = new(false); + return; + } + + var useIds = doc.Contains("_UseIds") ? doc["_UseIds"] : null; + + var res = new FieldMapping(useIds?.AsBoolean ?? false); + foreach (var elem in doc.Elements.Where(x => !x.Name.StartsWith("_")).Select(x => x.Name)) + { + var d = doc[elem].AsBsonDocument; + + res[elem] = new() + { + Id = d.Names.Any(x => x == "Id") ? d["Id"].ToInt32() : 0, + Type = ReinterpretType( d["Type"].ToString()! ), + Purpose = Convert(d["Purpose"].ToString()!) + }; + } + + _fieldMap[collectionName] = res; + + // add fields missing in the mapping but present in the data set + + await UpdateMissingFieldMappings(collectionName, skipCache, token); + } + + private static PivotFieldPurpose Convert( string s ) + { + switch (s.ToLower()) + { + case "key": + return PivotFieldPurpose.Key; + case "primary key 1": + return PivotFieldPurpose.PrimaryKey1; + case "primary key 2": + return PivotFieldPurpose.PrimaryKey2; + case "info": + return PivotFieldPurpose.Info; + case "hidden": + return PivotFieldPurpose.Hidden; + default: + return PivotFieldPurpose.Data; + } + } + + private async Task UpdateMissingFieldMappings(string collectionName, bool skipCache, CancellationToken token = default ) + { + if ( _fieldMap[collectionName].UseMapping ) // can't pick up missing fields + return; + + // collection does not use field ids. pick up missing fields + + var coll = GetCollectionWithRetries(collectionName); + + var cobs = await GetCobDatesAsync(collectionName, skipCache, token); + var filter = ""; + + if ( cobs.Length > 0 ) + filter = $"{{ $match : {{ \"COB\" : ISODate(\"{cobs[^1]}\") }} }}, "; + + var json = "["+filter+"{ $sample: { size: 16 } }]"; + var pipeline = MongoDB.Bson.Serialization.BsonSerializer.Deserialize(json).Select(p => p.AsBsonDocument).ToList(); + + try + { + var options = new AggregateOptions + { + BypassDocumentValidation = true, + //AllowDiskUse = true + }; + + using var cursor = await coll.AggregateAsync( pipeline, options, token); + while ( await cursor.MoveNextAsync(token) ) + { + var batch = cursor.Current; + foreach ( var doc in batch ) + { + UpdateMissingFieldMappings(collectionName, doc ); + } + } + } + catch ( Exception ) + { + // ignore + } + } + + public void UpdateMissingFieldMappings( + string collectionName, + BsonDocument doc, + string prefix = "") + { + foreach ( var element in doc.Elements ) + { + if ( element.Name == "_id" ) + continue; + + + var name = prefix == "" + ? element.Name + : $"{prefix}.{element.Name}" + ; + + if ( !_fieldMap.ContainsKey(collectionName)) + _fieldMap[collectionName] = new(false); + + if (_fieldMap[collectionName].ContainsKey(name) ) + continue; + + var shouldIgnore = _fieldMap[collectionName].Fields + .Any( x => x.Value.Purpose == PivotFieldPurpose.Hidden && name.IndexOf( x.Key + ".", StringComparison.Ordinal ) == 0 ); + + if ( shouldIgnore ) + continue; + + var val = element.Value; + Type t; + switch ( val.BsonType ) + { + case BsonType.Double : t = typeof(double); break; + case BsonType.String : t = typeof(string); break; + case BsonType.Document : UpdateMissingFieldMappings(collectionName, val.AsBsonDocument, name ); continue; + case BsonType.Boolean : t = typeof(bool); break; + case BsonType.DateTime : t = typeof(DateTime); break; + case BsonType.Null : continue; // this can be an object + case BsonType.Symbol : t = typeof(string); break; + case BsonType.Int32 : t = typeof(int); break; + case BsonType.Timestamp : t = typeof(DateTime); break; + case BsonType.Int64 : t = typeof(long); break; + default : continue; + } + + UpdateMappingIfFieldIsMissing( collectionName, name, t); + } + } + + public void UpdateMappingIfFieldIsMissing( + string collectionName, + string name, + Type t + ) + { + if ( !_fieldMap.ContainsKey(collectionName)) + _fieldMap[collectionName] = new(false); + + if (_fieldMap[collectionName].ContainsKey(name) ) return; + + var shouldIgnore = _fieldMap[collectionName].Fields + .Any( x => x.Value.Purpose == PivotFieldPurpose.Hidden && name.IndexOf( x.Key + ".", StringComparison.Ordinal ) == 0 ); + + if (shouldIgnore) return; + + var isNumber = t == typeof(double) + || t == typeof(decimal) + || t == typeof(int) + || t == typeof(long) + ; + + _fieldMap[collectionName][name] = new() + { + Id = 0, + Purpose = isNumber + ? PivotFieldPurpose.Data + : PivotFieldPurpose.Info, + Type = t + }; + } + + + private static Type ReinterpretType( string type ) + { + var t = Type.GetType( type ); + if ( t != null ) + return t; + + switch ( type ) + { + case "double": + return typeof(double); + case "string": + return typeof(string); + case "int32": + return typeof(int); + case "int64": + return typeof(long); + case "decimal": + return typeof(decimal); + case "date": + return typeof(DateTime); + default: + return typeof(double); + } + } + + /// + /// CollectionType.All: If includeMeta is CollectionType.All, it simply returns all collection names as an array of strings. + /// The OfType_string_() ensures that only strings are included (though they should all be strings already). + /// + /// CollectionType.NoMeta: If includeMeta is CollectionType.NoMeta, it filters the collection names to include only those + /// that do not have a corresponding metadata collection (i.e., a collection with the same name plus "-Meta"). + /// + /// CollectionType.MetaOnly: If includeMeta is CollectionType.MetaOnly, it filters the collection names to include only those + /// that do have a corresponding metadata collection. + /// + public async Task GetCollectionsAsync(CollectionType includeMeta = CollectionType.All, CancellationToken token = default) + { + var cursor = await Database.ListCollectionsAsync(cancellationToken: token); + var list = await cursor.ToListAsync(cancellationToken: token); + var coll = list.Select( x => x["name"].ToString() ).OfType().ToHashSet(StringComparer.OrdinalIgnoreCase); + + var res = includeMeta switch + { + CollectionType.All => coll.OrderBy(x => x).ToArray(), + CollectionType.NoMeta => coll.Where(x => IsNotMeta(x) && !HaveMeta(coll,x)).OrderBy(x => x).ToArray(), + CollectionType.HaveMeta => coll.Where(x => IsNotMeta(x) && HaveMeta(coll,x)).OrderBy(x => x).ToArray(), + _ => throw new ArgumentOutOfRangeException(nameof(includeMeta), includeMeta, null) + }; + + return res; + + static bool IsNotMeta(string? name) => + !string.IsNullOrWhiteSpace(name) + && !name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase); + + static bool HaveMeta(HashSet allCollections, string? collectionName) => + allCollections.Contains(collectionName + "-Meta"); + } + + public async Task GetKeyFieldsAsync(string collectionName, CancellationToken token = default) + { + var fields = await GetFieldMapping(collectionName, token); + + if ( fields?.Count > 0 ) + return fields.Fields + .Where( x => !IsData( x.Value ) ) + .OrderBy( x => x.Value.Id ) + .Select( x => x.Key ) + .ToArray() + ; + + return new[] + { + "Department", "Location", "Book", "Currency", "TradeType", "SystemId", "Ver", "Expiry Date", "COB" + } + .OrderBy( x => x ) + .ToArray(); + } + + public FieldMapping GetFieldMapping(string collectionName) => + !_fieldMap.TryGetValue(collectionName, out var fields) + ? new(false) + : fields; + + public async Task GetFieldMapping(string collectionName, CancellationToken token) + { + if ( !_fieldMap.TryGetValue(collectionName, out var fields)) + { + await InitAsync(collectionName, false, token); + _fieldMap.TryGetValue(collectionName, out fields); + } + + return fields ?? throw new ApplicationException($"Mapping for Collection=\"{collectionName}\" is not found"); + } + + private static bool IsData( SingleFieldMapping x ) + { + if ( x.Purpose == PivotFieldPurpose.Key + || x.Purpose == PivotFieldPurpose.PrimaryKey1 + || x.Purpose == PivotFieldPurpose.PrimaryKey2 + || x.Purpose == PivotFieldPurpose.Info + ) + { + return false; + } + + return + x.Type == typeof(double) + || x.Type == typeof(decimal) + || x.Type == typeof(int) + || x.Type == typeof(long) + ; + } + + public async Task GetDrilldownKeyFieldsAsync(string collectionName,PivotFieldPurpose keyLevel, CancellationToken token = default) + { + var fields = await GetFieldMapping(collectionName, token); + if ( fields.Count > 0 ) + return fields.Fields + .Where( x => x.Value.Purpose == keyLevel ) + .OrderBy( x => x.Value.Id ) + .Select( x => x.Key ) + .ToArray(); + + return new[] + { + "SystemId", "Ver" + } + .OrderBy( x => x ) + .ToArray(); + } + + public async Task GetDrilldownAsync(string collectionName, string name, string nullValue = "\"\"", bool equals = false, CancellationToken token = default) + { + var fields = await GetFieldMapping(collectionName, token); + return fields.GetDrilldown(name, nullValue, equals ); + } + + public async Task GetDataFieldsAsync(string collectionName, CancellationToken token = default) + { + var fields = await GetFieldMapping(collectionName, token); + + if ( fields == null ) + return []; + + var fieldNames = fields.FieldNames; + var calculatedFields = fields.CalculatedFields; + var all = fieldNames.Concat(calculatedFields).ToList(); + all.Sort(); + var keys = new HashSet( await GetKeyFieldsAsync(collectionName, token) ); + return all.Where( x => x != "_id" && x != "id" && !keys.Contains( x ) ).ToArray(); + } + + private async Task InternalGetCobDatesAsync(string collectionName, CancellationToken token = default) + { + var coll = GetCollectionWithRetries(collectionName); + var emptyFilter = new FilterDefinitionBuilder().Empty; + + // using DateTime? in case we have a document without COB + // in this case Null can't be converted to DateTime and MongoDB driver throws an exception + var fields = await GetFieldMapping(collectionName, token); + var docs = (await coll.DistinctAsync( fields.MapField( "COB" ), emptyFilter, cancellationToken: token)).ToList(); + + var cobs = docs.Where(x => x != null).Select( x => x!.Value.ToString( "yyyy-MM-dd" ) ).ToArray(); + + return cobs; + } + + public async Task GetCobDatesAsync(string collectionName, bool force = false, CancellationToken token = default) + { + var cobs = force + ? [] + : await this.LoadCachedCobDatesAsync(collectionName, token) + ; + + // contains today or yesterday or last Friday if today is Monday + if ( + cobs.Contains( DateTime.Now.ToString( "yyyy-MM-dd" ) ) + || cobs.Contains( (DateTime.Now - TimeSpan.FromDays( 1 )).ToString( "yyyy-MM-dd" ) ) + || (DateTime.Now.DayOfWeek == DayOfWeek.Monday && + cobs.Contains( (DateTime.Now - TimeSpan.FromDays( 3 )).ToString( "yyyy-MM-dd" ) )) + ) + { + return cobs; + } + + cobs = await InternalGetCobDatesAsync(collectionName, token); + await this.CacheCobDates(collectionName, cobs, token); + + return cobs; + } + + public async Task GetDepartmentsAsync(string collectionName, CancellationToken token = default) + { + var cob = await GetCobDatesAsync(collectionName, token: token); + if ( cob.Length == 0 ) + return []; + + var (stillValid,cached) = await this.LoadCachedDepartmentsAsync(collectionName, token); + + if (stillValid && cached.Length > 0) + return cached; + + var coll = GetCollectionWithRetries(collectionName); + + var fields = await GetFieldMapping(collectionName, token); + + var filter = new FilterDefinitionBuilder().Eq(fields.MapField("COB"), DateTime.ParseExact(cob[^1], "yyyy-MM-dd", null, DateTimeStyles.AssumeUniversal)); + var docs = await (await coll.DistinctAsync(fields.MapField("Department"), filter, cancellationToken: token)).ToListAsync(cancellationToken: token); + var departments = docs.Concat(cached).Distinct().OrderBy(x => x).ToArray(); + + await this.CacheDepartments(collectionName, departments, token); + + return departments; + } + + public async Task<(string, string)[]> GetDesksWithDepartmentAsync(string collectionName, CancellationToken token = default) + { + var (stillValid,cached) = await this.LoadCachedDesksAsync(collectionName, token); + + if (stillValid) + return cached; + + var res = new List<(string, string)>(); + var coll = GetCollectionWithRetries(collectionName); + + try + { + var json = "[{ \"$group\": { \"_id\": { Department: \"$Department\", Desk: \"$Desk\" } } }]"; + var pipeline = MongoDB.Bson.Serialization.BsonSerializer.Deserialize(json) + .Select(p => p.AsBsonDocument).ToList(); + + var options = new AggregateOptions + { + BypassDocumentValidation = true, + //AllowDiskUse = true + }; + + using var cursor = await coll.AggregateAsync(pipeline, options, token); + + while ( await cursor.MoveNextAsync(token) ) + { + var batch = cursor.Current; + foreach ( var doc in batch ) + { + foreach ( var element in doc.Elements ) + { + var bsonValue = element.Value.AsBsonDocument; + + var desk = bsonValue.GetValue("Desk", null)?.ToString(); + var department = bsonValue.GetValue("Department", null)?.ToString(); + + if (!string.IsNullOrEmpty(desk) && !string.IsNullOrEmpty(department)) + res.Add((desk, department)); + } + } + } + } + catch + { + //do nothing + } + + var desks = res.ToArray(); + await this.CacheDesks(collectionName, desks, token); + + return desks; + } + + + public async Task PivotAsync( + string collectionName, + PivotDefinition def, + FilterExpressionTree.ExpressionGroup? extraFilter, + bool skipCache, + string? userName, + int maxFetchSize = -1, + CancellationToken token = default ) + { + var sw = new Stopwatch(); + sw.Start(); + + ArrayBasedPivotData? data; + + // preload field mapping. we'll need it in GetQueryText + _ = await GetFieldMapping(collectionName, token); + + if ( !skipCache ) + { + data = await GetCachedResultAsync(collectionName, def, extraFilter, GetQueryText, token); + if ( data != null ) + { + sw.Stop(); + _log.Info( $"Loaded from cache. User=\"{userName}\" Pivot=\"{def.Name}\" PivotType={def.PivotType} Collection=\"{collectionName}\" ExtraFilter=\"{extraFilter}\" executed in Elapsed=\"{sw.Elapsed}\"" ); + return data.Count == 0 + ? ArrayBasedPivotData.NoData + : data + ; + } + } + + try + { + data = await AggregationPivotAsync(collectionName, def, extraFilter, maxFetchSize, token); + + if ( data is { Count: > 0, Headers.Count: > 0 } ) + await CacheResultsAsync(collectionName, data, def, extraFilter, GetQueryText, token); + + return data.Count == 0 + ? ArrayBasedPivotData.NoData + : data + ; + } + finally + { + sw.Stop(); +#pragma warning disable CS8604 // Possible null reference argument. + _log.Info( $"Executed. User=\"{userName}\" Pivot=\"{def.Name}\" PivotType={def.PivotType} Collection=\"{collectionName}\" ExtraFilter=\"{extraFilter}\" executed in Elapsed=\"{sw.Elapsed}\"" ); +#pragma warning restore CS8604 // Possible null reference argument. + } + } + + public async Task GetCachedResultAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, Func getQueryText, CancellationToken token = default ) + { + try + { + var id = GetCacheKey(collectionName, def, extraFilter, getQueryText ); + + var coll = GetCollectionWithRetries( CacheCollectionName ); + var c = coll.Find( new BsonDocument {{"_id", id}} ); + var doc = await c.FirstOrDefaultAsync(cancellationToken: token); + doc?.ReorderColumns( def.ColumnsOrder ); + return doc; + } + catch ( Exception ) + { + // ignore + return null; + } + } + + private static string GetCacheKey(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, Func getQueryText) + { + var text = getQueryText(collectionName, def, extraFilter ); + using var hash = MD5.Create(); + hash.Initialize(); + var bytes = Encoding.UTF8.GetBytes( text ); + hash.TransformFinalBlock( bytes, 0, bytes.Length ); + var name = $"{collectionName}-{def.Name}-{BitConverter.ToString( hash.Hash! ).Replace( "-", "" )}"; + + return name; + } + + public async Task CacheResultsAsync(string collectionName, ArrayBasedPivotData data, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, Func getQueryText, CancellationToken token = default ) + { + var id = GetCacheKey(collectionName, def, extraFilter, getQueryText); + data.Id = id; + + if ( data.Count == 0 || data.Count > MaxCachedRows ) + return; + + try + { + data.ExpireAt = DateTime.UtcNow + TimeSpan.FromHours( PivotCacheTTLHours ); + + var coll = GetCollectionWithRetries(CacheCollectionName); + var filter = $"{{_id : \"{id}\" }}"; + try + { + await coll.FindOneAndDeleteAsync( filter, cancellationToken: token); + } + catch ( Exception ) + { + // ignore + } + + await coll.InsertOneAsync( data, new() {BypassDocumentValidation = true}, token); + } + catch (Exception) + { + // ignore + } + } + + public Task GetQueryTextAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default ) => Task.FromResult( GetQueryText(collectionName, def, extraFilter ) ); + + private string GetQueryText(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter) => + GetPivotPipelineText(collectionName, def, extraFilter ); + //""" + // db.getCollection("[COLLECTION]").aggregate( + // [TEXT] + // ) + // """ + // .Replace( "[COLLECTION]", collectionName) + // .Replace( "[TEXT]", GetPivotPipelineText(collectionName, def, extraFilter ) ); + + private string GetPivotPipelineText(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter ) + { + try + { + var fields = GetFieldMapping(collectionName); + + def = def.Clone(); + foreach ( var f in def.DataFields + .Where( x => fields.IsCalculated( x ) && !string.IsNullOrWhiteSpace( fields.GetLookup( x ) ) ) + .Select( x => Tuple.Create( x, fields.GetLookup( x ) ) ) + ) + { + if ( !string.IsNullOrWhiteSpace( def.BeforeGrouping ) ) + def.BeforeGrouping += ",\n"; + def.BeforeGrouping += f.Item2; // lookup def + } + + var json = def.ToJson(extraFilter, GetAllFields(collectionName), x => fields.GetAggregationOperator( x ) ); + json = RemoveComments(json); + + var pipeline = MongoDB.Bson.Serialization.BsonSerializer.Deserialize( json ).Select( p => p.AsBsonDocument ).ToList(); + + fields.MapAllFields( pipeline ); + + json = pipeline.ToJson( new() {Indent = true, OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson} ); + + return json; + } + catch + { + try + { + var json = def.ToJson( extraFilter, GetAllFields(collectionName),x => _fieldMap[collectionName].GetAggregationOperator( x ) ); + json = RemoveComments(json); + return json; + } + catch + { + return ""; + } + } + } + + private Dictionary GetAllFields(string collectionName) + { + if ( !_fieldMap.TryGetValue( collectionName, out var mapping ) || mapping.Count == 0 ) + return []; + var res = new Dictionary(); + foreach ( var field in mapping.Fields ) + { + res[field.Key] = field.Value.Type; + } + return res; + } + + + + private static string RemoveComments(string input) + { + // quick check + if (input.IndexOf("//", StringComparison.Ordinal) < 0 && input.IndexOf("/*", StringComparison.Ordinal) < 0) + return input; + + //https://stackoverflow.com/questions/3524317/regex-to-strip-line-comments-from-c-sharp/3524689#3524689 + + const string blockComments = @"/\*(.*?)\*/"; + const string lineComments = @"//(.*?)\r?\n"; + const string strings = """ + "((\\[^\n]|[^"\n])*)" + """; + const string verbatimStrings = """@("[^"]*")+"""; + + try + { + var noComments = Regex.Replace(input, + blockComments + "|" + lineComments + "|" + strings + "|" + verbatimStrings, + me => + { + if (me.Value.StartsWith("/*") || me.Value.StartsWith("//")) + return me.Value.StartsWith("//") ? Environment.NewLine : ""; + // Keep the literal strings + return me.Value; + }, + RegexOptions.Singleline); + return noComments; + } + catch (Exception ex) + { + _log.Error("Can't remove comments from this pipeline:\n"+input, ex); + return input; + } + } + + private async Task AggregationPivotAsync( + string collectionName, + PivotDefinition def, + FilterExpressionTree.ExpressionGroup? extraFilter, + int maxFetchSize = -1, + CancellationToken token = default ) + { + var coll = GetCollectionWithRetries( collectionName ); + + def.AllowDiskUsage = true; + var options = new AggregateOptions + { + BypassDocumentValidation = true, + AllowDiskUse = true + }; + + // preload field mapping. we'll need it in GetPivotPipelineText + _ = await GetFieldMapping(collectionName, token); + + var json = GetPivotPipelineText(collectionName, def, extraFilter); + Debug.WriteLine(json); + var pipeline = MongoDB.Bson.Serialization.BsonSerializer.Deserialize(json) + .Select(p => p.AsBsonDocument) + .ToList(); + + using var cursor = await coll.AggregateAsync( pipeline, options, token); + var pd = await FetchPivotData( + collectionName, + def, + cursor, + extraFilter, + maxFetchSize, + false, + token); + + return pd; + } + + public static async IAsyncEnumerable ToAsyncEnumerable(IAsyncCursor asyncCursor) + { + while (await asyncCursor.MoveNextAsync()) + { + foreach (var current in asyncCursor.Current) + { + yield return current; + } + } + } + + private async Task FetchPivotData(string collectionName, + PivotDefinition def, + IAsyncCursor cursor, + FilterExpressionTree.ExpressionGroup? extraFilter, + int maxFetchSize = -1, + bool includeId = false, + CancellationToken token = default + ) + { + var (pd, _) = await FetchPivotData( + collectionName, + def.Name, + _fieldMap, + ToAsyncEnumerable(cursor), + extraFilter, + false, + maxFetchSize, + includeId, + token); + pd.ReorderColumns( def.ColumnsOrder ); + return pd; + } + + public static async Task<(ArrayBasedPivotData, List)> FetchPivotData( + string collectionName, + string pivotName, + ConcurrentDictionary fieldMap, + IAsyncEnumerable batch, + FilterExpressionTree.ExpressionGroup? extraFilter, + bool needBson = false, + int maxFetchSize = -1, + bool includeId = false, + CancellationToken token = default) + { + var sw = new Stopwatch(); + sw.Start(); + + var pd = new ArrayBasedPivotData( Array.Empty() ); + + var headers = new List>(); // display text / column in the data set + var headersSet = new HashSet(); // for speed + + if (includeId) + { + headers.Add(Tuple.Create("_id", "_id")); + headersSet.Add("_id"); + pd.AddHeader("_id"); + + } + + List documents = []; + + var docs = 0; + await using var enumerator = batch.GetAsyncEnumerator(token); + + while ( await enumerator.MoveNextAsync() ) + { + var doc = enumerator.Current; + if ( doc.IsBsonNull ) + continue; + + //{ _id: wateva, values: {Book:a, tenor:1w, value:2}} + //{ _id: wateva, values: {rows : { anyId1: {Book:a, tenor:1w, value:2}, anyid2: {Book:a, tenor:1y, value:1}} }} } + + if (needBson) + documents.Add(doc); + + var res = doc.ToDictionary(); + var id = res["_id"] as Dictionary; + + // for map/reduce results + var value = ( res.TryGetValue("value", out var re) + ? (Dictionary)re : null ) ?? []; + + if ( id == null && value.Count == 0 && res.Count == 2 && res.ContainsKey("_id") && res.ContainsKey("value") ) + continue; + + //The columns are sparse, so we have to process every row, looking for new/additional columns to add + UpdateHeaders(collectionName, res, headersSet, headers, pd, fieldMap); + + if (value.TryGetValue("rows", out var value1)) + { + var rows = (Dictionary)value1; + + //multirow response, this is for optimising large Rho/Vega aggregations + //{ _id: wateva, values: {rows : { anyId1: {Book:a, tenor:1w, value:2}, anyid2: {Book:a, tenor:1y, value:1}} }} } + foreach (var row in rows) + { + var rowId = new Dictionary + { + { "_id", row.Key } + }; + var rowValues = (Dictionary)row.Value; + + pd.Add(headers + .Select(x => x.Item2) + .Select(x => rowId.TryGetValue(x, out var val) || rowValues.TryGetValue(x, out val) + ? val + : null)); + } + } + else + { + var objects = headers + .Select(x => x.Item2) + .Select(x => res.TryGetValue(x, out var val) || id != null && id.TryGetValue(x, out val) || + value.TryGetValue(x, out val) + ? val + : null); + + pd.Add(objects); + } + + docs++; + if (docs >= PivotMaxReturnedRows || ( maxFetchSize > 0 && docs >= maxFetchSize) ) + { + _log.Warn($"Data truncated at PivotMaxReturnedRows. Pivot=\"{pivotName}\", Collection=\"{collectionName}\", ExtraFilter=\"{extraFilter}\" Rows={docs}"); + break; + } + } + + if ( pd.Headers.Count == 0 ) + { + _log.Warn( $"No results fetched for Pivot=\"{pivotName}\"" ); + pd = new(["Message"]); + pd.Add(["No results"]); + } + + sw.Stop(); + _log.Debug($"Data fetched. Pivot=\"{pivotName}\", Collection=\"{collectionName}\", ExtraFilter=\"{extraFilter}\" Rows={pd.Count}, Cols={pd.Headers.Count}, ElapsedMs={sw.Elapsed.TotalMilliseconds}"); + return (pd, documents); + } + + private static void UpdateHeaders( + string collectionName, + Dictionary document, + ISet headersSet, + ICollection> headers, + ArrayBasedPivotData pd, + ConcurrentDictionary fieldMap ) + { + foreach (var kvp in document) + { + switch (kvp.Key) + { + case "_id": + if (kvp.Value is IDictionary kvpIDict) + { + foreach (var key in kvpIDict.Keys) + { + var unmapped = fieldMap[collectionName].UnmapField(key); + if (headersSet.Contains(unmapped)) + continue; + pd.AddHeader(unmapped); // new header! + headers.Add(Tuple.Create(unmapped, key)); + headersSet.Add(unmapped); + } + } + + break; + case "value": + if (kvp.Value is Dictionary kvpDict) + { + foreach (var (key, value) in kvpDict) + { + if (key.ToUpper() == "ROWS") + { + //this structure allows us to return multiple rows from a single map/reduce + //{ _id: wateva, values : {rows : { anyId1: {Book:a, tenor:1w, value:2}, anyid2: {Book:a, tenor:1y, value:1}} }} } + foreach (var inner in value as Dictionary ?? []) + foreach (var innerInner in inner.Value as Dictionary ?? []) + { + var unmapped = fieldMap[collectionName].UnmapField(innerInner.Key); + if (headersSet.Contains(unmapped)) + continue; + pd.AddHeader(unmapped); // new header! + headers.Add(Tuple.Create(unmapped, innerInner.Key)); + headersSet.Add(unmapped); + } + } + else + { + var unmapped = fieldMap[collectionName].UnmapField(key); + if (headersSet.Contains(unmapped)) + continue; + pd.AddHeader(unmapped); // new header! + headers.Add(Tuple.Create(unmapped, key)); + headersSet.Add(unmapped); + } + } + } + + break; + default: + { + var unmapped = fieldMap[collectionName].UnmapField(kvp.Key); + if (headersSet.Contains(unmapped)) + continue; + pd.AddHeader(unmapped); // new header! + headers.Add(Tuple.Create(unmapped, kvp.Key)); + headersSet.Add(unmapped); + } + break; + } + } + } + + public async Task> GetPivotsAsync(string collectionName, + IPivotTableDataSource.PivotType pivotType, + string? userName = null, CancellationToken token = default) + { + userName = string.IsNullOrWhiteSpace(userName) ? User : userName; + + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + + //get all the predefined pivots + var standardPivots = (await coll.Find(x => x.Id.Equals("PredefinedPivots")).FirstOrDefaultAsync(cancellationToken: token))?.Pivots ?? []; + //get all user pivots + var allUserPivots = await coll.Find(x => x.Id.StartsWith("PredefinedPivots-")).ToListAsync(cancellationToken: token) ?? []; + //find the specific users private pivots from the set of user pivots + var userPivots = (allUserPivots.FirstOrDefault(doc => doc.Id.Equals( $"PredefinedPivots-{userName}", StringComparison.OrdinalIgnoreCase)))?.Pivots ?? []; + + // fix accidental saves where a user pivot is saved into the global section + foreach (var def in standardPivots.Where(def => def.Group == PivotDefinition.UserPivotsGroup)) + { + def.Group = PivotDefinition.PredefinedPivotsGroup; + } + + // Change group for user pivots to match the user's name + foreach (var pivots in allUserPivots) + { + var name = pivots.Id.StartsWith("PredefinedPivots-") + ? pivots.Id["PredefinedPivots-".Length..] + : string.Empty; + + if (string.IsNullOrEmpty(name)) + continue; + + foreach (var def in pivots.Pivots) + def.Group = $"User pivots - {name}"; + } + + var res = pivotType switch + { + IPivotTableDataSource.PivotType.Predefined => standardPivots, + IPivotTableDataSource.PivotType.User => userPivots, + IPivotTableDataSource.PivotType.UserAndPredefined => standardPivots.Concat(userPivots), + IPivotTableDataSource.PivotType.All => standardPivots.Concat(allUserPivots.SelectMany(x => x.Pivots)), + _ => throw new ArgumentOutOfRangeException(nameof(pivotType), pivotType, null) + }; + + var result = res + .OrderBy(x => (x.Group, x.Name)) + .ToList() + ; + + return result; + } + + + public async Task UpdatePredefinedPivotsAsync(string collectionName, IEnumerable pivots, bool predefined = false, string? userName = null, CancellationToken token = default ) + { + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + + PivotMetaCache.Reset(this, collectionName); + + userName = string.IsNullOrWhiteSpace(userName) ? Environment.UserName : userName; + + var id = predefined + ? "PredefinedPivots" + : $"PredefinedPivots-{userName}" + ; + + var doc = await coll.Find( new BsonDocument {{"_id", id}} ) + .FirstOrDefaultAsync(cancellationToken: token) + ?? new PivotDefinitions + { + Id = id, + Pivots = [] + } + ; + + foreach (var p in pivots) + { + var existing = doc.Pivots.FirstOrDefault(x => x.Name == p.Name && x.Group == p.Group); + if (existing == null) + { + p.Owner = userName; + doc.Pivots.Add(p); + } + else + { + // retain order for easier git diffs + var idx = doc.Pivots.IndexOf(existing); + p.Owner = userName; + doc.Pivots[idx] = p; + } + } + + var options = new ReplaceOptions + { + IsUpsert = true + }; + + await coll.ReplaceOneAsync( new BsonDocument {{"_id", id}}, doc, options, token); + } + + // ReSharper disable MemberCanBePrivate.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable CollectionNeverUpdated.Local + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + // ReSharper disable UnusedMember.Local + + [BsonIgnoreExtraElements] + // ReSharper disable once ClassNeverInstantiated.Local + private class InternalPivotColumnDescriptor + { + public override string ToString() => $"{NameRegex} {Background} {Format}"; + + public string NameRegex { get; set; } = ""; + public string? Background { get; set; } + public string? AlternateBackground { get; set; } + public string? Format { get; set; } + + public PivotColumnDescriptor ToPivotColumnDescriptor() => + new() + { + NameRegexString = NameRegex, + // ReSharper disable PossibleNullReferenceException + Background = ColorConverter.ConvertFromString( Background ?? "White" ), AlternateBackground = ColorConverter.ConvertFromString( AlternateBackground ?? "White" ), + // ReSharper restore PossibleNullReferenceException + Format = Format ?? "" + }; + } + + [BsonIgnoreExtraElements] + // ReSharper disable once ClassNeverInstantiated.Local + private class InternalPivotColumnDescriptors + { + [BsonId] public string Id { get; set; } = ""; + + public List Fields { get; set; } = []; + } + + // ReSharper restore UnusedMember.Local + // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local + // ReSharper restore CollectionNeverUpdated.Local + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore MemberCanBePrivate.Local + + public async Task GetColumnDescriptorsAsync(string collectionName, CancellationToken token = default) + { + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + var descriptors = await coll.Find( new BsonDocument {{"_id", "FieldDescriptors"}} ).FirstOrDefaultAsync(cancellationToken: token); + + if ( descriptors == null ) + { + coll = GetCollectionWithRetries("Global-Meta"); + descriptors = await coll.Find( new BsonDocument {{"_id", "FieldDescriptors"}} ).FirstOrDefaultAsync(cancellationToken: token); + } + + return descriptors?.Fields.Select( x => x.ToPivotColumnDescriptor() ).ToArray() ?? []; + } + + public Dictionary GetFieldTypes(string collectionName) + { + var fields = _fieldMap[collectionName]; + var data = fields.Fields + .Select( x => new KeyValuePair( x.Key, x.Value.Type ) ) + .Concat( fields.CalculatedFields.Where( x => !fields.ContainsKey( x ) ).Select( x => new KeyValuePair( x, typeof(double) ) ) ); + + var res = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in data) + { + if (res.ContainsKey(item.Key)) + continue; + res[item.Key] = new() + { + Name = item.Key, + Purpose = GetPurpose(collectionName, item.Key), + Type = GetFieldType(collectionName, item.Key) + }; + } + + return res; + } + + private Type GetFieldType(string collectionName, string key ) => + !_fieldMap[collectionName].TryGetValue( key, out var m ) + ? typeof(double) + : m?.Type ?? typeof(object); + + private PivotFieldPurpose GetPurpose(string collectionName, string key ) => + !_fieldMap[collectionName].TryGetValue( key, out var m ) + ? PivotFieldPurpose.Data + : m?.Purpose ?? PivotFieldPurpose.Data; + + [BsonIgnoreExtraElements] + private class WindowStateDesc + { + [BsonId] public string Id { get; set; } = ""; + + public int Width { get; set; } + public int Height { get; set; } + public int Left { get; set; } + public int Top { get; set; } + public string? WindowState { get; set; } + public bool AutoRunEnabled { get; set; } + public bool ExpandTable { get; set; } + public bool ExpandDataFields { get; set; } + public string []? SelectedDepartments { get; set; } + public bool ShowCobRange { get; set; } + public string? DefaultCollection { get; set; } + + } + + /// + /// + /// Get a single document + /// + /// + /// Primary key fields in no particular order + /// Extra $match stage + /// + /// Json string + public Task GetDocumentAsync(string collectionName, KeyValuePair [] keys, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default ) + { + var filter = new FilterExpressionTree.ExpressionGroup(); + + filter.Children.AddRange( + keys.Select( x => new FilterExpressionTree.FieldExpression() + { + Field = x.Key, + Argument = x.Value.ToString() ?? "", + Condition = FilterExpressionTree.FieldConditionType.EqualTo + } ) + ); + + if ( extraFilter != null ) + filter.Children.AddRange( extraFilter.Children ); + + return GetDocumentAsync(collectionName, filter, token); + } + + /// + public async Task GetDocumentAsync(string collectionName, FilterExpressionTree.ExpressionGroup extraFilter, CancellationToken token = default ) + { + var filter = BsonDocument.Parse( extraFilter.ToJson(GetAllFields(collectionName)) ); + + var coll = GetCollectionWithRetries(collectionName); + var res = await (await coll.FindAsync( filter, cancellationToken: token)).FirstOrDefaultAsync(cancellationToken: token); + + return res?.ToJson(new() { Indent = true }) ?? $"Not found: {extraFilter}"; + } + + public async Task DeletePivotAsync(string collectionName, string pivotName, string groupName, string userName, CancellationToken token = default) + { + _log.Debug($"Deleting Pivot=\"{pivotName}\" from Group=\"{groupName}\" for User=\"{userName}\"..."); + + if (groupName != PivotDefinition.UserPivotsGroup) + throw new ApplicationException($"You can only delete from \"{PivotDefinition.UserPivotsGroup}\" group"); + + var coll = GetCollectionWithRetries($"{collectionName}-Meta"); + + userName = string.IsNullOrWhiteSpace(userName) ? Environment.UserName : userName; + + var id = $"PredefinedPivots-{userName}"; + + var doc = await coll.Find( new BsonDocument {{"_id", id}} ) + .FirstOrDefaultAsync(cancellationToken: token) + ?? new PivotDefinitions + { + Id = id, + Pivots = [] + } + ; + + var existing = doc.Pivots.FirstOrDefault(x => !x.IsPredefined && x.Name == pivotName) + ?? throw new ApplicationException($"Pivot=\"{pivotName}\" from Group=\"{groupName}\" was NOT deleted for User=\"{userName}\" as it was not found") + ; + + // retain order for easier git diffs + var idx = doc.Pivots.IndexOf(existing); + doc.Pivots.RemoveAt(idx); + + var options = new ReplaceOptions + { + IsUpsert = true + }; + + await coll.ReplaceOneAsync( new BsonDocument {{"_id", id}}, doc, options, token); + + _log.Info($"Pivot=\"{pivotName}\" from Group=\"{groupName}\" deleted for User=\"{userName}\""); + } + + internal IMongoCollection GetCollectionWithRetries(string name) where T: class => + RetryHelper.DoWithRetries(() => + { + if (_database == null || !MongoDbHelper.IsConnected(_database)) + _database = null; + return Database.GetCollection(name); + }, + 3, + TimeSpan.FromSeconds(5), logFunc: LogMethod); + + /// + /// Log method for retry function, called for each iteration + /// + /// + /// + /// + private static void LogMethod(int iteration, int maxRetries, Exception e) + { + if (iteration < maxRetries - 1) + _log.Warn($"MongoDB error, retrying RetriesLeft={maxRetries-iteration-1}\"", e); + else + _log.Error("MongoDB error, retries exhausted", e); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/MongoShardInfo.cs b/Rms.Risk.Mango.Pivot.Core/Models/MongoShardInfo.cs new file mode 100644 index 0000000..8641ffb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/MongoShardInfo.cs @@ -0,0 +1,31 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text.Json.Serialization; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class MongoShardInfo +{ + [JsonPropertyName("_id")] public string Id { get; set; } = ""; + + [JsonPropertyName("host")] public string Host { get; set; } = ""; + + [JsonPropertyName( "state")] + public int State { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/Navigation.cs b/Rms.Risk.Mango.Pivot.Core/Models/Navigation.cs new file mode 100644 index 0000000..7f7f6e9 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/Navigation.cs @@ -0,0 +1,92 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class Navigation(Action apply) where T : class +{ + + private class NavigationRecord + { + public PivotDefinition? Def { get; init; } + public T? Data { get; init; } + + public override string ToString() => $"{Def?.Name} Hash={Def?.GetHashCode():X}"; + } + + private readonly Action _apply = apply; + private readonly Stack _backStack = new(); + private readonly Stack _forwardStack = new(); + private NavigationRecord? _current; + + public bool CanGoBackward => _backStack.Count > 0; + public bool CanGoForward => _forwardStack.Count > 0; + + public void Add( PivotDefinition defToAdd, T data ) + { + var rec = new NavigationRecord + { + Def = defToAdd, + Data = data + }; + if ( _current != null ) + _backStack.Push( _current ); + _current = rec; + _forwardStack.Clear(); + } + + public void Clear() + { + _current = null; + _backStack.Clear(); + _forwardStack.Clear(); + } + + public bool Back() + { + if (_backStack.Count == 0) + return false; + + var rec = _backStack.Pop(); + if ( _current != null ) + _forwardStack.Push( _current ); + Apply( rec ); + + return true; + } + + public bool Forward() + { + if (_forwardStack.Count == 0) + return false; + + var rec = _forwardStack.Pop(); + if ( _current != null ) + _backStack.Push( rec ); + + Apply( rec); + + return true; + } + + private void Apply( NavigationRecord rec ) + { + _current = rec; + _apply(rec.Def!, rec.Data!); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/NotifyPropertyChangedBase.cs b/Rms.Risk.Mango.Pivot.Core/Models/NotifyPropertyChangedBase.cs new file mode 100644 index 0000000..bb39506 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/NotifyPropertyChangedBase.cs @@ -0,0 +1,179 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.ComponentModel; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public interface IMutable +{ + bool IsBusy { get; } + void Mute(); + void Unmute(); +} + +public class NotifyPropertyChangedBase : INotifyPropertyChanged, IMutable +{ + private static Action _invokeWrapper = action => action?.Invoke(); + + /// + /// For WPF applications call: + /// NotifyPropertyChangedBase.SetInvokeWrapper( Application.Current.Dispatcher.Invoke ); + /// + /// + public static void SetInvokeWrapper(Action invokeWrapper) + => _invokeWrapper = invokeWrapper; + +#region INotifyPropertyChanged Members + + public event PropertyChangedEventHandler? PropertyChanged; + +#endregion + + /// + /// Returns the name of a property dynamically using an expression tree without requiring an instance + /// usage: GetPropertyName@gt;MyClass@lt;( (MyClass c) => c.MyProperty) returns "MyProperty" + /// + public static string GetPropertyName(Expression> property) + { + var lambda = (LambdaExpression)property; + MemberExpression memberExpression; + + if (lambda.Body is UnaryExpression body) + { + var unaryExpression = body; + memberExpression = (MemberExpression)unaryExpression.Operand; + } + else + { + memberExpression = (MemberExpression)lambda.Body; + } + + return memberExpression.Member.Name; + } + + /// + /// Returns the name of a property dynamically using an expression tree + /// usage: GetPropertyName( () => someinstance.MyProperty) returns "MyProperty" + /// + public static string GetPropertyName(Expression> property) + { + var lambda = (LambdaExpression)property; + MemberExpression memberExpression; + + if (lambda.Body is UnaryExpression body) + { + var unaryExpression = body; + memberExpression = (MemberExpression)unaryExpression.Operand; + } + else + { + memberExpression = (MemberExpression)lambda.Body; + } + + return memberExpression.Member.Name; + } + + /// + /// Overriden by subclasses, which may way want to to enable/diable the notification event. + /// Called before every event notification + /// + /// + protected virtual bool AllowPropertyChanged() => true; + + /// + /// Fires the property changed event + /// Dynamically generates the property name using an expression tree + /// usage: OnPropertyChanged( (MyClass m) => m.MyProperty) + /// + protected void OnPropertyChanged(Expression> property) + { + var e = new PropertyChangedEventArgs(GetPropertyName(property)); + OnPropertyChanged(e); + } + + + /// + /// Fires the property changed event + /// Dynamically generates the property name using an expression tree + /// usage: OnPropertyChanged( () => MyProperty) + /// + protected void OnPropertyChanged(Expression> property) + { + var e = new PropertyChangedEventArgs(GetPropertyName(property)); + OnPropertyChanged(e); + } + + /// + /// Fire the notify property changed event with a static property name + /// No not supply propName as it'll be automatically filled in + /// + protected void OnPropertyChanged([CallerMemberName] string propName = "") + { + var e = new PropertyChangedEventArgs(propName); + OnPropertyChanged(e); + } + + private void OnPropertyChanged(PropertyChangedEventArgs e) + { +// if (IsBusy) +// return; + + var pc = PropertyChanged; + if (pc != null && AllowPropertyChanged()) + { + _invokeWrapper( () => pc(this, e) ); + } + } + + public bool IsBusy + { + get => _muteCount > 0; + set + { + if (value) + Mute(); + else + Unmute(); + } + } + + private int _muteCount; + + public virtual void Mute() + { + _muteCount += 1; + PropertyChanged?.Invoke(this, new(nameof(IsBusy))); + } + + public virtual void Unmute() + { + _muteCount -= 1; + PropertyChanged?.Invoke(this, new(nameof(IsBusy))); + if (_muteCount < 0) + { +#if DEBUG + throw new ApplicationException("Mute count becomes negative"); +#else + _muteCount = 0; +#endif + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/PivotMetaCache.cs b/Rms.Risk.Mango.Pivot.Core/Models/PivotMetaCache.cs new file mode 100644 index 0000000..58ab658 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/PivotMetaCache.cs @@ -0,0 +1,360 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics; +using System.Reflection; +using log4net; +using static Rms.Risk.Mango.Pivot.Core.IPivotTableDataSource; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public static class PivotMetaCache +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + private static readonly SemaphoreSlim _lock = new(1, 1); + + public const string Any = ""; + + public static async Task PreloadCollections(this IPivotTableDataSourceMetaProvider pivotService, List collections, string? userEmail = null, CancellationToken token = default) + { + if ( pivotService == null ) + throw new ArgumentNullException(nameof(pivotService)); + if ( collections == null ) + throw new ArgumentNullException(nameof(collections)); + + if ( string.IsNullOrWhiteSpace(pivotService.Prefix) ) + throw new ArgumentException("Pivot service must have a valid Prefix", nameof(pivotService)); + + if ( collections.Count == 0 ) + { + var coll = await LoadCollections(pivotService, token); + collections.AddRange(coll); + } + + var changed = 0; + + var collectionsToLoad = collections + .Where(x => + x is { IsGroup: false } + && x.DataSourcePrefix == pivotService.Prefix + && x.Pivots.Count == 0 + ) + .ToList(); + + if ( collectionsToLoad.Count == 0 ) + return false; + + var sw = Stopwatch.StartNew(); + + _log.Debug($"{pivotService.GetType().Name}: Waiting for lock within PreloadCollections..."); + + await _lock.WaitAsync(token); + + if ( sw.Elapsed > TimeSpan.FromSeconds(30) ) + _log.Warn($"Waited {sw.Elapsed} for lock within PreloadCollections"); + + try + { + var tasks = collectionsToLoad.Select(x => LoadPivots(pivotService, x, PivotType.UserAndPredefined, userEmail, token)) + .ToArray(); + + await Task.WhenAll(tasks); + changed += collectionsToLoad.Sum(x => x.Pivots.Count); + + if ( changed > 0 ) + _log.Debug($"Finished preloading {collections.Count} collections, {changed} pivots for {pivotService.SourceId}"); + + return changed > 0; + } + finally + { + _lock.Release(); + _log.Debug($"{pivotService.GetType().Name}: Released lock within PreloadCollections. Elapsed=\"{sw.Elapsed}\""); + } + } + + public static void Clear() => _collectionsCache?.Clear(); + + public static void Reset(IPivotTableDataSourceMetaProvider pivotService, string collection) + { + var id = MakeId(pivotService, collection); + _collectionsCache?.Remove(id); + } + + + + + private static async Task> LoadCollections(IPivotTableDataSourceMetaProvider pivotService, CancellationToken token = default) + { + var collectionNames = (await pivotService.GetCollectionsAsync(CollectionType.HaveMeta, token)) + .Distinct() + .OrderBy(x => x) + .ToArray() + ; + + if (collectionNames.Length == 0) + throw new ApplicationException("No collections found"); + + var d = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var coll in collectionNames) + { + var s = coll.Split(':'); + + string group; + string name; + + if (s.Length != 2) + { + group = pivotService.Prefix; + name = coll; + } + else + { + group = s[0].Trim(); + name = s[1].Trim(); + } + + name = name.Replace("-Meta", "", StringComparison.InvariantCultureIgnoreCase); + + if (d.TryGetValue(group, out var thisGroup)) + thisGroup.Add(name); + else + d[group] = [name]; + } + + var keys = d.Keys.ToArray(); + Array.Sort(keys); + var collections = new List(); + + foreach (var k in keys) + { + collections.Add(new() + { + DataSourcePrefix = k, + CollectionNameWithoutPrefix = k, + IsGroup = true + }); + collections.AddRange(d[k] + .OrderBy(x => x) + .Select(x => new GroupedCollection + { + DataSourcePrefix = k, + CollectionNameWithoutPrefix = x, + IsGroup = false + })); + } + + return collections; + } + + private record LoaderArg( + IPivotTableDataSourceMetaProvider PivotService, + PivotType PivotType, + string CollectionNameWithoutPrefix, + string? UserName + ); + + private static ExpiringObjectPool? _collectionsCache; + + private static async Task LoadPivots( + IPivotTableDataSourceMetaProvider pivotService, + GroupedCollection collection, + PivotType pivotType, + string? userName = null, + CancellationToken token = default + ) + { + if ( collection.IsGroup ) + return; + + _collectionsCache ??= new(LoadPivotsInternal); + + var id = MakeId(pivotService, collection.CollectionNameWithoutPrefix); + + var coll = await _collectionsCache.Get(id, new (pivotService, PivotType.All, collection.CollectionNameWithoutPrefix, userName), token); + collection.CopyFrom(coll); + + var allPivots = coll.Pivots.Where(x => !x.IsGroup).Select(x => x.Pivot).ToList(); + var predefinedPivots = allPivots.Where(x => !x.Group.StartsWith("User ")); + var thisUserPivots = string.IsNullOrWhiteSpace(userName) ? [] : allPivots.Where(x => x.Group.StartsWith("User ") && x.Group.EndsWith(userName)); + + var res = pivotType switch + { + PivotType.Predefined => predefinedPivots, + PivotType.User => thisUserPivots, + PivotType.UserAndPredefined => predefinedPivots.Concat(thisUserPivots), + PivotType.All => allPivots, + _ => throw new ArgumentOutOfRangeException(nameof(pivotType), pivotType, null) + }; + + var pivots = res + .OrderBy(x => (x.Group, x.Name)) + .ToList() + ; + + collection.Pivots = MakeGroupedPivots(pivots); + } + + private static string MakeId(IPivotTableDataSourceMetaProvider pivotService, string collection) => $"Collection=\"{collection}\" {pivotService.SourceId}"; + + private static async Task LoadPivotsInternal(string key, LoaderArg args, CancellationToken token) + { + var pivotService = args.PivotService; + var pivotType = args.PivotType; + var userName = args.UserName; + + List ? pivotDefinitions = null; + HashSet ? allKeyFields = null; + HashSet ? allDataFields = null; + string[] ? departments = null; + DateTime[] ? cobs = null; + PivotColumnDescriptor[]? descriptors = null; + + var collection = new GroupedCollection + { + CollectionNameWithoutPrefix = args.CollectionNameWithoutPrefix, + DataSourcePrefix = args.PivotService.Prefix, + IsGroup = false + }; + + await Task.WhenAll( + LoadPivotDefinitions(), + LoadCobs(), + LoadDepartments(), + LoadKeyFields(), + LoadDataFields(), + LoadColumnDescriptors() + ); + + var fieldTypes = pivotService.GetFieldTypes(collection.CollectionNameWithoutPrefix); + var pivots = MakeGroupedPivots(pivotDefinitions); + + collection.Pivots = pivots; + + if ( allKeyFields != null ) + collection.KeyFields = allKeyFields; + if ( allDataFields != null ) + collection.DataFields = allDataFields; + if ( departments != null ) + collection.Departments = departments; + if ( cobs != null ) + collection.Cobs = cobs; + if ( descriptors != null ) + collection.ColumnDescriptors = descriptors; + if ( fieldTypes.Count > 0 ) + collection.FieldTypes = fieldTypes; + + return collection; + + async Task LoadPivotDefinitions() + { + if ( collection.Pivots.Count > 0 ) + return; + pivotDefinitions = await pivotService.GetPivotsAsync(collection.CollectionNameWithoutPrefix, pivotType, userName, token); + } + + async Task LoadDepartments() + { + if ( collection.Departments.Length > 0 ) + return; + departments = (await pivotService.GetDepartmentsAsync(collection.CollectionNameWithoutPrefix, token)) + .Concat([Any]) + .ToArray() + ; + } + + async Task LoadKeyFields() + { + if ( collection.KeyFields.Count > 0 ) + return; + allKeyFields = [..await pivotService.GetKeyFieldsAsync(collection.CollectionNameWithoutPrefix, token)]; + } + + async Task LoadDataFields() + { + if ( collection.DataFields.Count > 0 ) + return; + allDataFields = [..await pivotService.GetDataFieldsAsync(collection.CollectionNameWithoutPrefix, token)]; + } + + async Task LoadCobs() + { + if ( collection.Cobs.Length > 0 ) + return; + cobs = (await pivotService.GetCobDatesAsync(collection.CollectionNameWithoutPrefix, token: token)).Select(DateTime.Parse).ToArray(); + } + + async Task LoadColumnDescriptors() + { + if ( collection.ColumnDescriptors.Length > 0 ) + return; + descriptors = await pivotService.GetColumnDescriptorsAsync(collection.CollectionNameWithoutPrefix, token); + } + } + + private static List MakeGroupedPivots(List? pivotDefinitions) + { + if ( pivotDefinitions == null || pivotDefinitions.Count == 0 ) + { + return NoPivotsFound(); + } + + var d = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var pivot in pivotDefinitions.Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name != "")) + { + var group = pivot.Group; + + if (d.TryGetValue(group, out var thisGroup)) + thisGroup.Add(pivot); + else + d[group] = [pivot]; + } + + var keys = d.Keys.ToArray(); + Array.Sort(keys); + var pivots = new List(); + + foreach (var k in keys) + { + pivots.Add(new() {Text = k, Pivot = new(), IsGroup = true}); + pivots.AddRange(d[k] + .OrderBy(x => x.Name) + .Select(x => new GroupedPivot + { + Text = x.Name, + IsGroup = false, + Pivot = x + })); + } + + return pivots.Count == 0 + ? NoPivotsFound() + : pivots + ; + } + + private static List NoPivotsFound() => + [ + new() + { + IsGroup = true, + Text = "No pivots found", + Pivot = new() + } + ]; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/PivotSettings.cs b/Rms.Risk.Mango.Pivot.Core/Models/PivotSettings.cs new file mode 100644 index 0000000..fec906e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/PivotSettings.cs @@ -0,0 +1,47 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using NjiAttribute = Newtonsoft.Json.JsonIgnoreAttribute; +using TjiAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class PivotSettings +{ + public class ClickHouseSettings + { + + public string ClickHouseUrl { get; set; } = ""; + public string ClickHouseDatabase { get; set; } = ""; + public string ClickHouseUser { get; set; } = ""; + public string ClickHousePassword { get; set; } = ""; + } + + public int ReloadIntervalMin { get; set; } + public string[] PreloadedCollections { get; set; } = []; + + + public MongoDbConfigRecord? MongoDb { get; set; } + public ClickHouseSettings? BFG { get; set; } + + [Nji,Tji] public string ClickHouseUrl => BFG?.ClickHouseUrl ?? ""; + [Nji,Tji] public string ClickHouseDatabase => BFG?.ClickHouseDatabase ?? ""; + [Nji,Tji] public string ClickHouseUser => BFG?.ClickHouseUser ?? ""; + [Nji,Tji] public string ClickHousePassword => BFG?.ClickHousePassword ?? ""; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/RolesInfoModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/RolesInfoModel.cs new file mode 100644 index 0000000..d1d0e86 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/RolesInfoModel.cs @@ -0,0 +1,257 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class RolesInfoModel +{ + public List Roles { get; set; } = []; + + public static RolesInfoModel FromBson(BsonDocument bsonDocument) + { + var model = new RolesInfoModel(); + var rolesArray = bsonDocument["roles"].AsBsonArray; + + foreach (var roleElement in rolesArray) + { + var roleDoc = roleElement.AsBsonDocument; + var role = new RoleInfoModel + { + RoleName = roleDoc.Contains("role") ? roleDoc["role"].AsString : string.Empty, + Db = roleDoc.Contains("db") ? roleDoc["db"].AsString : string.Empty, + IsBuiltin = roleDoc.Contains("isBuiltin") && roleDoc["isBuiltin"].AsBoolean, + Roles = roleDoc.Contains("roles") && roleDoc["roles"].IsBsonArray + ? roleDoc["roles"].AsBsonArray.Select(r => + { + var roleInDbDoc = r.AsBsonDocument; + return new RoleInDbModel + { + Role = roleInDbDoc.Contains("role") ? roleInDbDoc["role"].AsString : string.Empty, + Db = roleInDbDoc.Contains("db") ? roleInDbDoc["db"].AsString : string.Empty + }; + }).ToList() + : [], + InheritedRoles = roleDoc.Contains("inheritedRoles") && roleDoc["inheritedRoles"].IsBsonArray + ? roleDoc["inheritedRoles"].AsBsonArray.Select(r => r.ToString()!).ToList() + : [], + Privileges = roleDoc.Contains("privileges") && roleDoc["privileges"].IsBsonArray + ? ParsePrivileges(roleDoc["privileges"].AsBsonArray) + : [], + InheritedPrivileges = roleDoc.Contains("inheritedPrivileges") && roleDoc["inheritedPrivileges"].IsBsonArray + ? ParsePrivileges(roleDoc["inheritedPrivileges"].AsBsonArray) + : [] + }; + model.Roles.Add(role); + } + + return model; + } + + private static List ParsePrivileges(BsonArray privilegesArray) + { + var privileges = new List(); + + foreach (var privilegeElement in privilegesArray) + { + var privilegeDoc = privilegeElement.AsBsonDocument; + var resourceDoc = privilegeDoc["resource"].AsBsonDocument; + + var privilege = new PrivilegeModel + { + Resource = new() + { + AnyResource= resourceDoc.Contains("anyResource") && resourceDoc["anyResource"].AsBoolean, + Db = resourceDoc.Contains("db") ? resourceDoc["db"].AsString : string.Empty, + Collection = resourceDoc.Contains("collection") ? resourceDoc["collection"].AsString : string.Empty, + Cluster = resourceDoc.Contains("cluster") && resourceDoc["cluster"].AsBoolean, + Buckets = resourceDoc.Contains("system_buckets") ? resourceDoc["system_buckets"].AsString : null, + }, + Actions = privilegeDoc.Contains("actions") && privilegeDoc["actions"].IsBsonArray + ? privilegeDoc["actions"].AsBsonArray.Select(a => a.AsString).ToList() + : [] + }; + + privileges.Add(privilege); + } + + return privileges; + } +} + +public class RoleInfoModel +{ + public override string ToString() => $"{RoleName} ({Db})"; + + public string RoleName { get; set; } = string.Empty; + public string Db { get; set; } = string.Empty; + public bool IsBuiltin { get; set; } + public List Roles { get; set; } = []; + public List Privileges { get; set; } = []; + [JsonIgnore] + public List InheritedRoles { get; set; } = []; + [JsonIgnore] + public List InheritedPrivileges { get; set; } = []; + + public void CopyFrom(RoleInfoModel other) + { + RoleName = other.RoleName; + Db = other.Db; + IsBuiltin = other.IsBuiltin; + Roles = new(other.Roles); + InheritedRoles = new(other.InheritedRoles); + Privileges = other.Privileges.Select(p => new PrivilegeModel + { + Resource = new() + { + AnyResource = p.Resource.AnyResource, + Db = p.Resource.Db, + Collection = p.Resource.Collection, + Cluster = p.Resource.Cluster, + Buckets = p.Resource.Buckets + }, + Actions = [..p.Actions] + }).ToList(); + InheritedPrivileges = other.InheritedPrivileges.Select(p => new PrivilegeModel + { + Resource = new() + { + AnyResource = p.Resource.AnyResource, + Db = p.Resource.Db, + Collection = p.Resource.Collection, + Cluster = p.Resource.Cluster, + Buckets = p.Resource.Buckets + }, + Actions = [..p.Actions] + }).ToList(); + } + + public RoleInfoModel Clone() + { + var clone = new RoleInfoModel(); + clone.CopyFrom(this); + return clone; + } + + public BsonDocument CreateUpdateRoleCommand(bool add) + { + var command = new BsonDocument + { + { add ? "createRole" : "updateRole", RoleName }, + { "privileges", new BsonArray( + Privileges.Select(p => new BsonDocument + { + { "resource", ToBsonDocument(p.Resource) }, + { "actions", new BsonArray(p.Actions) } + }) + ) }, + { "roles", new BsonArray( + Roles.Select(x => + string.IsNullOrWhiteSpace(x.Db) + ? BsonValue.Create(x.Role) + : new BsonDocument + { + { "role", x.Role }, + { "db", x.Db } + } + ) + ) + }, + // { "authenticationRestrictions", new BsonArray( + // role.AuthenticationRestrictions.Select(ar => new BsonDocument + // { + // { "clientSource", new BsonArray(ar.ClientSource) }, + // { "serverAddress", new BsonArray(ar.ServerAddress) } + // }) + // ) }, + { "writeConcern", new BsonDocument { { "w", "majority" } } } + }; + return command; + } + + private static BsonDocument ToBsonDocument(ResourceModel resource) + { + var resourceDoc = new BsonDocument(); + + if (resource.Cluster) + { + resourceDoc.Add("cluster", true); + } + else if (resource.AnyResource) + { + resourceDoc.Add("anyResource", true); + } + else if ( string.IsNullOrWhiteSpace(resource.Buckets) ) + { + resourceDoc.Add("db", resource.Db); + resourceDoc.Add("collection", resource.Collection); + } + else + { + resourceDoc.Add("system_buckets", resource.Buckets); + } + + return resourceDoc; + } + +} + +public class RoleInDbModel +{ + public string Db { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + + public override string ToString() => $"{Role}, {Db}"; + + public override bool Equals(object? obj) + { + if (obj is not RoleInDbModel other) + return false; + + return Db == other.Db && Role == other.Role; + } + + public override int GetHashCode() + { + return HashCode.Combine(Db, Role); + } +} + +public class PrivilegeModel +{ + public override string ToString() => $"{Resource} => {string.Join(", ", Actions)}"; + + public ResourceModel Resource { get; set; } = new(); + public List Actions { get; set; } = []; +} + +public class ResourceModel +{ + public override string ToString() => $"Db={Db}, Collection={Collection}, Any={AnyResource}, Cluster={Cluster} {Buckets}"; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool AnyResource { get; set; } + public string Db { get; set; } = string.Empty; + public string Collection { get; set; } = string.Empty; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Cluster { get; set;} + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? Buckets { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/SimpleTranspose.cs b/Rms.Risk.Mango.Pivot.Core/Models/SimpleTranspose.cs new file mode 100644 index 0000000..319f1fd --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/SimpleTranspose.cs @@ -0,0 +1,90 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public static class SimpleTranspose +{ + + public static IPivotedData? Transpose(IPivotedData? src) + { + if ( src == null + || src.Count < 1 + || src.Headers.Count < 1 + || src is { Count: 1, Headers.Count: 1 } + ) + { + return src; + } + + var columnHeaders = GetColHeaders(src); + if ( columnHeaders == null || columnHeaders.Count != src.Count+1 ) + return src; + + var headers = columnHeaders + .OrderBy(x => x.Value) + .Select(x => x.Key) + .ToArray(); + + var dest = new ArrayBasedPivotData(headers); + + var srcHeaders = src.Headers; + + + // row and col corresponds to dest + for ( var row = 0; row < srcHeaders.Count-1; row++ ) + { + var r = new object?[src.Count+1]; + // first column is src.Headers + r[0] = srcHeaders.Skip(row+1).First(); // [row+1] + // the rest (1st src column becomes dest row header) + for ( var col = 1; col < headers.Length; col++ ) + r[col] = src.Get(row+1, col-1); // note reverse row/col + dest.Add(r); + } + + // double check + if ( dest.Count != src.Headers.Count-1 + || dest.Headers.Count != src.Count+1 ) + { + return src; + } + + return dest; + } + + private static Dictionary? GetColHeaders(IPivotedData src) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [src.Headers.First() ?? string.Empty] = 0 + }; + + for ( var i = 0; i < src.Count; i++ ) + { + var key = src.Get(0, i)?.ToString() ?? string.Empty; + if ( key == string.Empty ) + key = "-"; + + if ( !headers.TryAdd(key, i+1) ) + return null; + } + + return headers; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/SingleFieldMapping.cs b/Rms.Risk.Mango.Pivot.Core/Models/SingleFieldMapping.cs new file mode 100644 index 0000000..0f3de72 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/SingleFieldMapping.cs @@ -0,0 +1,43 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +[BsonIgnoreExtraElements] +public class SingleFieldMapping : ICloneable +{ + public override string ToString() => $"{Id} {Type} {Purpose}"; + + public int Id { get; set; } + public PivotFieldPurpose Purpose { get; set; } + + [BsonIgnore] + public Type Type { get; set; } = typeof(object); + + public string TypeName + { + get => Type.FullName!; + set => Type = Type.GetType( value ) ?? typeof(object); + } + + + object ICloneable. Clone() => Clone(); + public SingleFieldMapping Clone() => (SingleFieldMapping)MemberwiseClone(); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/TransposedPivotData.cs b/Rms.Risk.Mango.Pivot.Core/Models/TransposedPivotData.cs new file mode 100644 index 0000000..7ea8422 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/TransposedPivotData.cs @@ -0,0 +1,295 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class TransposedPivotData : NotifyPropertyChangedBase, IPivotedData +{ + private readonly IPivotedData _data; + private IPivotedData? _transposedData; + + public TransposedPivotData( IPivotedData data, string columnHeaderColumn, string [] rowHeaderColumns, + string dataColumn, CancellationToken token ) + { + _data = data; + _columnHeaderColumn = columnHeaderColumn; + _rowHeaderColumns = rowHeaderColumns; + _dataColumn = dataColumn; + _transposedData = CreateTransposedTable( token ); + Id = $"{data.Id}-Transposed"; + } + + public string Id { get; set; } + + private string [] _rowHeaderColumns; + public string [] RowHeaderColumn + { + get => _rowHeaderColumns; + set + { + if (_rowHeaderColumns == value) + return; + _rowHeaderColumns = value; + Refresh(); + OnPropertyChanged(() => RowHeaderColumn); + } + } + + private string _columnHeaderColumn; + public string ColumnHeaderColumn + { + get => _columnHeaderColumn; + set + { + if (_columnHeaderColumn == value) + return; + _columnHeaderColumn = value; + Refresh(); + OnPropertyChanged(() => ColumnHeaderColumn); + } + } + + private string _dataColumn; + public string DataColumn + { + get => _dataColumn; + set + { + if (_dataColumn == value) + return; + _dataColumn = value; + Refresh(); + OnPropertyChanged(() => DataColumn); + } + } + +#region IPivotedData + + public IReadOnlyCollection Headers => _transposedData?.Headers ?? _data.Headers; + public int Count => _transposedData?.Count ?? _data.Count; + + public Dictionary GetColumnPositions() + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // this is safer than calling ToDictionary as it handles duplicate headers + foreach ( var (key, pos) in Headers.Select((x, i) => (Key: x, Value: i)) ) + { + dict.TryAdd(key, pos); + } + + return dict; + } + + public object? Get( int col, int row ) + { + if ( _transposedData != null ) + { + if ( col >= _transposedData.Headers.Count + || row >= _transposedData.Count ) + return null; + return _transposedData.Get( col, row ); + } + + if (col >= _data.Headers.Count + || row >= _data.Count) + return null; + + return _data.Get( col, row ); + } + + public Type GetColumnType( int col ) => + _transposedData != null + ? _transposedData.GetColumnType( col ) + : _data.GetColumnType( col ); + + public IPivotedData Filter(Func filter) + { + var data = _data.Filter(filter); + return new TransposedPivotData(data, + _columnHeaderColumn, + _rowHeaderColumns, + _dataColumn, + CancellationToken.None); + } + +#endregion + + private CancellationTokenSource _cancellationTokenSource = new(); + + private async void Refresh() + { + try + { + await _cancellationTokenSource.CancelAsync(); + _cancellationTokenSource = new(); + + var token = _cancellationTokenSource.Token; + var data = await Task.Run( () => CreateTransposedTable(token), token ); + if ( data == null ) + return; + _transposedData = data; + OnPropertyChanged( () => Headers ); + OnPropertyChanged( () => Count ); + } + catch ( Exception ) + { + // + } + } + + private ArrayBasedPivotData? CreateTransposedTable( CancellationToken token ) + { + if ( token.IsCancellationRequested ) + return null; + + if ( _data == null + || _data.Headers.Count == 0 + || _data.Count == 0 ) + return null; + + var rhc = RowHeaderColumn; + var chc = ColumnHeaderColumn; + var dc = DataColumn; + + if ( rhc == null + || rhc.Length == 0 + || string.IsNullOrWhiteSpace( chc ) + || string.IsNullOrWhiteSpace( dc ) + ) + return null; + + if ( rhc.Any( x => !_data.Headers.Contains( x ) ) + || !_data.Headers.Contains( chc ) + || !_data.Headers.Contains( dc ) + ) + return null; + + var headersDict = _data.Headers.Select((s, i) => (s, i)).ToDictionary(x => x.s, x => x.i); + + var columns = new Dictionary(StringComparer.OrdinalIgnoreCase); + var colPos = headersDict[chc]; + var count = rhc.Length; + + for ( var row = 0; row < _data.Count; row++ ) + { + var v = ConvertToString( _data.Get( colPos, row ) ); + + if ( columns.ContainsKey( v ) ) + continue; + + columns[v] = count++; + } + + if ( token.IsCancellationRequested ) + return null; + + var rows = new Dictionary(StringComparer.OrdinalIgnoreCase); + var rowPos = rhc.Select( x => headersDict[x]).ToArray(); + count = 0; + + for ( var row = 0; row < _data.Count; row++ ) + { + var r = row; + var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) ))); + + if ( rows.ContainsKey( v ) ) + continue; + + rows[v] = count++; + } + + if ( token.IsCancellationRequested ) + return null; + + var d = new ArrayBasedPivotData( + rhc + .Concat( columns + .OrderBy( x => x.Value ) + .Select( x => x.Key ) + ) + ); + + // preallocate space + + for ( var row = 0; row < rows.Count; row++ ) + { + d.Add( new object[columns.Count+rhc.Length] ); + var k = rows.First( x => x.Value == row ).Key.Split( '|' ); + for ( var col = 0; col < k.Length; col++ ) + d[col, row] = k[col]; + } + + if ( token.IsCancellationRequested ) + return null; + + var dataPos = headersDict[dc]; + + for ( var row = 0; row < _data.Count; row++ ) + { + if ( row % 2048 == 0) + if ( token.IsCancellationRequested ) + return null; + + var destCol = columns[ConvertToString(_data.Get( colPos, row ))]; + + var r = row; + var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) ))); + var destRow = rows[v]; + + + var dest = d[destCol, destRow]; + var src = _data.Get( dataPos, row ); + + if ( src == null ) + continue; + if (dest == null) + d[destCol, destRow] = src; + else + { + switch ( dest ) + { + case double dbl: + dest = dbl + Convert.ToDouble( src ); + break; + case int i: + dest = i + Convert.ToInt32( src ); + break; + case long l: + dest = l + Convert.ToInt64( src ); + break; + case decimal dec: + dest = dec + Convert.ToInt64( src ); + break; + default: + dest = src; + break; + } + d[destCol, destRow] = dest; + } + } + + return token.IsCancellationRequested ? null : d; + } + + private static string ConvertToString( object? val ) + { + var v = val is DateTime time ? time.ToString( "yyyy-MM-dd" ) : val?.ToString() ?? ""; + return v; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Models/UserInfoModel.cs b/Rms.Risk.Mango.Pivot.Core/Models/UserInfoModel.cs new file mode 100644 index 0000000..e721a31 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Models/UserInfoModel.cs @@ -0,0 +1,103 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace Rms.Risk.Mango.Pivot.Core.Models; + +public class UserInfoModel +{ + public string UserName { get; set; } = string.Empty; + public string Db { get; set; } = string.Empty; + [JsonIgnore] + public bool IsBuiltin { get; set; } + [JsonIgnore] + public string? Password { get; set; } + public List Roles { get; set; } = []; + + public static UserInfoModel FromBson(BsonDocument bsonDocument) => + new() + { + UserName = bsonDocument.GetValue("user", string.Empty).AsString, + Db = bsonDocument.GetValue("db", string.Empty).AsString, + IsBuiltin = false, + Password = null, + Roles = bsonDocument.Contains("roles") + ? bsonDocument["roles"].AsBsonArray + .Select(role => new RoleInDbModel + { + Db = role["db"].AsString, + Role = role["role"].AsString + }).ToList() + : [] + }; + + public void CopyFrom(UserInfoModel other) + { + if (other == null) throw new ArgumentNullException(nameof(other)); + + UserName = other.UserName; + Db = other.Db; + IsBuiltin = other.IsBuiltin; + Password = other.Password; + Roles = other.Roles.Select(role => new RoleInDbModel + { + Db = role.Db, + Role = role.Role + }).ToList(); + } + + public UserInfoModel Clone() + { + var clone = new UserInfoModel(); + clone.CopyFrom(this); + return clone; + } + +} + +public class UsersModel +{ + public List Users { get; set; } = []; + + public static UsersModel FromBson(BsonDocument bsonDocument) + { + var model = new UsersModel(); + var usersArray = bsonDocument["users"].AsBsonArray; + foreach (var userElement in usersArray) + { + var userDoc = userElement.AsBsonDocument; + var user = UserInfoModel.FromBson(userDoc); + model.Users.Add(user); + } + return model; + } + + public void CopyFrom(UsersModel other) + { + Users = other.Users.Select(user => user.Clone()).ToList(); + } + + public UsersModel Clone() + { + var clone = new UsersModel(); + clone.CopyFrom(this); + return clone; + } +} diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/AdminServiceExtensions.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/AdminServiceExtensions.cs new file mode 100644 index 0000000..603313a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/AdminServiceExtensions.cs @@ -0,0 +1,336 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Rms.Risk.Mango.Pivot.Core.Models; +using Rms.Risk.Mango.Services; +using Rms.Risk.Mango.Services.Models; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public class ListDatabasesResultItem +{ + public string Name { get; set; } = string.Empty; + public long Size { get; set; } + public bool IsEmpty { get; set; } +} + +public class CollectionStats +{ + public string Name { get; init; } = ""; + public long Size { get; init; } + public long Count { get; init; } + public long StorageSize { get; init; } + public long TotalIndexSize { get; init; } + public long TotalSize { get; init; } + public bool Sharded { get; init; } + public BsonDocument? Details { get; set; } = new(); +} + +public static class AdminServiceExtensions +{ + public static async Task> ListDatabases( + this IMongoDbDatabaseAdminService service, + CancellationToken token = default) + { + var dbs = await service.RunCommand(new ("listDatabases", 1), token); + + return dbs["databases"].AsBsonArray + .Select(x => new ListDatabasesResultItem + { + Name = x["name"].ToString() ?? "???", + Size = x["sizeOnDisk"].ToInt64(), + IsEmpty = x["empty"].ToBoolean() + }) + .ToList(); + } + + public static async Task GetVersion( + this IMongoDbDatabaseAdminService service, + CancellationToken token = default) + { + var res = await service.RunCommand(new ("buildInfo", 1), token); + + return res["version"].ToString() ?? "0.0.0"; + } + + //public static async Task IsSharded( this IMongoDbDatabaseAdminService db, string database, string collectionName ) + //{ + // try + // { + // var command = BsonDocument.Parse( + // $"{{ getShardVersion: \"{database}.{collectionName}\" }}" + // ); + // var doc = await db.RunCommand( command ); + // return (int)(doc["ok"].ToDouble()) == 1; + // } + // catch ( Exception ) + // { + // //if ( ex.Message.Contains( "is not sharded" ) || ex.Message.Contains( "does not have a routing table" ) ) + // return false; + // } + //} + + public static async Task IsSharded( this IMongoDbDatabaseAdminService db, string database, string collectionName ) + { + var stats = await db.CollStats($"{database}.{collectionName}" ); + return stats.Sharded; + } + + public static async Task IsSharded( this IMongoDbDatabaseAdminService db, string collectionName ) + { + var stats = await db.CollStats(collectionName); + return stats.Sharded; + } + + public static async Task CollStats( + this IMongoDbDatabaseAdminService service, + string collectionName, + CancellationToken token = default) + { + var command = new BsonDocument + { + { "collStats", collectionName }, + { "scale", 1 }, + }; + var res = await service.RunCommand(command, token); + + var stats = new CollectionStats + { + Name = res["ns"].ToString() ?? "", + Size = res["size"].ToInt64(), + Count = res["count"].ToInt64(), + StorageSize = res["storageSize"].ToInt64(), + TotalIndexSize = res["totalIndexSize"].ToInt64(), + TotalSize = res["totalSize"].ToInt64(), + Sharded = res.Contains("sharded") && res["sharded"].ToBoolean(), + Details = res + }; + return stats; + } + + public static async Task CollStatsDetailed( + this IMongoDbDatabaseAdminService service, + string collectionName, + CancellationToken token = default) + { + var command = new BsonDocument + { + { "collStats", collectionName }, + { "scale", 1 }, + }; + var res = await service.RunCommand(command, token); + var json = res.ToJson(); + return CollStatsModel.FromJson(json); + } + + public static async Task DbStats( + this IMongoDbDatabaseAdminService service, + CancellationToken token = default) + { + var command = new BsonDocument + { + { "dbStats", 1 }, + { "freeStorage", 1 }, + }; + var res = await service.RunCommand(command, token); + return DatabaseStatsModel.FromBson(res); + } + + public static async Task DropRole( + this IMongoDbDatabaseAdminService service, + string roleName, + CancellationToken token = default) + { + var command = new BsonDocument + { + { "dropRole", roleName }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + + await service.RunCommand(command, token); + } + + public static async Task CreateRole( + this IMongoDbDatabaseAdminService service, + RoleInfoModel role, + CancellationToken token = default) + { + var command = role.CreateUpdateRoleCommand(true); + await service.RunCommand(command, token); + } + + public static async Task UpdateRole( + this IMongoDbDatabaseAdminService service, + RoleInfoModel role, + CancellationToken token = default) + { + var command = role.CreateUpdateRoleCommand(false); + await service.RunCommand(command, token); + } + + + public static async Task GetRolesInfo( + this IMongoDbDatabaseAdminService service, + string? roleName = null, + string? dbName = null, + bool showBuiltInRoles = true, + CancellationToken token = default) + { + BsonDocument command; + if ( string.IsNullOrWhiteSpace(roleName) ) + { + command = new () + { + { "rolesInfo", 1 }, + { "showPrivileges", true }, + { "showBuiltinRoles", showBuiltInRoles } + }; + } + else if (dbName == null) + { + command = new() + { + { "rolesInfo", roleName }, + { "showPrivileges", true }, + { "showBuiltinRoles", showBuiltInRoles } + }; + } + else + { + command = new() + { + { "rolesInfo", new BsonDocument + { + { "role", roleName }, + { "db", dbName } + } + }, + { "showPrivileges", true }, + { "showBuiltinRoles", showBuiltInRoles } + }; + } + + var res = await service.RunCommand(command, token); + return RolesInfoModel.FromBson(res); + } + + public static async Task GetIndexes( + this IMongoDbDatabaseAdminService service, + string collectionName, + CancellationToken token = default) + { + var command = new BsonDocument() + { + { "listIndexes", collectionName } + }; + + var res = await service.RunCommand(command, token); + return IndexesInfoModel.FromBson(res); + } + + public static async Task GetUsersInfo( + this IMongoDbDatabaseAdminService service, + CancellationToken token = default) + { + var command = new BsonDocument() + { + { "usersInfo", 1 } + }; + + var res = await service.RunCommand(command, token); + return UsersModel.FromBson(res); + } + + public static async Task DropUser( + this IMongoDbDatabaseAdminService service, + string userName, + CancellationToken token = default) + { + var command = new BsonDocument() + { + { "dropUser", userName }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + + _ = await service.RunCommand(command, token); + } + + public static async Task GetLogCategories( + this IMongoDbDatabaseAdminService service, + CancellationToken token = default + ) + { + var command = new BsonDocument + { + { "getLog", "*" } + }; + var doc = await service.RunCommand(command, token); + + var logs = doc.Contains("names") ? doc["names"].AsBsonArray : new(); + + return logs.Where(log => log.IsString).Select(log => log.AsString).ToArray(); + } + + public static async Task> GetLogs( + this IMongoDbDatabaseAdminService service, + string logCategory = "global", + CancellationToken token = default + ) + { + var command = new BsonDocument + { + { "getLog", logCategory } + }; + var doc = await service.RunCommand(command, token); + + var logs = doc.Contains("log") ? doc["log"].AsBsonArray : new(); + var res = new List(); + + foreach (var log in logs) + { + if (log is BsonDocument d) + { + res.Add(LogRecordModel.FromBson(d)); + } + else if (log.IsString) + { + try + { + var bson = MongoDB.Bson.Serialization.BsonSerializer.Deserialize(log.AsString); + res.Add(LogRecordModel.FromBson(bson)); + } + catch + { + // ignore + } + } + } + + return res; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/BsonMongoDbService.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/BsonMongoDbService.cs new file mode 100644 index 0000000..a86841b --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/BsonMongoDbService.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using System.Diagnostics.CodeAnalysis; + + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public delegate IMongoDbService BsonMongoDbServiceFactory(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? databaseInstance = null); + +/// +/// An implementation of the mongoservice interface that allows you to interact with a collection of BsonDocuments +/// This is ultimately all our collections, so this implementation can be used as a generic interaface to all our collections +/// +[ExcludeFromCodeCoverage] +public class BsonMongoDbService : MongoDbServiceBase +{ + /// + /// An implementation of the mongoservice interface that allows you to interact with a collection of BsonDocuments + /// This is ultimately all our collections, so this implementation can be used as a generic interaface to all our collections + /// + public BsonMongoDbService(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? databaseInstance = null) + : base(config, settings, collectionName, databaseInstance) + { + } + + public static IMongoDbService DefaultFactory(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? databaseInstance = null) + => new BsonMongoDbService(config, settings, collectionName, databaseInstance); + + protected override int ProcessDuplicates(HashSet duplicateIds, List dataList) + { + //upsert the duplicate bson items + var replaced = 0; + + foreach (var item in dataList.Select( x => new { Id = x.GetElement("_id").Value.AsString, Data = x }).Where(x => duplicateIds.Contains(x.Id))) + { + Collection.ReplaceOne($"{{ _id : \"{item.Id}\"}}", item.Data); + replaced += 1; + } + + return replaced; + } +} + +public static class BsonMongoDbServiceExtensions +{ + public static IServiceCollection AddBsonMongoDbServiceFactory(this IServiceCollection services) + { + services.AddSingleton(BsonMongoDbService.DefaultFactory); + return services; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/CobDocument.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/CobDocument.cs new file mode 100644 index 0000000..16c08a4 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/CobDocument.cs @@ -0,0 +1,53 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Runtime.Serialization; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +[DataContract +, Serializable +] +public abstract class CobDocument : MongoDbDocumentBase, ICobDocument +{ + public override string ToString() => Id ?? GetType().Name; + + private string _layer = ""; + + public static string MakeId(DateTime cob, string id, string? layer = null) + { + if (cob != default) + return string.IsNullOrWhiteSpace(layer) + ? $"{cob:yyyyMMdd}-{id}" + : $"{cob:yyyyMMdd}-{id}-{layer}" + ; + return string.IsNullOrWhiteSpace(layer) + ? $"{id}" + : $"{id}-{layer}" + ; + } + + [DataMember] public DateTime ExpireAt { get; set; } + [DataMember] + public string Layer + { + get => _layer; + set => _layer = string.IsNullOrWhiteSpace(value) ? "" : string.Intern(value); + } + [DataMember] public DateTime COB { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/DbDocumentMongoDbService.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/DbDocumentMongoDbService.cs new file mode 100644 index 0000000..a6b275c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/DbDocumentMongoDbService.cs @@ -0,0 +1,69 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public delegate IMongoDbService DbDocumentMongoDbServiceFactory(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? database = null, string? url = null, string? databaseInstance = null) where T : IMongoDbDocumentBase; + + +/// +/// An implementation of the mongo service interface for collections of documents that service from IMongoDbDocumentBase +/// +/// +[ExcludeFromCodeCoverage] +public class DbDocumentMongoDbService : MongoDbServiceBase where T: class, IMongoDbDocumentBase +{ + /// + /// An implementation of the mongo service interface for collections of documents that service from IMongoDbDocumentBase + /// + public DbDocumentMongoDbService(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? databaseInstance = null) + : base(config, settings, collectionName, databaseInstance) + { + } + + public static IMongoDbService DefaultFactory(MongoDbConfigRecord config, MongoDbSettings settings, string collectionName, string? database = null, string? url = null, string? databaseInstance = null) + => new DbDocumentMongoDbService(config, settings, collectionName, databaseInstance); + + protected override int ProcessDuplicates(HashSet duplicateIds, List dataList) + { + //upset the duplicate bson items + var replaced = 0; + + //extract all the items in the datalist with the duplicate ids + foreach (var item in dataList.Where(x => duplicateIds.Contains(x.Id))) + { + Collection.ReplaceOne(x => x.Id == item.Id, item); //TODO figure out how to call the base impl + replaced += 1; + } + + return replaced; + } +} + +public static class DbDocumentMongoDbServiceExtensions +{ + public static IServiceCollection AddMongoDbService(this IServiceCollection services) where T : class, IMongoDbDocumentBase + { + services.AddSingleton>(DbDocumentMongoDbService.DefaultFactory); + return services; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/GridDictionarySerializer.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/GridDictionarySerializer.cs new file mode 100644 index 0000000..44bbca5 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/GridDictionarySerializer.cs @@ -0,0 +1,36 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public class GridDictionarySerializer : DictionarySerializerBase, T>> +{ + public GridDictionarySerializer() + : base(DictionaryRepresentation.Document, new RiskGridKeySerializer(), BsonSerializer.LookupSerializer() ) + { + } + public GridDictionarySerializer(IBsonSerializer valueSerializer) + : base(DictionaryRepresentation.Document, new RiskGridKeySerializer(), valueSerializer) + { + } + protected override Dictionary, T> CreateInstance() => []; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/ICobDocument.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/ICobDocument.cs new file mode 100644 index 0000000..d2b10f7 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/ICobDocument.cs @@ -0,0 +1,35 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public interface ICobDocument : IMongoDbDocumentBase +{ + DateTime ExpireAt { get; set; } + string Layer { get; set; } + public DateTime COB { get; set; } + + public void CopyFrom(ICobDocument other) + { + CopyFrom((IMongoDbDocumentBase)other); + + ExpireAt = other.ExpireAt; + Layer = other.Layer; + COB = other.COB; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoClientFactory.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoClientFactory.cs new file mode 100644 index 0000000..a3af9f1 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoClientFactory.cs @@ -0,0 +1,28 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public interface IMongoClientFactory +{ + MongoClient GetClient(); + MongoClient GetClient(MongoClientSettings settings); + MongoClientSettings GetMongoClientSettings(string? mongoUrl = null); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDatabaseAdminService.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDatabaseAdminService.cs new file mode 100644 index 0000000..f316b2a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDatabaseAdminService.cs @@ -0,0 +1,44 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public interface IMongoDbDatabaseAdminService +{ + /// + /// Database name + /// + string Database { get; } + + /// + /// List of all available collections + /// + Task> ListCollections(CancellationToken token = default); + + /// + /// Run a command on MongoDb database + /// + public Task RunCommand(BsonDocument doc, CancellationToken token = default) => RunCommand(doc, null, token); + + /// + /// Run a command on MongoDb database + /// + Task RunCommand(BsonDocument doc, string? originalCommand, CancellationToken token = default); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDocumentBase.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDocumentBase.cs new file mode 100644 index 0000000..5882acc --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbDocumentBase.cs @@ -0,0 +1,26 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public interface IMongoDbDocumentBase +{ + string Id { get; set; } + + public void CopyFrom(IMongoDbDocumentBase other) => Id = other.Id; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbService.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbService.cs new file mode 100644 index 0000000..2660382 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/IMongoDbService.cs @@ -0,0 +1,140 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +/// +/// Non templated portion of mongo interface +/// +public interface IGenericMongoDbServices +{ + /// Name of the collection this instance operates on + string CollectionName { get; } + + /// Number of records in the collection + long Count { get; } + + /// + /// Delete the entire collection. For temporary collections only! + /// + void DeleteCollection(); + + /// + /// Run aggregate query async. No support for reties. + /// + /// Aggregation pipeline query (JSON) + /// Maximum number of documents fetched. Use -1 for no limit. + /// + /// + IAsyncEnumerable> AggregateAsync(string jsonPipeline, int maxFetchSize = -1, CancellationToken token = default); + + /// + /// Run aggregate query async. No support for reties. + /// + /// Aggregation pipeline query (JSON) + /// Maximum number of documents fetched. Use -1 for no limit. + /// + /// + IAsyncEnumerable AggregateAsyncRaw(string jsonPipeline, int maxFetchSize = -1, CancellationToken token = default); + + /// + /// Explain the command execution plan + /// + Task ExplainAsync(string command, CancellationToken token = default); + + /// + /// Clear all data for given COB/layer/book/root + /// + /// + /// + /// + /// ??? + /// + Task ClearCOBAsync(DateTime cob, string? layer = null, string? book = null, string? root = null, CancellationToken token = default); +} + +/// +/// MongoDB interface for collections where all documents have type T (generic argument) +/// +/// +public interface IMongoDbService : IGenericMongoDbServices +{ + /// + /// Insert documents into collection + /// + /// Data to insert + /// If true data will replace existing documents with the same keys + /// + /// + /// + Task InsertAsync(IEnumerable data, bool overrideExisting, bool suppressWarning = false, CancellationToken token = default); + + /// + /// Get documents matching the filter + /// + /// + /// Enables retry logic. Warning: it sorting the results. Do not use for large result sets! + /// Optional projection definition + /// Max number of results returned + /// + /// + IAsyncEnumerable FindAsync(FilterDefinition filter, bool allowRetries = true, ProjectionDefinition? projection = null, int? limit = null, CancellationToken token = default); + + /// + /// Get documents matching the filter + /// + /// + /// + /// + Task CountAsync(FilterDefinition filter, CancellationToken token = default); + + /// + /// Get only keys matching the filter (not documents!) + /// + /// + /// + /// + IEnumerable FindKeys(FilterDefinition filter, CancellationToken token = default); + + /// + /// Update document(s) + /// + /// Filter + /// Update definition + /// + void UpdateOne( FilterDefinition filter, UpdateDefinition update, CancellationToken token = default ); + + /// + /// Replace document + /// + /// Filter selecting a single document + /// New value + /// + void ReplaceOne( FilterDefinition filter, T doc, CancellationToken token = default ); + + /// + /// Delete documents matching the filter + /// + /// Filter selecting a single document + /// + /// Number of deleted documents + Task Delete( FilterDefinition filter, CancellationToken token = default ); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbConfig.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbConfig.cs new file mode 100644 index 0000000..0aba965 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbConfig.cs @@ -0,0 +1,211 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public class MongoDbAuth +{ + public string User { get; set; } = MongoDbConfig.MongoDbUser ; + public string Password { get; set; } = MongoDbConfig.MongoDbPassword; + public string? AuthDatabase { get; set; } = MongoDbConfig.MongoDbAuthDatabase; + public string? Method { get; set; } = MongoDbConfig.MongoDbAuthMethod; + + public MongoDbAuth Clone() + => new() + { + User = User , + Password = Password , + AuthDatabase = AuthDatabase, + Method = Method + }; +} + +public class MongoDbSettings +{ + public TimeSpan MongoDbSocketTimeout { get; set; } = MongoDbConfig.MongoDbSocketTimeout ; + public TimeSpan MongoDbServerSelectionTimeout { get; set; } = MongoDbConfig.MongoDbServerSelectionTimeout; + public TimeSpan MongoDbConnectTimeout { get; set; } = MongoDbConfig.MongoDbConnectTimeout ; + public int MongoDbMinConnectionPoolSize { get; set; } = MongoDbConfig.MongoDbMinConnectionPoolSize ; + public int MongoDbMaxConnectionPoolSize { get; set; } = MongoDbConfig.MongoDbMaxConnectionPoolSize ; + public TimeSpan MaxConnectionIdleTime { get; set; } = MongoDbConfig.MongoDbMaxConnectionIdleTime ; + public TimeSpan MaxConnectionLifeTime { get; set; } = MongoDbConfig.MongoDbMaxConnectionLifeTime ; + public TimeSpan MongoDbQueryTimeout { get; set; } = MongoDbConfig.MongoDbQueryTimeout ; + public int MongoDbPingTimeoutSec { get; set; } = MongoDbConfig.MongoDbPingTimeoutSec ; + public int MongoDbQueryBatchSize { get; set; } = MongoDbConfig.MongoDbQueryBatchSize ; + public int MongoDbConnectionRetries { get; set; } = MongoDbConfig.MongoDbConnectionRetries ; + public int MongoDbRetryTimeoutSec { get; set; } = MongoDbConfig.MongoDbRetryTimeoutSec ; + public TimeSpan CollectionsCachingPeriod { get; set; } = TimeSpan.FromHours(2); +} + +public class MongoDbConfigRecord +{ + public string MongoDbUrl { get; set; } = MongoDbConfig.MongoDbUrl ; + public string MongoDbDatabase { get; set; } = MongoDbConfig.MongoDbDatabase ; + public MongoDbAuth? Auth { get; set; } + public MongoDbAuth? AdminAuth { get; set; } + public bool DirectConnection { get; set; } = MongoDbConfig.MongoDbDirectConnection ; + public bool UseTls { get; set; } = MongoDbConfig.MongoDbUseTls ; + public bool AllowShardAccess { get; set; } = MongoDbConfig.MongoDbAllowShardAccess; + + public override string ToString() => GetKey(); + + public string GetKey(string? databaseInstance = null) + => $"Url=\"{MongoDbUrl}\" Database=\"{databaseInstance ?? MongoDbDatabase}\" Direct={DirectConnection} Tls={UseTls} User=\"{Auth?.User ?? "null"}\" AdminUser=\"{AdminAuth?.User ?? Auth?.User ?? "null"}\""; + + public MongoDbConfigRecord Clone() + => new() + { + MongoDbUrl = MongoDbUrl , + MongoDbDatabase = MongoDbDatabase , + Auth = Auth?.Clone() , + AdminAuth = AdminAuth?.Clone(), + DirectConnection = DirectConnection , + UseTls = UseTls , + AllowShardAccess = AllowShardAccess + }; + + public void Check() + { + if (MongoDbUrl?.StartsWith("<<") ?? true) + throw new ApplicationException($"Invalid MongoDB URL: {MongoDbUrl}"); + } +} + +/// +/// Do not use these values directly. Always use see . +/// +public static class MongoDbConfig +{ + // MongoDbConfigRecord + public static string MongoDbUrl = "<>"; + public static string MongoDbDatabase = "System"; + public static string MongoDbUser = ""; + public static string MongoDbPassword = ""; + public static string MongoDbAuthMethod = ""; + public static string MongoDbAuthDatabase = "admin"; + public static string MongoDbAdminUser = ""; + public static string MongoDbAdminPassword = ""; + public static string MongoDbAdminDatabase = "admin"; + public static bool MongoDbDirectConnection; + public static bool MongoDbAllowShardAccess; + public static bool MongoDbUseTls; + + // MongoDbSettings + public static TimeSpan MongoDbSocketTimeout = MongoDefaults.SocketTimeout; + public static TimeSpan MongoDbServerSelectionTimeout = MongoDefaults.ServerSelectionTimeout; + public static TimeSpan MongoDbConnectTimeout = MongoDefaults.ConnectTimeout; + public static int MongoDbMinConnectionPoolSize = MongoDefaults.MinConnectionPoolSize; + public static int MongoDbMaxConnectionPoolSize = MongoDefaults.MaxConnectionPoolSize; + public static TimeSpan MongoDbMaxConnectionIdleTime = MongoDefaults.MaxConnectionIdleTime; // TimeSpan.FromHours(8); // mongo default - 10 mins + public static TimeSpan MongoDbMaxConnectionLifeTime = MongoDefaults.MaxConnectionLifeTime; // TimeSpan.FromHours(8); // mongo default - 30 mins + public static TimeSpan MongoDbQueryTimeout = TimeSpan.FromMinutes(60); + public static int MongoDbPingTimeoutSec = 60; + public static int MongoDbQueryBatchSize = 5_000; + public static int MongoDbConnectionRetries = 5; + public static int MongoDbRetryTimeoutSec = 5; + + public static MongoDbConfigRecord GetConfigRecord() + { + var res = new MongoDbConfigRecord + { + MongoDbUrl = MongoDbUrl, + MongoDbDatabase = MongoDbDatabase, + DirectConnection = MongoDbDirectConnection, + UseTls = MongoDbUseTls, + AllowShardAccess = MongoDbAllowShardAccess + }; + + if ( !string.IsNullOrEmpty(MongoDbUser) || !string.IsNullOrEmpty(MongoDbPassword) ) + { + res.Auth = new () + { + User = MongoDbUser, + Password = MongoDbPassword, + AuthDatabase = MongoDbAuthDatabase, + Method = MongoDbAuthMethod + }; + } + + if ( !string.IsNullOrEmpty(MongoDbAdminUser) || !string.IsNullOrEmpty(MongoDbAdminPassword) ) + { + res.AdminAuth = new () + { + User = MongoDbAdminUser, + Password = MongoDbAdminPassword, + AuthDatabase = MongoDbAdminDatabase, + Method = MongoDbAuthMethod + }; + } + + res.Check(); + return res; + } + + public static MongoDbSettings GetSettings() + { + var res = new MongoDbSettings + { + MongoDbSocketTimeout = MongoDbSocketTimeout, + MongoDbServerSelectionTimeout = MongoDbServerSelectionTimeout, + MongoDbConnectTimeout = MongoDbConnectTimeout, + MongoDbMinConnectionPoolSize = MongoDbMinConnectionPoolSize, + MongoDbMaxConnectionPoolSize = MongoDbMaxConnectionPoolSize, + MaxConnectionIdleTime = MongoDbMaxConnectionIdleTime, + MaxConnectionLifeTime = MongoDbMaxConnectionLifeTime, + MongoDbQueryTimeout = MongoDbQueryTimeout, + MongoDbPingTimeoutSec = MongoDbPingTimeoutSec, + MongoDbQueryBatchSize = MongoDbQueryBatchSize, + MongoDbConnectionRetries = MongoDbConnectionRetries, + MongoDbRetryTimeoutSec = MongoDbRetryTimeoutSec + }; + return res; + } + + public static void CopyFrom(MongoDbConfigRecord rec, MongoDbSettings settings) + { + rec.Check(); + + MongoDbUrl = rec.MongoDbUrl; + MongoDbDatabase = rec.MongoDbDatabase; + MongoDbUser = rec.Auth?.User ?? ""; + MongoDbPassword = rec.Auth?.Password ?? ""; + MongoDbAuthDatabase = rec.Auth?.AuthDatabase ?? "admin"; + MongoDbAuthMethod = rec.Auth?.Method ?? ""; + MongoDbAdminUser = rec.AdminAuth?.User ?? ""; + MongoDbAdminPassword = rec.AdminAuth?.Password ?? ""; + MongoDbAdminDatabase = rec.AdminAuth?.AuthDatabase ?? "admin"; + MongoDbDirectConnection = rec.DirectConnection; + MongoDbUseTls = rec.UseTls; + MongoDbAllowShardAccess = rec.AllowShardAccess; + + MongoDbSocketTimeout = settings.MongoDbSocketTimeout; + MongoDbServerSelectionTimeout = settings.MongoDbServerSelectionTimeout; + MongoDbConnectTimeout = settings.MongoDbConnectTimeout; + MongoDbMinConnectionPoolSize = settings.MongoDbMinConnectionPoolSize; + MongoDbMaxConnectionPoolSize = settings.MongoDbMaxConnectionPoolSize; + MongoDbMaxConnectionIdleTime = settings.MaxConnectionIdleTime; + MongoDbMaxConnectionLifeTime = settings.MaxConnectionLifeTime; + MongoDbQueryTimeout = settings.MongoDbQueryTimeout; + MongoDbPingTimeoutSec = settings.MongoDbPingTimeoutSec; + MongoDbQueryBatchSize = settings.MongoDbQueryBatchSize; + MongoDbConnectionRetries = settings.MongoDbConnectionRetries; + MongoDbRetryTimeoutSec = settings.MongoDbRetryTimeoutSec; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDatabaseAdminService.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDatabaseAdminService.cs new file mode 100644 index 0000000..0a2fd75 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDatabaseAdminService.cs @@ -0,0 +1,61 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public class MongoDbDatabaseAdminService(MongoDbConfigRecord _config, MongoDbSettings _settings, string? _databaseInstance = null) : IMongoDbDatabaseAdminService +{ + private readonly Lazy _db = new(() => MongoDbHelper.GetDatabase(_config, _settings, _databaseInstance ?? _config.MongoDbDatabase)); + + public string Database => !string.IsNullOrWhiteSpace(_databaseInstance) + ? _databaseInstance + : _config.MongoDbDatabase + ; + + public async Task> ListCollections(CancellationToken token = default) + { + var db = MongoDbHelper.GetDatabase(_config, _settings, Database); + var cursor = await db.ListCollectionNamesAsync(cancellationToken: token); + var docs = await cursor.ToListAsync(cancellationToken: token); + var coll = docs + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Where(x => x != null) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x) + .ToList(); + + return coll; + } + + public async Task RunCommand(BsonDocument doc, string? originalCommand, CancellationToken token = default ) + { + if ( doc.ElementCount == 0 ) + throw new ApplicationException("Empty command"); + + var res = await _db.Value.RunCommandAsync(new BsonDocumentCommand(doc), cancellationToken: token); + + if (res.Contains("ok") && !res["ok"].ToBoolean()) + throw new ApplicationException($"Failed to execute command: {doc.ElementAt(0).Name}"); + + return res; + } + +} diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDocumentBase.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDocumentBase.cs new file mode 100644 index 0000000..a09aaa5 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbDocumentBase.cs @@ -0,0 +1,54 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Runtime.Serialization; +using MongoDB.Bson.Serialization.Attributes; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +[DataContract +, Serializable +] +public abstract class MongoDbDocumentBase : IMongoDbDocumentBase +{ + public override string ToString() => Id ?? GetType().Name; + + private string _id = ""; + + /// + /// Method used to build document id from fields. + /// The method MUST return null in case of insufficient data. + /// Once value returned Id field will NOT be automatically updated on any changes to the data used to make Id + /// + /// Id string + protected abstract string BuildId(); + + [BsonId] + [DataMember] + public string Id + { + get + { + if ( !string.IsNullOrWhiteSpace( _id ) ) + return _id; + _id = BuildId(); + return _id; + } + set => _id = value; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbHelper.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbHelper.cs new file mode 100644 index 0000000..367a793 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbHelper.cs @@ -0,0 +1,311 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics.CodeAnalysis; +using System.Net.Security; +using System.Reflection; +using System.Text.RegularExpressions; +using log4net; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +public static class MongoDbHelperConfig +{ + // ReSharper disable ConvertToConstant.Global + // ReSharper disable InconsistentNaming +#pragma warning disable CA2211 + + public static bool MongoDbHelper_ShouldDispose = false; + public static int MongoDbHelper_ExpirationMinutes = 30; + public static int MongoDbHelper_CleanupIntervalMinutes = 5; + +#pragma warning restore CA2211 + // ReSharper restore InconsistentNaming + // ReSharper restore ConvertToConstant.Global +} + +[ExcludeFromCodeCoverage] +public static class MongoDbHelper +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + private static bool _initialised; + private static readonly Lock _globalSyncObject = new (); + + public static RemoteCertificateValidationCallback? RemoteCertificateCheck { get; set; } + + private static void Init() + { + if ( _initialised ) + return; + + lock (_globalSyncObject) + { + if ( _initialised ) + return; + try + { + InitUnsafe(); + } + finally + { + _initialised = true; + } + } + } + + private static void InitUnsafe() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(MongoDatabaseInfo))) + BsonClassMap.RegisterClassMap(cm => + { + cm.MapProperty(c => c.Name).SetElementName("name"); + cm.MapProperty(c => c.SizeOnDisk).SetElementName("sizeOnDisk"); + cm.MapProperty(c => c.Empty).SetElementName("empty"); + }); + } + + + private static DateTime _lastChecked; + + public static bool IsConnected(IMongoDatabase? database, bool force = false) + { + if (database == null) + return false; + + try + { + lock (_globalSyncObject) + { + if (_lastChecked + TimeSpan.FromMinutes(1) < DateTime.Now && force == false) + return true; + + if (IsAlive(database, TimeSpan.FromSeconds(5), out _)) + { + _lastChecked = DateTime.Now; + return true; + } + } + } + catch (Exception e) + { + _log.Warn("MongoDB connectivity lost. Reconnecting...", e); + } + + return false; + } + + private class MongoDbClientRecord(MongoClient client, IMongoDatabase database) + { + public IMongoDatabase Database { get; } = database ?? throw new ArgumentNullException(nameof(database)); + // ReSharper disable once UnusedMember.Local + public MongoClient Client { get; } = client ?? throw new ArgumentNullException(nameof(client)); + public DateTime LastChecked { get; set; } = DateTime.Now; + } + + private static readonly Lazy> _databases = new ( () => new( + TimeSpan.FromMinutes(MongoDbHelperConfig.MongoDbHelper_ExpirationMinutes), + TimeSpan.FromMinutes(MongoDbHelperConfig.MongoDbHelper_CleanupIntervalMinutes), + MongoDbHelperConfig.MongoDbHelper_ShouldDispose + ) ); + + /// + /// Connect to the database with retries. + /// + public static IMongoDatabase GetDatabase( + MongoDbConfigRecord config, + MongoDbSettings mongoSettings, + string? databaseInstance = null + ) + { + var key = config.GetKey(databaseInstance); + Exception? firstException = null; + + for ( var i = 0 ; i < mongoSettings.MongoDbConnectionRetries; i++ ) + { + var db = _databases.Value.GetOrAdd( + key, + _ => + { + var rec = GetDatabaseInternal(config, mongoSettings, databaseInstance); + return rec; + } + ); + + if ( db.LastChecked + TimeSpan.FromMinutes(1) < DateTime.Now ) + { + if ( !IsAlive(db.Database, TimeSpan.FromSeconds(mongoSettings.MongoDbPingTimeoutSec), out var exception)) + { + firstException ??= exception; + _databases.Value.TryRemove(key); + continue; // Try to reconnect + } + db.LastChecked = DateTime.Now; + } + + return db.Database; + } + + throw new ApplicationException($"Failed to connect to MongoDB after {mongoSettings.MongoDbConnectionRetries} retries. {key}", firstException); + } + + private static MongoDbClientRecord GetDatabaseInternal( + MongoDbConfigRecord config, + MongoDbSettings mongoSettings, + string? databaseInstance = null + ) => RetryHelper.DoWithRetries( () => + { + Init(); + + var database = databaseInstance ?? config.MongoDbDatabase; + + _log.Debug($"Connecting to {config.GetKey(database)}"); + + var settings = GetMongoClientSettings(config, mongoSettings,database == "admin"); + + var client = new MongoClient(settings); + var db = client.GetDatabase(database); + + if (!IsAlive(db, TimeSpan.FromSeconds(mongoSettings.MongoDbPingTimeoutSec), out var exception)) + throw new ApplicationException($"{exception?.Message} MongoDB connectivity lost {config.GetKey(database)}", exception); + + return new MongoDbClientRecord(client, db); + }, + mongoSettings.MongoDbConnectionRetries, + TimeSpan.FromSeconds(mongoSettings.MongoDbRetryTimeoutSec), + null, + LogMethod + ); + + private static bool IsAlive(IMongoDatabase db, TimeSpan pingTimeout, out Exception? exception ) + { + try + { + exception = null; + var cts = new CancellationTokenSource(pingTimeout); + var isMongoLive = db.RunCommand((Command)"{ping:1}", cancellationToken: cts.Token); + + if (isMongoLive == null) + return false; + + return true; + } + catch (Exception ex) + { + exception = ex; + return false; + } + } + + /// + /// Log method for retry function, called for each iteration + /// + /// + /// + /// + private static void LogMethod(int iteration, int maxRetries, Exception e) + { + if (iteration < maxRetries - 1) + _log.Warn($"MongoDB connection error, retrying RetriesLeft={maxRetries - iteration - 1}\": {e.Message}", e); + else + _log.Error($"MongoDB connection error, retries exhausted: {e.Message}", e); + } + + public static MongoClientSettings GetMongoClientSettings( + MongoDbConfigRecord config, + MongoDbSettings mongoSettings, + bool toAdmin + ) + { + if (config == null) + throw new ArgumentNullException(nameof(config)); + + var settings = MongoClientSettings.FromUrl( new( config.MongoDbUrl ) ); + + var auth = toAdmin + ? config.AdminAuth ?? config.Auth + : config.Auth; + + if ( !string.IsNullOrWhiteSpace(auth?.User) && !string.IsNullOrWhiteSpace(auth.Password) ) + { + var credential = new MongoCredential( + auth.Method, + new MongoInternalIdentity(auth.AuthDatabase ?? "admin", auth.User), + new PasswordEvidence(auth.Password)); + + settings.Credential = credential; + } + + settings.SocketTimeout = mongoSettings.MongoDbSocketTimeout; + settings.ServerSelectionTimeout = mongoSettings.MongoDbServerSelectionTimeout; + settings.ConnectTimeout = mongoSettings.MongoDbConnectTimeout; + settings.MinConnectionPoolSize = mongoSettings.MongoDbMinConnectionPoolSize; + settings.MaxConnectionPoolSize = mongoSettings.MongoDbMaxConnectionPoolSize; + settings.MaxConnectionIdleTime = mongoSettings.MaxConnectionIdleTime; + settings.MaxConnectionLifeTime = mongoSettings.MaxConnectionLifeTime; + settings.DirectConnection = config.DirectConnection; + settings.UseTls = config.UseTls; + + if (RemoteCertificateCheck != null) + settings.SslSettings.ServerCertificateValidationCallback = RemoteCertificateCheck; + + // This doc seems to suggest we can turn writeretries on (as we have mongos, with a sharded cluster) + // https://docs.mongodb.com/manual/core/retryable-writes/ But when we enable it we get an exception + //"One or more errors occurred. (A bulk write operation resulted in one or more errors. Transaction numbers are only allowed on a replica set member or mongos" + + settings.RetryReads = false; + settings.RetryWrites = false; + return settings; + } + + private static int _counter; + + public static string GetTempCollectionName(string? prefix = null) => + string.IsNullOrWhiteSpace(prefix) + ? $"Temp-{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}-{Interlocked.Increment(ref _counter)}" + : $"Temp-{prefix}-{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}-{Interlocked.Increment(ref _counter)}"; + + + public static HashSet ExtractDuplicateKeys( Exception ex ) + { + var rx = new Regex( """.*E11000 duplicate key error collection.*\{[^\:]*: "(?[^"]+)".*""", + RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.Singleline ); + + + var toProcess = new HashSet( + ex.Message + .Split( "Category" ) + .Select( x => + { + var m = rx.Match( x ); + return m.Success + ? m.Groups["Id"].Value + : null; + } ) + .Where( x => x != null ) + .OfType() + ); + return toProcess; + } + + public static bool IsDuplicateKeyError( Exception ex ) => ex.Message.IndexOf("duplicate key error", StringComparison.Ordinal) > 0; +} diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbServiceBase.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbServiceBase.cs new file mode 100644 index 0000000..3d08399 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/MongoDbServiceBase.cs @@ -0,0 +1,530 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +internal static class FetchInfo +{ + public static long FetchId; + public static int ParallelFinds; +} + + +public abstract class MongoDbServiceBase : IMongoDbService where T : class +{ + // ReSharper disable once StaticMemberInGenericType + // ReSharper disable once InconsistentNaming + protected static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + protected readonly IMongoCollection Collection; + private readonly MongoDbSettings _settings; + + + public long Count => Collection.CountDocuments(x => true); + + public string CollectionName => Collection.CollectionNamespace.CollectionName; + + protected MongoDbServiceBase( + MongoDbConfigRecord config, + MongoDbSettings settings, + string collectionName, + string? databaseInstance = null) + { + _settings = settings; + var db = MongoDbHelper.GetDatabase(config, settings, databaseInstance); + Collection = db.GetCollection(collectionName); + } + + /// + /// When inserting items into the collection, sometimes we have duplicates, so this method handles the duplicate items + /// + /// + /// + /// + protected abstract int ProcessDuplicates(HashSet duplicateIds, List duplicateItems); + + public IEnumerable FindKeys(FilterDefinition filter, CancellationToken token = default) + => RetryHelper.DoWithRetries(() => FindKeysInternal(filter), 3, TimeSpan.FromSeconds(5), logFunc: LogMethod); + + public Task InsertAsync(IEnumerable data, bool overrideExisting, bool suppressWarning = false, + CancellationToken token = default) + => RetryHelper.DoWithRetriesAsync(() => InsertAsyncInternalAsync(data, overrideExisting, suppressWarning), 3, TimeSpan.FromSeconds(5), logFunc: LogMethod, token: token); + + public async IAsyncEnumerable> AggregateAsync( + string jsonPipeline, int maxFetchSize = -1, [EnumeratorCancellation] CancellationToken token = default) + { + await foreach (var doc in AggregateInternalAsync(jsonPipeline, maxFetchSize, token)) + { + yield return ConvertBsonToDictionary(doc); + } + } + + public IAsyncEnumerable AggregateAsyncRaw(string jsonPipeline, + int maxFetchSize = -1, CancellationToken token = default) + => AggregateInternalAsync(jsonPipeline, maxFetchSize, token); + + public Task ClearCOBAsync(DateTime cob, string? layer, string? book = null, string? root = null, CancellationToken token = default) + => RetryHelper.DoWithRetriesAsync(() => ClearCobInternalAsync(cob, layer, book, root, token), 3, TimeSpan.FromSeconds(5), logFunc: LogMethod, token: token); + + private const int ReportEveryNDocuments = 50_000; + private const int NumberOfRetries = 3; + + public IAsyncEnumerable FindAsync(FilterDefinition filter, bool allowRetries = true, + ProjectionDefinition? projection = null, int? limit = null, CancellationToken token = default) + => allowRetries + ? FindAllowRetries(RenderToBsonDocument(filter).ToJson(), RenderToBsonDocument(projection)?.ToJson(), limit, token) + : FindNoRetries(RenderToBsonDocument(filter).ToJson(), RenderToBsonDocument(projection)?.ToJson(), limit, token) + ; + + public Task CountAsync(FilterDefinition filter, CancellationToken token = default) + => CountNoRetries(RenderToBsonDocument(filter).ToJson(), token) + ; + + public async Task ExplainAsync(string command, CancellationToken token) + { + var explain = new BsonDocument + { + { "explain", BsonDocument.Parse(command) } + }; + + var res = await Collection.Database.RunCommandAsync(new BsonDocumentCommand(explain), cancellationToken: token); + return res; + } + + + public static BsonDocument? RenderToBsonDocument(FilterDefinition? filter) + { + if (filter == null) + return null; + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var args = new RenderArgs(documentSerializer, serializerRegistry); + return filter.Render(args); + } + + public static BsonDocument? RenderToBsonDocument(ProjectionDefinition? filter) + { + if (filter == null) + return null; + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var args = new RenderArgs(documentSerializer, serializerRegistry); + return filter.Render(args); + } + + private async Task CountNoRetries(string filter, CancellationToken token = default) + { + var options = new CountOptions + { + MaxTime = _settings.MongoDbQueryTimeout + }; + var count = await Collection.CountDocumentsAsync(filter, options, token); + return count; + } + + public async IAsyncEnumerable FindNoRetries(string filter, string? projection = null, int? limit = null, [EnumeratorCancellation] CancellationToken token = default) + { + var processed = 0; + var options = new FindOptions + { + MaxTime = _settings.MongoDbQueryTimeout, + NoCursorTimeout = true, + BatchSize = _settings.MongoDbQueryBatchSize, + }; + + if (limit != null) + options.Limit = limit.Value; + + if (projection != null) + options.Projection = projection; + + var jsonFilter = filter + .Replace("\n", " ") + .Replace("\r", " ") + .Replace("\t", " ") + .Replace(" ", " ") + ; + + var id = Interlocked.Increment(ref FetchInfo.FetchId); + Interlocked.Increment(ref FetchInfo.ParallelFinds); + try + { + var sw = Stopwatch.StartNew(); + + _log.Debug($"Id={id:000} Starting Find (no retries) Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Filter=\"{jsonFilter}\""); + + var cursor = await Collection.FindAsync(filter, options, token); + + if (sw.Elapsed > TimeSpan.FromSeconds(10)) + _log.Debug($"Id={id:000} Slow Find (no retries) Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Elapsed=\"{sw.Elapsed:g}\" Filter=\"{jsonFilter}\""); + + sw.Restart(); + + var prevBatchElapsed = TimeSpan.Zero; + + while (true) + { + IEnumerable batch; + try + { + if (!await cursor.MoveNextAsync(token)) + { + _log.Debug($"Id={id:000} Find complete (no retries) Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Docs={processed} " + + $"Elapsed=\"{sw.Elapsed:g}\" DocsSec={processed / (sw.ElapsedMilliseconds / 1000.0):0.00} Filter=\"{jsonFilter}\""); + yield break; + } + batch = cursor.Current; + } + catch (Exception e) + { + var dps = processed / (sw.ElapsedMilliseconds / 1000.0); + throw new ApplicationException($"Id={id:000} Find (no retries) failed: {e.Message}. " + + $"Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Docs={processed} Elapsed=\"{sw.Elapsed:g}\" DocsSec={dps:0.00} Filter=\"{jsonFilter}\"", e); + } + + foreach (var bson in batch) + { + yield return bson; + + processed += 1; + + if ((processed % ReportEveryNDocuments) != 0) + continue; + + var dps = ReportEveryNDocuments / ((sw.ElapsedMilliseconds - prevBatchElapsed.TotalMilliseconds) / 1000.0); + _log.Debug($"Id={id:000} Fetch {ReportEveryNDocuments:N0} documents (no retries) Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} " + + $"Elapsed=\"{sw.Elapsed - prevBatchElapsed:g}\" DocsSec={dps:0.00} (processed {processed:N0} so far)"); + prevBatchElapsed = sw.Elapsed; + } + } + } + finally + { + Interlocked.Decrement(ref FetchInfo.ParallelFinds); + } + } + + public async IAsyncEnumerable FindAllowRetries(string filter, string? projection = null, int? limit = null, [EnumeratorCancellation] CancellationToken token = default) + { + var processed = 0; + var attempts = 0; + Exception? firstException = null; + var options = new FindOptions + { + MaxTime = _settings.MongoDbQueryTimeout, + NoCursorTimeout = true, + BatchSize = _settings.MongoDbQueryBatchSize, + Sort = "{ _id: 1 }" + }; + + if (limit != null) + options.Limit = limit.Value; + + if (projection != null) + options.Projection = projection; + + var jsonFilter = filter + .Replace("\n", " ") + .Replace("\r", " ") + .Replace("\t", " ") + .Replace(" ", " ") + ; + + var id = Interlocked.Increment(ref FetchInfo.FetchId); + Interlocked.Increment(ref FetchInfo.ParallelFinds); + try + { + + var sw = Stopwatch.StartNew(); + + _log.Debug($"Id={id:000} Starting Find Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Filter=\"{jsonFilter}\""); + + //var coll = Collection.Database.GetCollection(CollectionName); + + //default is 10, prevent the cursor expiring between calling MoveNext() + //unrestricted, it appears batch sizes are ~10K when running locally + + //https://stackoverflow.com/questions/44248108/mongodb-error-getmore-command-failed-cursor-not-found + + //the cursor returns batches, so iterate through each batch + while (true) + { + options.Skip = processed; + var cursor = await Collection.FindAsync(filter, options, token); + + if (sw.Elapsed > TimeSpan.FromSeconds(5)) + _log.Debug($"Id={id:000} Slow Find Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Elapsed=\"{sw.Elapsed:g}\" Filter=\"{jsonFilter}\""); + + sw.Restart(); + + var prevBatchElapsed = TimeSpan.Zero; + + while (true) + { + IEnumerable batch; + try + { + if (!await cursor.MoveNextAsync(token)) + { + yield break; + } + batch = cursor.Current; + } + catch (Exception e) + { + attempts++; + + _log.Warn($"Id={id:000} Exception when calling Find Attempt={attempts}/3 Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Processed={processed} Filter=\"{jsonFilter}\"", e); + + firstException ??= e; + + if (attempts == NumberOfRetries) + { + var dps = processed / (sw.ElapsedMilliseconds / 1000.0); + throw new ApplicationException($"Id={id:000} Find failed: {firstException.Message}. " + + $"Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} Docs={processed} Elapsed=\"{sw.Elapsed:g}\" DocsSec={dps:0.00} Filter=\"{jsonFilter}\"", firstException); + } + + //pause before trying again + Thread.Sleep(TimeSpan.FromSeconds(5)); + break; //return back to the while loop + } + + foreach (var bson in batch) + { + yield return bson; + + processed++; + + if ((processed % ReportEveryNDocuments) != 0) + continue; + + var dps = ReportEveryNDocuments / ((sw.ElapsedMilliseconds - prevBatchElapsed.TotalMilliseconds) / 1000.0); + _log.Debug($"Id={id:000} Fetch {ReportEveryNDocuments:N0} documents Collection=\"{CollectionName}\" Concurrency={FetchInfo.ParallelFinds} " + + $"Elapsed=\"{sw.Elapsed - prevBatchElapsed:g}\" DocsSec={dps:0.00} (processed {processed:N0} so far)"); + prevBatchElapsed = sw.Elapsed; + + } + } + } + } + finally + { + Interlocked.Decrement(ref FetchInfo.ParallelFinds); + } + } + + public void DeleteCollection() => RetryHelper.DoWithRetries(() => Collection.Database.DropCollection(Collection.CollectionNamespace.CollectionName), 3, TimeSpan.FromSeconds(5)); + + public void UpdateOne(FilterDefinition filter, UpdateDefinition update, CancellationToken token = default) + => RetryHelper.DoWithRetries(() => Collection.UpdateOne(filter, update), 3, TimeSpan.FromSeconds(5)); + + public void ReplaceOne(FilterDefinition filter, T doc, CancellationToken token) => RetryHelper.DoWithRetries(() + => ReplaceOne(filter, doc, token), 3, TimeSpan.FromSeconds(5)); + + #region private methods + private IEnumerable FindKeysInternal(FilterDefinition filter) + { + var projection = BsonSerializer.Deserialize("{ _id : 1}"); + + var ids = Collection + .Find(filter) + .Project(projection) + .ToEnumerable() + .Select(doc => doc[0].AsString) + .ToList(); + return ids; + } + + private async Task InsertAsyncInternalAsync(IEnumerable data, bool overrideExisting, bool suppressWarning) + { + var ignored = 0; + + var options = new InsertManyOptions + { + BypassDocumentValidation = true, + IsOrdered = false + }; + + var dataList = data.ToList(); + if (dataList.Count == 0) + return 0; + + try + { + await Collection.InsertManyAsync(dataList, options); + } + catch (Exception ex) + { + if (MongoDbHelper.IsDuplicateKeyError(ex)) + { + var toProcess = MongoDbHelper.ExtractDuplicateKeys(ex); + if (!overrideExisting) + { + if ( !suppressWarning ) + _log.Warn($"Duplicate entries found and ignored. DataCount={dataList.Count} DuplicateCount={toProcess.Count} DocumentClass=\"{typeof(T).Name}\" Collection=\"{Collection.CollectionNamespace.CollectionName}\""); + ignored = toProcess.Count; + } + else + ProcessDuplicates(toProcess, dataList); + } + else + throw; + } + + //_log.Debug($"Collection=\"{Collection.CollectionNamespace.CollectionName}\" Inserted={dataList.Count - replaced - ignored} Replaced={replaced} Ignored={ignored} of Total={dataList.Count}"); + return dataList.Count - ignored; + } + + private async Task ClearCobInternalAsync(DateTime cob, string? layer, string? book = null, string? root = null, CancellationToken token = default) + { + var layerName = string.IsNullOrWhiteSpace(root) + ? "Layer" + : root + ".Layer"; + var bookName = string.IsNullOrWhiteSpace(root) + ? "Book" + : root + ".Book"; + var cobName = string.IsNullOrWhiteSpace(root) + ? "COB" + : root + ".COB"; + + FilterDefinition filter; + + if (!string.IsNullOrWhiteSpace(layer) && !string.IsNullOrWhiteSpace(book)) + filter = new FilterDefinitionBuilder().And( + new FilterDefinitionBuilder().Eq(cobName, cob.Date), + new FilterDefinitionBuilder().Eq(layerName, layer), + new FilterDefinitionBuilder().Eq(bookName, book) + ); + else if (!string.IsNullOrWhiteSpace(layer)) + filter = new FilterDefinitionBuilder().And( + new FilterDefinitionBuilder().Eq(cobName, cob.Date), + new FilterDefinitionBuilder().Eq(layerName, layer) + ) + ; + else filter = !string.IsNullOrWhiteSpace(book) + ? new FilterDefinitionBuilder().And( + new FilterDefinitionBuilder().Eq(cobName, cob.Date), + new FilterDefinitionBuilder().Eq(bookName, book) + ) + : new FilterDefinitionBuilder().Eq(cobName, cob.Date); + + var result = await Collection.DeleteManyAsync(filter, token); + _log.Debug($"Collection=\"{Collection.CollectionNamespace.CollectionName}\" Deleted={result.DeletedCount} Book=\"{book}\" COB=\"{cob:yyyy-MM-dd}\" Layer=\"{layer}\""); + } + + public static Dictionary ConvertBsonToDictionary(BsonDocument doc) + { + var res = doc.ToDictionary(); + + // for map/reduce results + var value = res.TryGetValue("value", out var re) + ? (Dictionary)re + : null; + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var data in res.Where(x => x.Key != "_id" && x.Key != "value")) + { + result[data.Key] = data.Value; + } + + if (value != null) + { + foreach (var data in value) + { + result[data.Key] = data.Value; + } + } + + if (res["_id"] is not Dictionary id) + return result; + + foreach (var data in id) + { + result[data.Key] = data.Value; + } + + return result; + } + + private async IAsyncEnumerable AggregateInternalAsync( + string jsonPipeline, + int maxFetchSize, + [EnumeratorCancellation] CancellationToken token = default + ) + { + var options = new AggregateOptions + { + BypassDocumentValidation = true, + AllowDiskUse = true, + }; + + var pipeline = BsonSerializer.Deserialize(jsonPipeline).Select(p => p.AsBsonDocument).ToList(); + + using var cursor = await Collection.AggregateAsync(pipeline, options, token); + var docs = 0; + + while (await cursor.MoveNextAsync(token)) + { + var batch = cursor.Current; + foreach (var doc in batch) + yield return doc; + + if ( maxFetchSize > 0 && docs++ >= maxFetchSize) + yield break; + } + } + + /// + /// Log method for retry function, called for each iteration + /// + /// + /// + /// + private void LogMethod(int iteration, int maxRetries, Exception e) + { + if (iteration < maxRetries - 1) + _log.Warn($"MongoDB error, retrying RetriesLeft={maxRetries-iteration-1}, Collection=\"{Collection.CollectionNamespace.CollectionName}\"", e); + else + _log.Error($"MongoDB error, retries exhausted, Collection=\"{Collection.CollectionNamespace.CollectionName}\"", e); + } + + /// + /// Delete documents matching the filter + /// + /// Filter selecting a single document + /// + public async Task Delete(FilterDefinition filter, CancellationToken token = default) + { + var result = await Collection.DeleteManyAsync(filter, token); + _log.Debug($"Collection=\"{Collection.CollectionNamespace.CollectionName}\" Deleted={result.DeletedCount} Filter:\n{filter.ToJson(new () {Indent = true})}"); + return result.DeletedCount; + } + + +#endregion private methods +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/MongoDb/RiskGridKeySerializer.cs b/Rms.Risk.Mango.Pivot.Core/MongoDb/RiskGridKeySerializer.cs new file mode 100644 index 0000000..25686b8 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/MongoDb/RiskGridKeySerializer.cs @@ -0,0 +1,59 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace Rms.Risk.Mango.Pivot.Core.MongoDb; + +internal class RiskGridKeySerializer : ClassSerializerBase> +{ + public override Tuple Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonReader = context.Reader; + var bsonType = bsonReader.GetCurrentBsonType(); + + switch (bsonType) + { + case BsonType.String: + { + var s = bsonReader.ReadString(); + if (string.IsNullOrWhiteSpace(s)) + throw CreateCannotBeDeserializedException(); + + var ss = s.Split('_'); + + if (ss.Length < 2) + throw CreateCannotBeDeserializedException(); + + if ( ss.Length > 2 ) // if instrument contained '_' let's re-join it + ss[1] = string.Join( "_", ss.Skip( 1 ) ); + + return Tuple.Create(string.Intern(ss[0]), string.Intern(ss[1])); + } + + default: + throw CreateCannotDeserializeFromBsonTypeException(bsonType); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Tuple value) => + // Format: tenor_instrument + context.Writer.WriteString($"{value.Item1}_{value.Item2}"); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/PivotDefinition.cs b/Rms.Risk.Mango.Pivot.Core/PivotDefinition.cs new file mode 100644 index 0000000..f79aaed --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/PivotDefinition.cs @@ -0,0 +1,319 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Pivot.Core; + +public enum PivotTypeEnum +{ + Unknown = 0, + SimpleAggregation = 1, +// MapReduce = 2, // Obsolete + CustomQuery = 3, + AggregationForHumans = 4 +} + + +[BsonIgnoreExtraElements] +public class PivotDefinition : ICloneable, IComparable, IComparable +{ + public const string UserPivotsGroup = "User pivots"; + public const string PredefinedPivotsGroup = "Predefined"; + public const string CurrentPivotGroup = ""; + public const string CurrentPivotName = ""; + + public static Func /*fieldTypes = null*/, Func /*getAggregationOperator*/, string> ConvertToJson { get; set; } + = (pivot, extraFilter, fieldTypes, getAggOperator) => pivot.DefaultConvertToJson(extraFilter, fieldTypes, getAggOperator); + + public override string ToString() => $"{Name}: {ToJson(null, [], _ => "$sum")}"; + + [BsonIgnore, JsonIgnore] public bool IsPredefined => !string.Equals(Group, UserPivotsGroup); + + object ICloneable.Clone() => Clone(); + + public PivotDefinition Clone() + { + var o = (PivotDefinition)MemberwiseClone(); + + o.KeyFields = (string[])KeyFields.Clone(); + o.DataFields = (string[])DataFields.Clone(); + o.Drilldown = Drilldown + .Select( + x => new DrilldownDef + { + ColumnName = x.ColumnName, + DrilldownCondition = x.DrilldownCondition, + AppendToBeforeGrouping = x.AppendToBeforeGrouping, + DrilldownPivot = x.DrilldownPivot + } ) + .ToList(); + if (LineChartDataSetKeys != null) + o.LineChartDataSetKeys = [..LineChartDataSetKeys]; + + return o; + } + + public string GetFilterExpression(FilterExpressionTree.ExpressionGroup? extraFilter, Dictionary fieldTypes) + { + var filters = new [] + { + Filter.Trim(), + DrilldownFilter.Trim(), + extraFilter?.ToJson(fieldTypes) + } + .Where(x => !string.IsNullOrWhiteSpace(x)) + .OfType() + .ToList(); + + return filters.Count switch + { + 0 => "", + 1 => filters[0], + _ => $"{{ \"$and\" : [ {string.Join(" ", filters)} ] }}" + }; + } + + public string ToJson( + FilterExpressionTree.ExpressionGroup? extraFilter, + Dictionary fieldTypes, + Func getAggregationOperator + ) + => ConvertToJson(this, extraFilter, fieldTypes, getAggregationOperator); + + private string DefaultConvertToJson(FilterExpressionTree.ExpressionGroup? extraFilter, Dictionary fieldTypes, Func? getAggregationOperator = null ) + { + switch ( PivotType ) + { + default: + return ""; + case PivotTypeEnum.SimpleAggregation: + return SimpleAggregationToJson( extraFilter, fieldTypes, getAggregationOperator ); + case PivotTypeEnum.CustomQuery: + return CustomQueryToJson( extraFilter, fieldTypes ); + case PivotTypeEnum.AggregationForHumans: + return AggregationForHumansToJson( extraFilter, fieldTypes ); + } + } + + private string AggregationForHumansToJson(FilterExpressionTree.ExpressionGroup? extraFilter, Dictionary? fieldTypes = null) + { + // It is actually implemented! + // See AfhHelpers.ConvertToJson for details + throw new NotSupportedException(); + } + + private string CustomQueryToJson( FilterExpressionTree.ExpressionGroup? extraFilter, Dictionary fieldTypes ) + { + var keys = string.Join( ",", KeyFields.Select( x => $"\"{x.Replace( ".", " " )}\" : \"${x}\"" ) ); + var preserveKeys = string.Join( ",", KeyFields.Select( x => $"\"{x.Replace( ".", " " )}\" : 1" ) ); + var keysFromIdToRoot = string.Join( ",", KeyFields.Select( x => $"\"{x.Replace( ".", " " )}\" : \"$_id.{x}\"" ) ); + var json = CustomQuery + .Replace( "[KEYS]", keys ) + .Replace( "[KEYS_PRESERVE]", preserveKeys ) + .Replace( "[KEYS_FROM_ID_TO_ROOT]", keysFromIdToRoot ) + .Replace( "[EXTRA_FILTER]", GetFilterExpression(extraFilter, fieldTypes)) + ; + return json; + } + + private string SimpleAggregationToJson(FilterExpressionTree.ExpressionGroup? extraFilter, Dictionary fieldTypes, Func? getAggregationOperator ) + { + getAggregationOperator ??= _ => "$sum"; + + var keys = string.Join( ",", KeyFields.Select( x => $"\"{x.Replace( ".", " " )}\" : \"${x}\"" ) ); + var data = string.Join( + ",", + DataFields.Select( x => $"\"{x.Replace( ".", " " )}\" : {{ \"{getAggregationOperator( x )}\" : \"${x}\" }}" ) ); + + var group = + $"{{ $group : {{ _id : {{ {keys} }}," + + data + + (string.IsNullOrWhiteSpace( WithinGrouping ) ? "\n" : $",\n{WithinGrouping}\n") + + "}},\n"; + + var filter = GetFilterExpression(extraFilter, fieldTypes); + var match = ""; + if (!string.IsNullOrWhiteSpace(filter)) + match = $"{{ $match : {filter} }},\n"; + + var bg = ""; + if ( !string.IsNullOrWhiteSpace( BeforeGrouping ) ) + bg = BeforeGrouping + ",\n"; + + var ag = ""; + if ( !string.IsNullOrWhiteSpace( AfterGrouping ) ) + ag = AfterGrouping + ",\n"; + + + var json = "[\n" + + match + + bg + + group + + ag + + "{ $sort : { _id : 1 } }\n" + + "]"; + + return FormatArray( json ); + } + + private static string FormatArray(string s) + { + try + { + var doc = MongoDB.Bson.Serialization.BsonSerializer.Deserialize(s); + return doc.ToJson( new() {Indent = true, OutputMode = JsonOutputMode.RelaxedExtendedJson} ); + } + catch + { + return s; + } + } + + [Obsolete] + public bool IsMapReduce { get; set; } + + [JsonConverter( typeof(StringEnumConverter))] + [BsonRepresentation( BsonType.String)] + public PivotTypeEnum PivotType { get; set; } + + public string Name { get; set; } = "New pivot"; + public string Group { get; set; } = UserPivotsGroup; + public bool UserVisible { get; set; } = true; + + public string Owner { get; set; } = string.Empty; + + // aggregation + + public string[] KeyFields { get; set; } = []; + public string[] DataFields { get; set; } = []; + public string Filter { get; set; } = ""; + public string DrilldownFilter { get; set; } = ""; // This should never be saved in metadata! + public string BeforeGrouping { get; set; } = ""; + public string WithinGrouping { get; set; } = ""; + public string AfterGrouping { get; set; } = ""; + + // map/reduce + public string MapFunction { get; set; } = ""; + public string ReduceFunction { get; set; } = ""; + public string PostProcessFunction { get; set; } = ""; + + // custom query + public string CustomQuery { get; set; } = ""; + + [BsonIgnoreExtraElements] + public class DrilldownDef + { + /// + /// Column to drill down to or "" for default condition. + /// + public string ColumnName { get; set; } = ""; + /// + /// Drilldown formulas. Each formula must end with comma! + /// + /// + /// Key is column name or empty string for default condition. + /// The value is comma-separated list of conditions. + /// Example: + /// { "RhoDetails.OpeningRho.AUD_CROSSCURRENCY.Data.3M" : { "$ne": 0.0 } }, + /// You can use "variables": + /// "<column_name>" - replace this with column value + /// "<COLNAME>" - replace this with column name + /// Example: + /// { "RhoDetails.<COLNAME>.<CCY>_<Curve>.Data.<Tenor>" : { "$ne": 0.0 } }, + /// + public string DrilldownCondition { get; set; } = ""; + /// + /// Append this to "Before Grouping" part of the query. You can use all "addFields" contents here. + /// + public string AppendToBeforeGrouping { get; set; } = ""; + /// + /// Use this pivot def as drilldown report (conditions will be applied) + /// + public string DrilldownPivot { get; set; } = ""; + } + + public List Drilldown { get; set; } = []; + + // options + + public Highlighting Highlighting { get; set; } = new(); + public bool AllowDiskUsage { get; set; } + + public List ColumnsOrder { get; set; } = []; + + [BsonIgnore] + public string ColumnsOrderText + { + get => string.Join("\n", ColumnsOrder); + set + { + ColumnsOrder.Clear(); + ColumnsOrder.AddRange(value?.Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace( x )) ?? Array.Empty()); + } + } + + /// + /// Column renaming map. Key is a real column name / value is display name. + /// + public Dictionary RenameColumn { get; set; } = []; + + public bool Make2DPivot { get; set; } + public List Pivot2DRows { get; set; } = []; + public string Pivot2DColumn { get; set; } = ""; + public string Pivot2DData { get; set; } = ""; + public string Pivot2DDataTypeColumn { get; set; } = ""; + + public bool MakeLineChart { get; set; } + public string? LineChartXAxis { get; set; } + public List? LineChartDataSetKeys { get; set; } + public List? LineChartYAxis { get; set; } + public bool LineChartShowLegend { get; set; } + public bool LineChartSteppedLine { get; set; } + public bool LineChartFill { get; set; } + + public bool ShowTotals { get; set; } = true; + public double HighlightTopPercent { get; set; } = 10.0; + + int IComparable.CompareTo(object? obj) => CompareTo(obj as PivotDefinition); + + public int CompareTo(PivotDefinition? other) + { + if (ReferenceEquals(this, other)) + return 0; + if (ReferenceEquals(null, other)) + return 1; + var groupComparison = string.Compare(Group, other.Group, StringComparison.Ordinal); + if (groupComparison != 0) + return groupComparison; + + return string.Compare(Name, other.Name, StringComparison.Ordinal); + } +} + +[BsonIgnoreExtraElements] +public class PivotDefinitions +{ + [BsonId] public string Id { get; set; } = ""; + public List Pivots { get; set; } = []; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/PivotedDataHelpers.cs b/Rms.Risk.Mango.Pivot.Core/PivotedDataHelpers.cs new file mode 100644 index 0000000..b9fd30f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/PivotedDataHelpers.cs @@ -0,0 +1,120 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text; + +namespace Rms.Risk.Mango.Pivot.Core; + +public static class PivotedDataHelpers +{ + public static string CopyToCsv(this IPivotedData data) + { + var csv = new StringBuilder(1024); + + // ordered headers + var headers = data.Headers; + + // write header + csv.AppendLine(string.Join(",", headers)); + + var sb = new StringBuilder(1024); + for (var i = 0; i < data.Count; i++) + { + sb.Clear(); + for (var j = 0; j < headers.Count; j++) + { + if (j > 0) + sb.Append(','); + sb.Append(ShieldElement(data.Get(j, i) ?? "")); + } + + csv.AppendLine(sb.ToString()); + } + + return csv.ToString(); + } + + public static int WriteToCsv( this IPivotedData data, string fileName, bool writeHeader=true, bool append = false, Dictionary? formats = null ) + { + using var writer = new StreamWriter( fileName, append ); + var count = WriteToCsv(data, writer, writeHeader, formats); + writer.Close(); + + return count; + } + + public static int WriteToCsv(this IPivotedData data, TextWriter writer, bool writeHeader = true, Dictionary? formats = null) + { + var count = 0; + // ordered headers + var headers = data.Headers; + + var colPositions = data.GetColumnPositions(); + var positionFormats = new string[headers.Count]; + if (formats != null) + { + foreach (var formattedColumn in formats.Keys) + { + if (colPositions.TryGetValue(formattedColumn, out var pos)) + positionFormats[pos] = formats[formattedColumn]; + } + } + + // write header + if (writeHeader) + writer.WriteLine( string.Join( ",", headers ) ); + + var sb = new StringBuilder( 1024 ); + for ( var i = 0; i < data.Count; i++ ) + { + sb.Clear(); + for ( var j = 0; j < headers.Count; j++ ) + { + if ( j > 0 ) + sb.Append( ',' ); + sb.Append( ShieldElement( data.Get(j, i) ?? "", positionFormats[j] ) ); + } + + writer.WriteLine( sb.ToString() ); + count += 1; + } + + return count; + } + + private static string ShieldElement(object e, string? format = null) + { + switch ( e ) + { + case double d: + return d.ToString(format); + + case int: + case long: + case bool: + return e.ToString() ?? ""; + +// case DateTime d: +// return "\"" + d.ToLocalTime() + "\""; + + default: + return "\"" + e + "\""; + } + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Properties/AssemblyInfo.cs b/Rms.Risk.Mango.Pivot.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d3fd313 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Rms.Risk.Mango.Pivot.Core")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e44e7aa0-5a80-42f8-9609-be4c366e08fd")] + diff --git a/Rms.Risk.Mango.Pivot.Core/RetryHelper.cs b/Rms.Risk.Mango.Pivot.Core/RetryHelper.cs new file mode 100644 index 0000000..e9b980f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/RetryHelper.cs @@ -0,0 +1,182 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using System.Reflection; + +namespace Rms.Risk.Mango.Pivot.Core; + +/// +/// Utility class used to run a method using a retry strategy +/// IF the method throws, the utility waits for a period of time and reruns it again, for a specific max number of times +/// +internal static class RetryHelper +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public static void DoWithRetries( Action doWhat, int retries, TimeSpan delay, Action? logFunc = null) + { + logFunc ??= DefaultLogFunc; + + Exception? firstException = null; + + for ( var i = 0; i < retries; i++ ) + { + try + { + doWhat(); + return; + } + catch ( Exception e ) + { + logFunc(i, retries, e); + + firstException ??= e; + } + + if ( i == retries - 1 ) + break; // If this is the last iteration, don't wait + + Thread.Sleep( delay ); + } + + throw new ApplicationException($"{firstException?.Message} (after {retries} retries)", firstException); + } + + public static T DoWithRetries(Func doWhat, int retries, TimeSpan delay, Action? resetFunc = null, Action? logFunc = null) + { + logFunc ??= DefaultLogFunc; + + Exception? firstException = null; + + for (var i = 0; i < retries; i++) + { + try + { + return doWhat(); + } + catch (Exception e) + { + logFunc(i, retries, e); + + firstException ??= e; + } + + resetFunc?.Invoke(); + + if ( i == retries - 1 ) + break; // If this is the last iteration, don't wait + + Thread.Sleep(delay); + } + + throw new ApplicationException($"{firstException?.Message} (after {retries} retries)", firstException); + } + + public static async Task DoWithRetriesAsync( + Func> doAsyncWhat, + int retries, + TimeSpan delay, + Action? resetFunc = null, + Action? logFunc = null, + CancellationToken token = default) + { + logFunc ??= DefaultLogFunc; + + Exception? firstException = null; + + for (var i = 0; i < retries; i++) + { + try + { + return await doAsyncWhat(); + } + catch (Exception e) + { + logFunc(i, retries, e); + + firstException ??= e; + } + + resetFunc?.Invoke(); + await Task.Delay(delay, token); + } + + if (firstException != null) + throw firstException; + throw new ApplicationException("Retries exhausted"); + } + + public static async Task DoWithRetriesAsync( + Func doAsyncWhat, + int retries, + TimeSpan delay, + Action? resetFunc = null, + Action? logFunc = null, + string? exceptionSubstring = null, + CancellationToken token = default) + + { + logFunc ??= DefaultLogFunc; + + Exception? firstException = null; + + for (var i = 0; i < retries; i++) + { + try + { + await doAsyncWhat(); + return; + } + catch (Exception e) + { + if ( exceptionSubstring != null && !e.Message.Contains(exceptionSubstring, StringComparison.OrdinalIgnoreCase) ) + { + _log.Warn($"Exception does not contain substring {exceptionSubstring} Not retrying. {e.Message}"); + throw; + } + + logFunc(i, retries, e); + + firstException ??= e; + } + + resetFunc?.Invoke(); + await Task.Delay(delay, token); + } + + if (firstException != null) + throw firstException; + } + + /// + /// This is the default logger callback that will log failed attempts, its recommended that the user supplies their own so + /// that they can log more contextual information about the operation + /// + /// + /// + /// + private static void DefaultLogFunc(int iteration, int maxRetries, Exception e) + { + if (iteration < maxRetries - 1) + _log.Warn($"Call failed, retrying RetriesLeft={maxRetries - iteration - 1}", e); + else + _log.Error("Call failed, retries exhausted", e); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.Core/Rms.Risk.Mango.Pivot.Core.csproj b/Rms.Risk.Mango.Pivot.Core/Rms.Risk.Mango.Pivot.Core.csproj new file mode 100644 index 0000000..3872c2f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.Core/Rms.Risk.Mango.Pivot.Core.csproj @@ -0,0 +1,14 @@ + + + + library + Rms.Risk.Mango.Pivot.Core + + + + + + + + + diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/IMinMaxCache.cs b/Rms.Risk.Mango.Pivot.UI/Controls/IMinMaxCache.cs new file mode 100644 index 0000000..a841624 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/IMinMaxCache.cs @@ -0,0 +1,36 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.UI.Controls; + +public class MinMax +{ + public double MinValue { get; set; } = double.MaxValue; + public double MaxValue { get; set; } = double.MinValue; + public double AbsMinValue { get; set; } = double.MaxValue; + public double AbsMaxValue { get; set; } = double.MinValue; + public double Total { get; set; } + public double AbsTotal { get; set; } + + public override string ToString() => $"{MinValue:N0} {MaxValue:N0} {Total:N0} {AbsTotal:N0}"; +} + +public interface IMinMaxCache +{ + MinMax? TryGet( string columnName ); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxCheckBoxesComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxCheckBoxesComponent.razor new file mode 100644 index 0000000..65820eb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxCheckBoxesComponent.razor @@ -0,0 +1,89 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +

@((MarkupString)Text)

+ +
+ @if (Info != null) + { +
+ @for ( var index = 0; index < Info.Count; index++) + { + var i = index; + +
+
+ + +
+
+ } +
+ } + + +
+ + + +@code +{ + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + public class BoolNameValue + { + public string Name { get; set; } = ""; + public bool Value { get; set; } + } + + [Parameter] public bool ShowCancel { get; set; } + [Parameter] public string Text { get; set; } = ""; + [Parameter] public List? Info { get; set; } + [Parameter] public string OkButtonName { get; set; } = "OK"; + + + private async Task OnOK() + { + await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxExceptionComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxExceptionComponent.razor new file mode 100644 index 0000000..9a0113c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxExceptionComponent.razor @@ -0,0 +1,62 @@ +@inject IJSRuntime JsRuntime + + +@if (!string.IsNullOrWhiteSpace(Message)) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +

@((MarkupString)Message)

+} +
+ + + + + +
+ +@code +{ + [CascadingParameter] public BlazoredModalInstance BlazoredModal { get; set; } = null!; + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public Exception Exception { get; set; } = new(); + [Parameter] public string Message { get; set; } = ""; + + private async Task OnOK() + { + await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + } + + private async Task OnCopy() + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", Exception.ToString()); + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", "Copied to the clipboard."); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxJsonComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxJsonComponent.razor new file mode 100644 index 0000000..df7230a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxJsonComponent.razor @@ -0,0 +1,92 @@ +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+

View or edit Json document

+
+ + + +
+
+ +@code +{ + public class ResultType + { + public bool ShouldUpdate { get; init; } + public string Json { get; init; } = ""; + } + + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Text { get; set; } = ""; + [Parameter] public bool EnableWrite { get; set; } + + private async Task OnUpdate() + { + if (!EnableWrite) + await BlazoredModal.CancelAsync(); + else + await BlazoredModal.CloseAsync(ModalResult.Ok(new ResultType { ShouldUpdate = true, Json = Text })); + } + + private async Task OnDelete() + { + if (!EnableWrite) + await BlazoredModal.CancelAsync(); + else + await BlazoredModal.CloseAsync(ModalResult.Ok(new ResultType { ShouldUpdate = false, Json = Text })); + } + + private async Task OnCopy() + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", Text); + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", "Copied to the clipboard."); + + } + private async Task OnPaste() + { + if (!EnableWrite) + return; + Text = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + StateHasChanged(); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxKeyValueComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxKeyValueComponent.razor new file mode 100644 index 0000000..10a424a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxKeyValueComponent.razor @@ -0,0 +1,150 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

@((MarkupString)Text)

+ +@if (ShowInputText) +{ +
+ + +
+} + +
+ @if (_infoData != null) + { + + + + + } +
+ + + +@code +{ + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + [Parameter] + public IEnumerable>? Info + { + get; + set + { + if (value == null) + { + field = null; + _infoData = null; + return; + } + + field = value.ToList(); + _infoData = field.Select(x => new KeyValue(x.Key, x.Value)).ToList(); + } + } + + [Parameter] public bool ShowCancel { get; set; } + [Parameter] public bool ShowInputText { get; set; } + [Parameter] public bool IsInputTextMandatory { get; set; } + [Parameter] public string Text { get; set; } = ""; + [Parameter] public string OkButtonName { get; set; } = "OK"; + [Parameter] public string CancelButtonName { get; set; } = "Cancel"; + [Parameter] public string? InputTextLabel { get; set; } + + [Parameter] + public string InputText + { + get; + set + { + if (field == value) + return; + field = value; + InputTextChanged.InvokeAsync(field); + } + } = ""; + + [Parameter] public EventCallback InputTextChanged { get; set; } + + private string InputClass => string.IsNullOrWhiteSpace(InputText) + ? "red-border" + : ""; + + // ReSharper disable NotAccessedPositionalProperty.Local + private record KeyValue(string Key, string Value); + // ReSharper restore NotAccessedPositionalProperty.Local + + private List? _infoData; + + private Task OnOK() + { + if (ShowInputText) + { + if (IsInputTextMandatory && string.IsNullOrWhiteSpace(InputText)) + { + return InvokeAsync(StateHasChanged); + } + return BlazoredModal.CloseAsync(ModalResult.Ok(InputText)); + } + else + return BlazoredModal.CloseAsync(ModalResult.Ok(true)); + } + + private static string Truncate(string? value, int maxLength, string truncationSuffix = "…") => + value?.Length > maxLength + ? value[..(maxLength - truncationSuffix.Length)] + truncationSuffix + : value!; + + + private async Task OnCommentsFocusOut() + { + if (!ShowInputText) + return; + + await InvokeAsync(StateHasChanged); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxProgressComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxProgressComponent.razor new file mode 100644 index 0000000..91aa8b7 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxProgressComponent.razor @@ -0,0 +1,103 @@ +@using System.Collections.Concurrent + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+
+
+        @foreach (var m in _messages)
+        {
+            @m
+ } +
+
+ +
+ +@code +{ + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + [Parameter] public Func Action { get; set; } = _ => Task.CompletedTask; + [Parameter] public string TextEditorClass { get; set; } = "text-area"; + + private readonly ConcurrentQueue _messages = []; + private bool _inProgress; + + private Task OnOK() => BlazoredModal.CloseAsync(ModalResult.Ok(true)); + + private void OnProgress(string message, bool isError = false, object? extraInfo = null) + { + foreach( var m in message.Split( "\n" )) + _messages.Enqueue( m ); + InvokeAsync(StateHasChanged); + } + + private async Task DoAction() + { + _inProgress = true; + await InvokeAsync(StateHasChanged); + try + { + await Action(OnProgress); + _messages.Enqueue("----------------------------- COMPLETE ----------------------------- "); + + _inProgress = false; + await InvokeAsync(StateHasChanged); + } + catch ( Exception ex) + { + var text = ex.ToString().Split( '\n' ); + _messages.Enqueue("----------------------------- ERROR ----------------------------- "); + foreach (var s in text ) + { + _messages.Enqueue( s ); + } + + _inProgress = false; + await InvokeAsync(StateHasChanged); + } + } + + protected override async Task OnAfterRenderAsync( bool firstRender ) + { + if (!firstRender) + return; + + _inProgress = true; + await InvokeAsync(StateHasChanged); + _ = Task.Run(DoAction); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxTextComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxTextComponent.razor new file mode 100644 index 0000000..a1c11c3 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MessageBoxTextComponent.razor @@ -0,0 +1,73 @@ +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +@if (!string.IsNullOrWhiteSpace(Message)) +{ +

@((MarkupString)Message)

+} +
+ + + +
+ +@code +{ + [CascadingParameter] public BlazoredModalInstance BlazoredModal { get; set; } = null!; + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public bool ShowCancel { get; set; } + [Parameter] public string Text { get; set; } = ""; + [Parameter] public string Message { get; set; } = ""; + [Parameter] public string OkButtonName { get; set; } = "OK"; + [Parameter] public string MimeType { get; set; } = "text/plain"; + [Parameter] public string TextEditorClass { get; set; } = "text-area"; + + private async Task OnOK() + { + await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + } + + private async Task OnCopy() + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", Text); + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", "Copied to the clipboard."); + + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MinMaxCache.cs b/Rms.Risk.Mango.Pivot.UI/Controls/MinMaxCache.cs new file mode 100644 index 0000000..02f6fe9 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MinMaxCache.cs @@ -0,0 +1,112 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; +using Rms.Risk.Mango.Pivot.Core; + +namespace Rms.Risk.Mango.Pivot.UI.Controls; + +internal class MinMaxCache : IMinMaxCache +{ + private readonly ConcurrentDictionary _minMaxCache = new(); + + public void Clear() => _minMaxCache.Clear(); + + public static bool IsNumeric(object? value) => value is double or int or long or uint or ulong; + + public void Update(IPivotedData data, IReadOnlyCollection rows) + { + Clear(); + + if ( data == null ) + return; + + foreach ( var (columnName, columnPosition) in data.GetColumnPositions() ) + { + if (string.IsNullOrWhiteSpace(columnName)) + continue; + + // works for double columns only + + var r = new MinMax(); + var found = false; + foreach ( var row in rows ) + { + found |= UpdateOne(data, columnPosition, row, r); + } + + if ( found ) + _minMaxCache[columnName] = r; + } + } + + public void Update(IPivotedData data) + { + Clear(); + + if ( data == null ) + return; + + foreach ( var (columnName, columnPosition) in data.GetColumnPositions() ) + { + if (string.IsNullOrWhiteSpace(columnName)) + continue; + + // works for double columns only + + var r = new MinMax(); + var found = false; + for( var row = 0; row < data.Count; row++ ) + { + found |= UpdateOne(data, columnPosition, row, r); + } + + if ( found ) + _minMaxCache[columnName] = r; + } + } + + private static bool UpdateOne(IPivotedData data, int col, int row, MinMax r) + { + var val = data.Get( col, row ); + if ( !IsNumeric(val) ) + return false; + + var d = Convert.ToDouble(val); + var absD = Math.Abs( d ); + + if (r.MinValue > d) + r.MinValue = d; + if (r.MaxValue < d) + r.MaxValue = d; + + if (r.AbsMinValue > absD) + r.AbsMinValue = absD; + if (r.AbsMaxValue < absD) + r.AbsMaxValue = absD; + + + r.Total += d; + r.AbsTotal += Math.Abs( d ); + + return true; + } + + public MinMax? TryGet( string columnName ) => + _minMaxCache.GetValueOrDefault(columnName); +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/ModalDialogUtils.cs b/Rms.Risk.Mango.Pivot.UI/Controls/ModalDialogUtils.cs new file mode 100644 index 0000000..27214c3 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/ModalDialogUtils.cs @@ -0,0 +1,276 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Blazored.Modal; +using Blazored.Modal.Services; +using Rms.Risk.Mango.Pivot.UI.Services; + +namespace Rms.Risk.Mango.Pivot.UI.Controls; + +public static class ModalDialogUtils +{ + private static ILogger? _logger; + + public static void SetLogger(ILogger logger) => _logger = logger; + + /// + /// Display a user information dialog box, allows the user to click ok or cancel + /// + public static async Task ShowConfirmationDialog( + IModalService service, + string header, + string message, + Dictionary? info = null, + Dictionary? additionalParams = null, + string? inputPrompt = null, + bool isInputTextMandatory = false + ) + { + var parameters = new ModalParameters { { "Text", message } }; + if (info != null) + parameters.Add("Info", info); + parameters.Add("ShowCancel", true); + + if (!string.IsNullOrEmpty(inputPrompt)) + { + parameters.Add("ShowInputText", true); + parameters.Add("InputTextLabel", inputPrompt); + parameters.Add("IsInputTextMandatory", isInputTextMandatory); + } + + if (additionalParams != null) + { + foreach (KeyValuePair parameter in additionalParams) + { + parameters.Add(parameter.Key, parameter.Value); + } + } + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + _logger?.LogInformation($"Confirmation: {header} - {message}"); + var form = service.Show(header, parameters, options); + return await form.Result; + } + + /// + /// Display a user information dialog box with checkboxes + /// + /// + /// + /// + /// + /// + public static async Task ShowConfirmationDialog(IModalService service, string header, string message, Dictionary info) + { + var parameters = new ModalParameters { { "Text", message } }; + var checkboxes = info + .Select(x => new MessageBoxCheckBoxesComponent.BoolNameValue { Name = x.Key, Value = x.Value }) + .ToList(); + parameters.Add("Info", checkboxes); + parameters.Add("ShowCancel", true); + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + _logger?.LogInformation($"Confirmation: {header} - {message}"); + var form = service.Show(header, parameters, options); + var res = await form.Result; + + if ( res.Confirmed ) + { + foreach ( var boolNameValue in checkboxes ) + { + info[boolNameValue.Name] = boolNameValue.Value; + } + } + + return res; + } + + /// + /// Display a query dialog box, allows the user to enter some text and click ok or cancel + /// + /// + /// + /// + /// + /// + /// + /// Input text or null if cancelled + public static async Task ShowConfirmationDialogWithInput( + IModalService service, + string header, + string message, + string? inputLabel = null, + string? initialInput = null, + Dictionary? info = null) + { + var parameters = new ModalParameters + { + { nameof(MessageBoxKeyValueComponent.Text), message }, + { nameof(MessageBoxKeyValueComponent.ShowInputText), true } + }; + if (info != null) + parameters.Add(nameof(MessageBoxKeyValueComponent.Info), info); + if (initialInput != null) + parameters.Add(nameof(MessageBoxKeyValueComponent.InputText), initialInput); + if (inputLabel != null) + parameters.Add(nameof(MessageBoxKeyValueComponent.InputTextLabel), inputLabel); + parameters.Add(nameof( MessageBoxKeyValueComponent.ShowCancel), true); + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + _logger?.LogInformation($"Confirmation: {header} - {message}"); + var form = service.Show(header, parameters, options); + var res = await form.Result; + return res.Cancelled ? null : res.Data?.ToString(); + } + + /// + /// Display a user information dialog box, just shows the ok button + /// + /// + /// + /// + /// + /// + public static async Task ShowInfoDialog(IModalService service, string header, string message, Dictionary? info = null) + { + var parameters = new ModalParameters { { "Text", message } }; + if (info != null) + parameters.Add("Info", info); + parameters.Add("ShowCancel", false); + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + _logger?.LogInformation($"Info: {header} - {message}"); + var form = service.Show(header, parameters, options); + await form.Result; + } + + + /// + /// Display a user information dialog box, just shows the ok button + /// + /// + /// + /// + /// + /// + /// + public static async Task ShowTextDialog(IModalService service, string header, string text, string? message = null, string mimeType="application/json") + { + var parameters = new ModalParameters { { "ShowCancel", false } }; + if ( !string.IsNullOrWhiteSpace(text)) + parameters.Add("Text" , text); + if ( !string.IsNullOrWhiteSpace(message)) + parameters.Add("Message" , message); + if ( !string.IsNullOrWhiteSpace(mimeType)) + parameters.Add("MimeType", mimeType); + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + var form = service.Show(header, parameters, options); + await form.Result; + } + + /// + /// Show a model error dialog when an exception was thrown, just shows an ok button + /// + /// + /// + /// + /// + public static async Task ShowExceptionDialog(IModalService service, string header, Exception e) + { + var parameters = new ModalParameters + { + { "Message", header }, + { "Exception", e } + }; + + var options = new ModalOptions + { + HideCloseButton = false, + DisableBackgroundCancel = false + }; + + _logger?.LogError(e, $"{header} - {e.Message}"); + var form = service.Show(header, parameters, options); + await form.Result; + } + + /// + /// Make async call UI-safe. Shows dialog if exception happen + /// + /// + /// + /// + /// + public static async Task SafeCall(IModalService service, string header, Func action) + { + try + { + await action(); + } + catch (Exception ex) + { + await ShowExceptionDialog(service, header, ex); + } + } + + /// + /// Make UI-safe async call and show its progress in the modal dialog. + /// + public static async Task ShowProgress(IModalService service, string header, Func action) + { + var parameters = new ModalParameters + { + { "Action", action }, + }; + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + var form = service.Show(header, parameters, options); + await form.Result; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/MultiSelectCheckboxDropdown.razor b/Rms.Risk.Mango.Pivot.UI/Controls/MultiSelectCheckboxDropdown.razor new file mode 100644 index 0000000..c660ed9 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/MultiSelectCheckboxDropdown.razor @@ -0,0 +1,277 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code +{ + [Parameter] public string Name { get; set; } = "Select"; + [Parameter] public string DenoteEmptyString { get; set; } = "(Blank)"; + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool ShowSelectionText { get; set; } + + [Parameter] + public List DropdownValues + { + get; + set + { + value ??= []; + if (field.SequenceEqual(value)) + return; + + field = value; + SetValues(BlankedDropdownValues); + UpdateAllSelected(); + InvokeAsync(StateHasChanged); + } + } = []; + + [Parameter] public EventCallback> SelectionChanged { get; set; } + [Parameter] public bool EnableDropdownRefresh { get; set; } + [Parameter] public bool EnableDropdownMenu { get; set; } = true; + [Parameter] public bool Filterable { get; set; } + [Parameter] public bool ResetNow { get; set; } + [Parameter] public bool SelectAllByDefault { get; set; } = true; + + [Parameter] + public string FilterString + { + get; + set + { + field = value; + _filterList = field.Split(",").Select(v => v.Trim()).Where(v => !string.IsNullOrWhiteSpace(v)).ToList(); + RefreshFilter(); + } + } = string.Empty; + + private bool _allSelected = true; + private List _dropdownValuesList = []; + private HashSet _dropdownValuesSet = []; + private HashSet _selectedValues = []; + private HashSet _selectedValuesBackup = []; + private List _filterList = []; + private List _filteredDropdown = []; + + IEnumerable BlankedDropdownValues + => DropdownValues?.Select(x => string.IsNullOrWhiteSpace(x) ? DenoteEmptyString : x) ?? Array.Empty(); + + [Parameter] public HashSet SelectedValues + { + get => _selectedValues; + set + { + if (value == null || ReferenceEquals(_selectedValues, value) || _selectedValues.SequenceEqual(value)) + return; + _selectedValues = value; + SelectedValuesChanged.InvokeAsync(_selectedValues); + } + } + + [Parameter] public EventCallback> SelectedValuesChanged { get; set; } + + + private void RefreshFilter() + { + _filteredDropdown.Clear(); + if (_filterList.Count == 0) + _filteredDropdown.AddRange(BlankedDropdownValues); + else + _filteredDropdown.AddRange( + BlankedDropdownValues.Where(x => + _filterList.Any(v => x.Contains(v, StringComparison.OrdinalIgnoreCase)))); + } + + private void SetValues(IEnumerable values) + { + _dropdownValuesList.Clear(); + _dropdownValuesList.AddRange(values); + _dropdownValuesSet.Clear(); + _dropdownValuesSet.UnionWith(_dropdownValuesList); + } + + private void ResetMenu(IEnumerable values) + { + SetValues(values); + _selectedValues .Clear(); + _selectedValues .UnionWith(_dropdownValuesSet); + _allSelected = true; + + SelectedValuesChanged.InvokeAsync(_selectedValues); + } + + protected override void OnInitialized() + { + if (SelectAllByDefault) + ResetMenu(BlankedDropdownValues); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender) + return; + + if (!SelectAllByDefault) + { + SetValues(BlankedDropdownValues); + + if ( UpdateAllSelected() ) + await InvokeAsync(StateHasChanged); + } + } + + private bool UpdateAllSelected() + { + var sel = _selectedValues.SetEquals(_dropdownValuesSet); + if (_allSelected != sel) + { + _allSelected = sel; + return true; + } + return false; + } + + protected override void OnParametersSet() + { + if (ResetNow) + { + ResetMenu(BlankedDropdownValues); + FilterString = string.Empty; + } + else if (EnableDropdownRefresh) + { + var newDropdownValues = new HashSet(BlankedDropdownValues); + if (!newDropdownValues.SetEquals(_dropdownValuesSet)) + ResetMenu(BlankedDropdownValues); + } + RefreshFilter(); + } + + public async Task AllClicked() + { + _allSelected = !_allSelected; + _selectedValues.Clear(); + if (_allSelected) + _selectedValues.UnionWith(_dropdownValuesSet); + + await SelectedValuesChanged.InvokeAsync(_selectedValues); + await InvokeAsync(StateHasChanged); + } + + public void ItemCheckboxClicked(string item, object? checkedValue) + { + if (checkedValue is true) + _selectedValues.Add(item); + else + _selectedValues.Remove(item); + _allSelected = _selectedValues.SetEquals(_dropdownValuesSet); + + SelectedValuesChanged.InvokeAsync(_selectedValues); + } + + public void CancelClicked() + { + _selectedValues.Clear(); + _selectedValues.UnionWith(_selectedValuesBackup); + _allSelected = _selectedValues.SetEquals(_dropdownValuesSet); + + SelectedValuesChanged.InvokeAsync(_selectedValues); + } + + public Task ApplyClicked() + { + _selectedValuesBackup.Clear(); + _selectedValuesBackup.UnionWith(_selectedValues); + return SelectionChanged.InvokeAsync(_selectedValues.Select(x => x == DenoteEmptyString ? "" : x) + .ToList()); + } + + private void ToggleFilterable() + { + + Filterable = !Filterable; + FilterString = string.Empty; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/ProgressSpinner.razor b/Rms.Risk.Mango.Pivot.UI/Controls/ProgressSpinner.razor new file mode 100644 index 0000000..68523d8 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/ProgressSpinner.razor @@ -0,0 +1,35 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ @if (ChildContent != null) + { + @ChildContent + } + else + { + Loading... + } +
+ +@code +{ + [Parameter] + public RenderFragment? ChildContent { get; set; } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/SplitPanel.razor b/Rms.Risk.Mango.Pivot.UI/Controls/SplitPanel.razor new file mode 100644 index 0000000..ecfd793 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/SplitPanel.razor @@ -0,0 +1,151 @@ +@inject IJSRuntime JS + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ @First +
+
+
+ @Second +
+
+ +@code { + [Parameter] public RenderFragment? First { get; set; } + [Parameter] public RenderFragment? Second { get; set; } + [Parameter] public string Orientation { get; set; } = "horizontal"; // "horizontal" or "vertical" + [Parameter] public double InitialSplit { get; set; } = 0.5; // 0.0 - 1.0 + [Parameter] public int SplitterWidthPx { get; set; } = 3; + + private string _id = $"S{Random.Shared.Next():08X}"; + + private double _split = 0.5; + private bool _isDragging = false; + private double _panelSize = 0; + private double _startPos = 0; + private double _startSplit = 0; + + protected override void OnInitialized() + { + _split = InitialSplit; + } + + private string FirstPaneStyle => + Orientation == "vertical" + ? $"height: {_split * 100}%;" + : $"width: {_split * 100}%;"; + private string SecondPaneStyle => + Orientation == "vertical" + ? $"height: {(1 - _split) * 100}%;" + : $"width: {(1 - _split) * 100}%;"; + private string SplitterStyle => + Orientation == "vertical" + ? $"height: {SplitterWidthPx}px; cursor: ns-resize;" + : $"width: {SplitterWidthPx}px; cursor: ew-resize;"; + + private async void OnMouseDown(MouseEventArgs e) + { + _isDragging = true; + _startSplit = _split; + if (Orientation == "vertical") + { + _panelSize = await GetPanelSizeAsync("clientHeight"); + _startPos = e.ClientY; + } + else + { + _panelSize = await GetPanelSizeAsync("clientWidth"); + _startPos = e.ClientX; + } + } + + private void OnMouseMove(MouseEventArgs e) + { + if (!_isDragging) return; + double delta = 0; + if (Orientation == "vertical") + delta = (e.ClientY - _startPos) / (_panelSize == 0 ? 1 : _panelSize); + else + delta = (e.ClientX - _startPos) / (_panelSize == 0 ? 1 : _panelSize); + + _split = Math.Clamp(_startSplit + delta, 0.01, 0.99); + StateHasChanged(); + } + + private void OnMouseUp(MouseEventArgs e) + { + _isDragging = false; + } + + private async Task GetPanelSizeAsync(string property) + { + // Inline JS: get the property directly from the element + return await JS.InvokeAsync( + "eval", + $"(function(element){{return element.{property} || 0;}})(document.getElementById('{_id}'))" + ); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TabControl.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TabControl.razor new file mode 100644 index 0000000..49ecf2e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TabControl.razor @@ -0,0 +1,142 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+ @if (ShowHeaders) + { +
+ +
+ } + + @ChildContent +
+
+ +@code { + // Next line is needed so we are able to add components inside + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string TabGroupClass { get; set; } = ""; + [Parameter] public bool ShowHeaders { get; set; } = true; + [Parameter] public bool Vertical { get; set; } + [Parameter] public bool PersistAllTabs { get; set; } + + [Parameter] public string ActivePage + { + get => _activePage?.Text ?? ""; + set + { + if (value == null) + return; + + if (_activePage?.Text == value) + return; + + var page = _pages.FirstOrDefault(x => x.Text == value); + if ( page?.IsSelectable != true) + return; + + _activePage = page; + ActivePageChanged.InvokeAsync(_activePage?.Text); + } + } + + [Parameter] public EventCallback ActivePageChanged { get; set; } + + private List _pages = []; + private TabPage? _activePage; + + private string TabControlClass => $"{Class}"; + private string TabPanelClass => Vertical + ? "nav nav-tabs flex-column" + : "nav nav-tabs"; + protected override void OnInitialized() + { + base.OnInitialized(); + _pages = []; + } + + private int IndexOf(TabPage tabPage) + { + for( var i = 0; i < _pages.Count; i+=1 ) + { + if (_pages[i].Text == tabPage.Text) + return i; + } + return -1; + } + + internal void AddPage(TabPage tabPage) + { + if (string.IsNullOrWhiteSpace(tabPage?.Text)) + throw new ApplicationException("TabPage must have Text field filled in"); + + var oldPage = IndexOf(tabPage); + if ( oldPage < 0 ) + { + _pages.Add(tabPage); + StateHasChanged(); + } + else if (_pages[oldPage] != tabPage) + { + _pages[oldPage] = tabPage; + StateHasChanged(); + } + + if (_pages.Count == 1) + ActivePage = tabPage.Text; + } + + bool IsSelected(TabPage page) => ActivePage == page.Text; + + string GetButtonClass(TabPage page) => page.Text == ActivePage ? "active show" : ""; + + void ActivatePage(TabPage page) + { + ActivePage = page.Text; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TabPage.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TabPage.razor new file mode 100644 index 0000000..eefefde --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TabPage.razor @@ -0,0 +1,48 @@ +@if (Parent.PersistAllTabs || IsActive) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+ @ChildContent +
+} + +@code { + [CascadingParameter] private TabControl Parent { get; set; } = null!; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] public string Text { get; set; } = "Tab"; + [Parameter] public bool IsSelectable { get; set; } = true; + + private string PageClass => IsActive ? "" : "d-none"; + + private bool IsActive => Parent.ActivePage == Text; + + protected override void OnInitialized() + { + if (Parent == null) + throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl"); + + base.OnInitialized(); + Parent.AddPage(this); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TableColumnControl.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TableColumnControl.razor new file mode 100644 index 0000000..64b1659 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TableColumnControl.razor @@ -0,0 +1,185 @@ +@implements IDisposable + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+ @if (Sortable) + { + + } + else + { + + } + @if (TableControl is { Filterable: true } && Filterable && !string.IsNullOrEmpty(Field) && (FilterDropdownValues?.Count ?? 0) == 0) + { + + } + @if (TableControl is { Filterable: true } && Filterable && FilterDropdownValues?.Count > 0) + { + + } +
+ + +@code +{ + [Inject] + private IJSRuntime? _js { get; set; } + + [Parameter] public List? FilterDropdownValues { get; set; } + [Parameter] public string? DefaultCheckedValue { get; set; } + [Parameter] public string Name { get; set; } = "Column"; + [Parameter] public string? Field { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public RenderFragment<(dynamic Row, TableColumnControl Column)>? Template { get; set; } + [Parameter] public string? Format { get; set; } + [Parameter] public string NegativeValueClass { get; set; } = "t-neg"; + [Parameter] public string ZeroValueClass { get; set; } = "t-zero"; + [Parameter] public string PositiveValueClass { get; set; } = "t-pos"; + [Parameter] public bool Sortable { get; set; } = true; + [Parameter] public bool Filterable { get; set; } = true; + [Parameter] public bool ExactMatch { get; set; } + [Parameter] public bool ShowTotals { get; set; } = true; + [Parameter] public string HeaderStyle { get; set; } = ""; + + [Parameter] + public string FilterString + { + get; + set + { + if (field == value) + return; + field = value; + FilterChanged(value.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList()); + } + } = string.Empty; + + [CascadingParameter] + internal TableControl? TableControl { get; set; } + + public void SetFilter(string filterString) + { + FilterString = filterString; + FilterChanged(FilterString.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList()); + } + + protected override void OnInitialized() + { + TableControl?.AddColumn(this); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !string.IsNullOrWhiteSpace(DefaultCheckedValue)) + { + if (_js == null) + throw new ApplicationException("JS Runtime must be injected"); + + var values = await _js.InvokeAsync("DashboardUtils.CheckBox"); + if (!string.IsNullOrEmpty(values)) + { + FilterChanged(values.Split(",").Where(v => !string.IsNullOrEmpty(v)).Select(x => + { + x = x.Trim(); + if (x.Equals("Blanks")) return ""; + return x; + }).ToList()); + } + } + } + + public async Task CheckboxClicked() + { + var values = await _js!.InvokeAsync("DashboardUtils.CheckBox"); + FilterChanged(values + .Split(",") + .Where(v => !string.IsNullOrEmpty(v)) + .Select(x => x.Trim().Equals("Blanks") ? "" : x) + .ToList() + ); + + } + + public void Dispose() + { + TableControl?.RemoveColumn(this); + } + + protected string GetSortStyle() + { + if (string.IsNullOrEmpty(Field) || TableControl?.CurrentSortColumn != Field) + { + return string.Empty; + } + + const string style = "ui-icon-font ui-icon-sm "; + return style + TableControl.SortMode switch + { + TableControl.SortModeType.NoSort => "", + TableControl.SortModeType.Ascending => "icon-caret-up-sm" , + TableControl.SortModeType.Descending => "icon-caret-down-sm", + TableControl.SortModeType.AscendingAbsolute => "icon-caret-up-sm" , + TableControl.SortModeType.DescendingAbsolute => "icon-caret-down-sm", + _ => throw new ArgumentOutOfRangeException() + }; + } + + private void SortAction() + { + if (!string.IsNullOrEmpty(Field)) + { + TableControl?.SetColumnSort(Field); + } + } + + private void FilterChanged(List filterValue) + { + if (Field == null || TableControl == null) + return; + var task = TableControl.SetColumnFilter(Field, filterValue, ExactMatch); + task.Wait(); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TableControl.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TableControl.razor new file mode 100644 index 0000000..f71626f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TableControl.razor @@ -0,0 +1,763 @@ +@using System.Dynamic +@using System.Runtime.CompilerServices +@using System.Text +@using Microsoft.CSharp.RuntimeBinder + +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+ + + + + + @ChildContent + + + + + + @if (DisplayedItems.Any()) + { + var ix = 1 + (_currentPage - 1) * PageSize; + @foreach (var item in DisplayedItems) + { + + + @foreach (var column in Columns) + { + @if (column.Template == null && !string.IsNullOrEmpty(column.Field)) + { + var val = GetLambdaForValue(column.Field)(item); + + + } + else if (column.Template != null) + { + + } + else + { + + } + } + + ix++; + } + @if (Totals != null) + { + + + @foreach (var column in Columns) + { + if (column.ShowTotals) + { + var val = Totals.TryGet(column?.Name ?? "")?.Total; + + + } + else + { + + } + } + + } + } + + + @TableFooter + +
+
+ + @if (Filterable) + { + # + } +
+
+ @ix + + @ConvertToString(val, column.Format) + + @column.Template((item, column)) + + ??? +
+ Total + + @ConvertToString(val, column!.Format) + +   +
+ + +
+ + + + +@code { + public enum Direction + { + Back, + Previous, + Next, + Forward + } + + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public RenderFragment? TableFooter { get; set; } + + [Parameter] + public IReadOnlyCollection Items + { + get; + set + { + //detect if the new list is actually different, this is a little basic, but handles deletes/adds + //to the list. Ideally a hash of the list is best, but because 'StateHasChanged' is being called so many times + //by some controls (ContextControl!) this logic + if (value == null || (value.Count == _itemCount && Equals(field, value))) + return; + + field = value; + _itemCount = field.Count; + + ActiveFilters.Clear(); + ActiveFiltersExactMatch.Clear(); + //reapply the filtering to the new items + Task.Run(ApplyColumnFilterAndSort); + } + } = []; + + [Parameter] public int PageSize { get; set; } = 10; + [Parameter] public int PagerSize { get; set; } = 5; + [Parameter] public string? Class { get; set; } + [Parameter] public string? TableClass { get; set; } + [Parameter] public bool Filterable { get; set; } + [Parameter] public bool Selectable { get; set; } + [Parameter] public dynamic? SelectedItem { get; set; } + [Parameter] public string? SelectedColumn { get; set; } + [Parameter] public EventCallback<(dynamic row, string col)> SelectionChanged { get; set; } + [Parameter] public Func RowClass { get; set; } = _ => ""; + [Parameter] public bool AbsoluteSort { get; set; } = true; + [Parameter] public IMinMaxCache? Totals { get; set; } + + private string UniqueID { get;set;} = Guid.NewGuid().ToString(); + + private int _itemCount; + + /// + /// Allow the user to specify a custom class per cell, this will completely override the default styling + /// + [Parameter] public Func CellClass { get; set; } = DefaultGetCellClass; + /// + /// Allow the user to specify a custom style per cell, this will be applied on top of normal styling + /// + [Parameter] public Func CellStyle { get; set; } = (_,_) => ""; + + /// + /// Allows the user to apply _Extra_ styling on top of the existing default styling + /// + [Parameter] public Func? GetCellClassCallback { get; set; } + + [Parameter] + public List FilteredItems { get; set; } = []; + + [Parameter] + public EventCallback> FilteredItemsChanged { get; set; } + + [Parameter] + public string? CurrentSortColumn + { + get; + set + { + if (field == value) + return; + + field = value; + CurrentSortColumnChanged.InvokeAsync(field); + } + } = null; + + [Parameter] + public EventCallback CurrentSortColumnChanged { get; set; } + + public enum SortModeType + { + NoSort, + Ascending, + Descending, + AscendingAbsolute, + DescendingAbsolute + } + + [Parameter] + public SortModeType SortMode + { + get; + set + { + if (field == value) + return; + + field = value; + SortModeChanged.InvokeAsync(field); + } + } = SortModeType.NoSort; + + [Parameter] + public EventCallback SortModeChanged { get; set; } + + //Column the table is currently sorted by. + + protected IEnumerable DisplayedItems => FilteredItems?.Skip((_currentPage - 1) * PageSize).Take(PageSize) ?? []; + + private int _totalPages; + private int _currentPage = 1; + private int _startPage; + private int _endPage; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + if (Items == null) + return; + + await ApplyColumnFilterAndSort(); + } + } + + private async Task UpdatePage(int page) + { + _currentPage = page; + _totalPages = (int)Math.Ceiling((FilteredItems?.Count ?? 0) / (decimal)PageSize); + + var sideWidth = (int)Math.Floor((PagerSize - 1) / 2d); + _startPage = _currentPage - sideWidth >= 1 ? _currentPage - sideWidth : 1; + _endPage = _currentPage + sideWidth <= _totalPages ? _currentPage + sideWidth : _totalPages; + + await InvokeAsync(StateHasChanged); + } + #region sorting + + //Direction the table is currently sorted by + + /// + /// Returns a function that given a row item, will return the value for a specified field + /// + /// + /// + public static Func GetLambdaForValue(string? field) + { + return i => + { + if (string.IsNullOrEmpty(field)) + { + return ""; + } + switch (i) + { + case IDictionary dict: + { + dict.TryGetValue(field, out var value); + return value; + } + case IDictionary dict1: + { + dict1.TryGetValue(field, out var value); + return value; + } + case DynamicObject dyn: + { + return GetDynamicMember(dyn, field); + } + default: + return i?.GetType().GetProperty(field)?.GetValue(i, null); + } + }; + } + + /// + /// Returns a function that given a row item, will return the absolute value for a specified field + /// + /// + /// + public static Func GetLambdaForAbsValue(string? field) + { + var getValue = GetLambdaForValue(field); + return x => + { + var val = getValue(x); + return val switch + { + double d => Math.Abs(d), + int i => Math.Abs(i), + long l => Math.Abs(l), + float f => Math.Abs(f), + _ => val + }; + }; + } + + public static bool IsNumeric(object value) => value is double || + value is int || + value is long || + value is uint || + value is ulong; + + /// + /// This is a rather weird way around the ability to create an Icomparer of dynamic + /// + /// + public abstract class GridComparerDynamic : IComparer + { + public abstract int Compare(T? row1, T? row2); + } + + + public class GridComparer(Func methodLambda) : GridComparerDynamic + { + public override int Compare(object? row1, object? row2) + { + var row1Value = methodLambda(row1); + var row2Value = methodLambda(row2); + + if (row1Value == null && row2Value == null) return 0; + //put any nulls at the end of the list + if (row1Value == null) return -1; + if (row2Value == null) return 1; + + //its possible for the type to be different here + if (row1Value.GetType() != row2Value.GetType()) + { + //if both types are at least numberic, then compare them as doubles + if (IsNumeric(row1Value) && IsNumeric(row2Value)) + return ((double)row1Value).CompareTo((double)row2Value); + //fall back to comparing as strings + return row1Value.ToString().CompareTo(row2Value.ToString()); + } + if (row1Value is IComparable) + return row1Value.CompareTo(row2Value); + return 0; + } + } + + #endregion + #region Filtering + + protected readonly Dictionary> ActiveFilters = new(); + protected readonly Dictionary ActiveFiltersExactMatch = new(); + + + public async Task SetColumnFilter(string columnName, List filterValue, bool exactMatch = false) + { + if (ActiveFilters.ContainsKey(columnName) && + ActiveFilters[columnName] == filterValue && + ActiveFiltersExactMatch.ContainsKey(columnName) && + ActiveFiltersExactMatch[columnName] == exactMatch) + return; + + ActiveFilters[columnName] = filterValue; + ActiveFiltersExactMatch[columnName] = exactMatch; + if (filterValue.Count == 0) + { + ActiveFilters.Remove(columnName); + ActiveFiltersExactMatch.Remove(columnName); + } + + _currentPage = 1; + + await ApplyColumnFilterAndSort(); + } + + public async Task SetColumnSort(string columnName) + { + //Sorting against a column that is not currently sorted against. + if (columnName != CurrentSortColumn) + { + //Force order on the new column + CurrentSortColumn = columnName; + SortMode = AbsoluteSort + ? SortModeType.DescendingAbsolute + : SortModeType.Descending; + } + else //Sorting against same column but in different direction + { + if (AbsoluteSort) + SortMode = SortMode switch + { + SortModeType.AscendingAbsolute => SortModeType.DescendingAbsolute, + SortModeType.DescendingAbsolute => SortModeType.AscendingAbsolute, + _ => SortModeType.DescendingAbsolute + }; + else + SortMode = SortMode switch + { + SortModeType.Ascending => SortModeType.Descending, + SortModeType.Descending => SortModeType.Ascending, + _ => SortModeType.Descending + }; + } + + await ApplyColumnFilterAndSort(); + } + + private async Task ApplyColumnFilterAndSort() + { + IEnumerable fa = Items; // no copying needed + + //create data filtering linq + foreach (var (column, value) in ActiveFilters) + { + var exactMatch = ActiveFiltersExactMatch[column]; + fa = fa.Where(x => + { + var tableColumnDef = Columns.FirstOrDefault(c => c.Field == column); + + var colOriginalValue = GetLambdaForValue(column)(x); + + var colValue = ConvertToString(colOriginalValue, tableColumnDef?.Format); + + if (colValue != null) + { + var res = exactMatch + ? value.Any(v => colValue.Equals(v)) + : value.Any(v => colValue.Contains(v, StringComparison.OrdinalIgnoreCase)); + return res; + } + return false; + }); + } + + //add data sorting on top + fa = SortMode switch + { + SortModeType.NoSort => fa, + SortModeType.Ascending => fa.OrderBy (x => x, new GridComparer(GetLambdaForValue(CurrentSortColumn))), + SortModeType.Descending => fa.OrderByDescending(x => x, new GridComparer(GetLambdaForValue(CurrentSortColumn))), + SortModeType.AscendingAbsolute => fa.OrderBy (x => x, new GridComparer(GetLambdaForAbsValue(CurrentSortColumn))), + SortModeType.DescendingAbsolute => fa.OrderByDescending(x => x, new GridComparer(GetLambdaForAbsValue(CurrentSortColumn))), + _ => throw new ArgumentOutOfRangeException() + }; + + FilteredItems = [..fa]; + + try + { + await InvokeAsync(() => FilteredItemsChanged.InvokeAsync(FilteredItems)); + } + catch (Exception) + { + // ignore + } + + await UpdatePage(_currentPage); + } + #endregion + + #region pagination + + public async Task NavigateToPage(Direction direction) + { + var page = _currentPage; + switch (direction) + { + case Direction.Next: + { + if (page < _totalPages) + { + page += 1; + } + break; + } + case Direction.Previous: + { + if (page > 1) + { + page -= 1; + } + break; + } + case Direction.Back: + { + page = 1; + break; + } + case Direction.Forward: + { + page = _totalPages; + break; + } + } + await UpdatePage(page); + } + + public async Task NavigateToPage(int page) + { + if (page < 1) + { + page = 1; + } + else if (page > _totalPages) + { + page = _totalPages; + } + + await UpdatePage(page); + } + #endregion + + List Columns { get; set; } = []; + + public void AddColumn(TableColumnControl column) + { + Columns.Add(column); + } + + public void RemoveColumn(TableColumnControl column) + { + Columns.Remove(column); + } + + public static string ConvertToString(object? value, string? format) + { + if (value == null) + return ""; + + if (value is string s) + return s; + + if (format == null) + return value.ToString() ?? ""; + + + return value switch + { + double d => d .ToString( format ), + float f => f .ToString( format ), + long l => l .ToString( format ), + int i => i .ToString( format ), + TimeSpan ts => ts .ToString( format == "N0" ? "g" : format), + decimal dc => dc .ToString( format ), + DateTime dt => DateTimeFormatting(dt, format), + _ => value.ToString() ?? "" + }; + } + + private static string DateTimeFormatting(DateTime dt, string format) + { + if ( format == "N2" ) + { + return dt == DateTime.MinValue ? "" : dt.ToString("yyyy-MM-dd HH:mm:ss"); + } + return format == "N1" && dt == DateTime.MinValue ? "" : + dt.ToString((format == "N0" || format == "N1") ? "yyyy-MM-dd" : format); + } + + public static string DefaultGetCellClass(dynamic item, TableColumnControl column) + { + if (column?.Field == null) + return ""; + + var val = GetLambdaForValue(column.Field)(item); + return GetCellClass(item, val, column); + } + + public static string GetCellClass(dynamic row, object? value, TableColumnControl column) + { + var c = column.Class; + + var s = ""; + var isZero = false; + var isNegative = false; + var isPositive = false; + var customStyle = ""; + switch (value) + { + case double.NaN: + break; + case null: + break; + case double n: + isZero = n == 0.0; + isNegative = n < 0.0; + isPositive = n > 0.0; + break; + case int n: + isZero = n == 0.0; + isNegative = n < 0.0; + isPositive = n > 0.0; + break; + case long n: + isZero = n == 0L; + isNegative = n < 0L; + isPositive = n > 0L; + break; + case float n: + isZero = n == 0.0; + isNegative = n < 0.0; + isPositive = n > 0.0; + break; + case decimal d: + isZero = d == 0.0M; + isNegative = d < 0.0M; + isPositive = d > 0.0M; + break; + default: //string value + s = value.ToString(); + break; + } + + if (column.TableControl?.GetCellClassCallback != null) + { + customStyle = column.TableControl.GetCellClassCallback(row, column); + } + + return (s?.Equals("") ?? false) switch + { + true when isZero => $"align-right {c} {column.ZeroValueClass} {customStyle}", + true when isNegative => $"align-right {c} {column.NegativeValueClass} {customStyle}", + true when isPositive => $"align-right {c} {column.PositiveValueClass} {customStyle}", + + _ => $"align-left {c}{customStyle}" + }; + } + + private async Task OnSelectionChanged(dynamic selection, string? field) + { + SelectedItem = selection; + SelectedColumn = field; + + if (Selectable) + { + await SelectionChanged.InvokeAsync((selection, field ?? "")); + } + } + + public static object? GetDynamicMember(object obj, string memberName) + { + var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, obj.GetType(), + [CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)]); + var callsite = CallSite>.Create(binder); + try + { + return callsite.Target(callsite, obj); + } + catch (Exception) + { + return null; + } + } + + private void TriggerFilterRow() + { + Filterable = !Filterable; + } + + private async Task OnCopyAllToClipboard() + { + string Shield(string s) => s.Contains(",") || s.Contains("\n") ? $"\"{s}\"" : s; + + var str = new StringBuilder(); + str.Append(string.Join(",", Columns.Select(col => Shield(col.Name ?? "")))); + str.AppendLine(); + + foreach (var row in FilteredItems) + { + str.Append(string.Join(",", Columns.Select(col => + { + var colOriginalValue = GetLambdaForValue(col.Field)(row); + var colValue = ConvertToString(colOriginalValue, col.Format); + return Shield(colValue); + }))); + str.AppendLine(); + } + + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", str.ToString()); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TextEditorComponent.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TextEditorComponent.razor new file mode 100644 index 0000000..2d381a3 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TextEditorComponent.razor @@ -0,0 +1,112 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + + + + + +
+ +
+
+ +@code { + [Parameter] public string Class { get; set; } = ""; + + [Parameter] + public string Text + { + get => _text; + set + { + if (_text == value) + return; + _text = value; + + Task.Run( async () => await JsRuntime!.InvokeVoidAsync("DashboardUtils.CodeEditor_SetValue", _scriptEditorReference, _text) ); + } + } + + [Parameter] public EventCallback TextChanged { get; set; } + [Parameter] public int Rows { get; set; } = 50; + [Parameter] public string MimeType { get; set; } = "text/plain"; + + [Parameter] + public bool Readonly + { + get; + set + { + field = value; + SetReadonlyMode(); + } + } = false; + + [Inject] public IJSRuntime? JsRuntime { get; set; } + + private bool _initialized = false; + private DotNetObjectReference? _objRef = null; + private ElementReference _scriptEditorReference; + private string _text = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _objRef = DotNetObjectReference.Create(this); + + await JsRuntime!.InvokeVoidAsync("DashboardUtils.LoadCodeEditor", "TECCodeMirrorArea", MimeType, _scriptEditorReference, _objRef, "UpdateTextField", Readonly); + _initialized = true; + + await InvokeAsync(StateHasChanged); + } + } + + private void SetReadonlyMode() + { + if (_objRef == null) + { + return; + } + + JsRuntime!.InvokeVoidAsync("DashboardUtils.CodeEditor_SetParam", _scriptEditorReference, "readOnly", Readonly); + } + + [JSInvokable("UpdateTextField")] + public async Task UpdateTextField(string codeValue) + { + if (_text == codeValue) + return; + + _text = codeValue; + await TextChanged.InvokeAsync(codeValue); + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Controls/TreeControl.razor b/Rms.Risk.Mango.Pivot.UI/Controls/TreeControl.razor new file mode 100644 index 0000000..cfcbf97 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Controls/TreeControl.razor @@ -0,0 +1,153 @@ +@typeparam TNode + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+ + @foreach (var node in Nodes.Where(x => x != null )) + { + var nodeExpanded = Handler.IsExpanded(node); + var nodeSelected = ReferenceEquals(Handler.GetSelectedNode(), node) || (node?.Equals(Handler.GetSelectedNode()) ?? false); + var children = Handler.ChildSelector(node); + var hasChildren = children?.Any() ?? false; + +
+ @if (hasChildren) + { + + + + } + +
+ + @if (TitleTemplate == null) + { + @node?.ToString() + } + else + { + @TitleTemplate(node) + } + +
+ + @if (hasChildren && nodeExpanded) + { + + } + +
+ } + +
+ +@code { + + public class TreeItemHandler + { + public Func GetSelectedNode { get; init; } = () => default; + public Action SetSelectedNode { get; init; } = _ => {}; + public Func> ChildSelector { get; init; } = _ => Array.Empty(); + public Func IsExpanded { get; init; } = _ => true; + public Func IsSelectable { get; init; } = _ => true; + public Action SetIsExpanded { get; init; } = (_, _) => {/*do nothing*/}; + + } + + [Parameter] public IReadOnlyCollection Nodes { get; set; } = []; + [Parameter] public TreeItemHandler Handler { get; set; } = new(); + [Parameter] public RenderFragment? TitleTemplate { get; set; } + [Parameter] public bool AutoExpandTransientNodes { get; set; } = true; + [Parameter] public TreeStyle Style { get; set; } = TreeStyle.Bootstrap; + + public class TreeStyle + { + public static readonly TreeStyle Bootstrap = new() + { + ExpandNodeIconClass = "ui-icon-font icon-caret-right cursor-pointer", + CollapseNodeIconClass = "ui-icon-font icon-caret-down cursor-pointer", + NodeTitleClass = "list-group-item uic-tree-title_text cursor-pointer", + NodeTitleSelectedClass = "active" + }; + + public string? ExpandNodeIconClass { get; set; } + public string? CollapseNodeIconClass { get; set; } + public string? NodeTitleClass { get; set; } + public string? NodeTitleSelectedClass { get; set; } + + } + + + private void OnToggleNode(TNode node, bool expand) + { + var expanded = Handler.IsExpanded(node); + + if (expanded && !expand) + { + Handler.SetIsExpanded(node, false); + } + else if (!expanded && expand) + { + Handler.SetIsExpanded(node, true); + ExpandTransientChildren( node ); + } + } + + ///Auto expand child nodes when there is only 1 child + private void ExpandTransientChildren( TNode node ) + { + if ( !AutoExpandTransientNodes ) + return; + + while (Handler.ChildSelector(node)?.Count > 0) + { + var children = Handler.ChildSelector( node ) ?? Array.Empty(); + + if ( children.Count != 1 ) + return; + + var first = children.FirstOrDefault(); + if (first == null || Handler.IsExpanded( first ) ) + return; + + Handler.SetIsExpanded(first, true); + node = first; + } + } + + private void OnSelectNode(TNode node) + { + if (!Handler.IsSelectable(node)) + return; + + Handler.SetSelectedNode(node); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/CobPicker.razor b/Rms.Risk.Mango.Pivot.UI/Forms/CobPicker.razor new file mode 100644 index 0000000..4ba1364 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/CobPicker.razor @@ -0,0 +1,186 @@ +@using BlazorDateRangePicker + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+ @if (ShowLabel) + { +
+ @if (SupportsCobRanges) + { + + + } + else + { + + } +
+ } + +
+ + +
+   + @pickerContext.FormattedRange @(string.IsNullOrEmpty(pickerContext.FormattedRange) ? "Latest COB" : "") + +
+
+
+
+
+
+ +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + + private DateOnly? _cob = null; + private DateOnly? _cobRangeEnd = null; + + private bool NotUseCobRange => !UseCobRange; + + [Parameter] public EventCallback CobStrChanged { get; set; } + [Parameter] public EventCallback CobEndStrChanged { get; set; } + [Parameter] public EventCallback UseCobRangeChanged { get; set; } + [Parameter] public EventCallback OnCobRangeChanged { get; set; } + [Parameter] public Func DaysEnabledFunction { get; set; } = _ => true; + + [Parameter] public string? Label { get; set; } + + [Parameter] + public string Format + { + get; + set => field = string.IsNullOrWhiteSpace(value) ? "yyyy-MM-dd" : value; // Set default value if empty + } = "yyyy-MM-dd"; + + [Parameter] + public string? CobStr + { + get => _cob?.ToString(Format); + set + { + if (string.IsNullOrWhiteSpace(value)) + return; + + var d = DateOnly.ParseExact(value, Format, System.Globalization.CultureInfo.InvariantCulture); + if (_cob == d) + return; + _cob = d; + CobStrChanged.InvokeAsync(CobStr); + } + } + + [Parameter] + public string? CobEndStr + { + get => !SupportsCobRanges || !UseCobRange || _cobRangeEnd == null || _cobRangeEnd == _cob || _cobRangeEnd.Value == default ? null : _cobRangeEnd.Value.ToString(Format); + set + { + DateOnly d = default; + if (!string.IsNullOrWhiteSpace(value)) + { + d = DateOnly.ParseExact(value, Format, System.Globalization.CultureInfo.InvariantCulture); + } + + if (_cobRangeEnd == d) + return; + _cobRangeEnd = d; + CobEndStrChanged.InvokeAsync(CobEndStr); + } + } + + [Parameter] + public bool UseCobRange + { + get; + set + { + if (field == value) + return; + field = value; + UseCobRangeChanged.InvokeAsync(UseCobRange); + CobEndStrChanged.InvokeAsync(CobEndStr); + } + } + + + [Parameter] public bool SupportsCobRanges { get; set; } + [Parameter] public bool ShowLabel { get; set; } = true; + [Parameter] public string Class { get; set; } = ""; + + private DateTimeOffset? Cob + { + get => CobHelper.ConvertStringToOffset(CobStr); + set + { + var v = value?.ToString(Format); + if (CobStr == v) + return; + CobStr = v; + } + } + + private DateTimeOffset? CobRangeEnd + { + get => UseCobRange ? CobHelper.ConvertStringToOffset(CobEndStr) : Cob; + set + { + var v = value?.ToString(Format); + if (CobEndStr == v) + return; + CobEndStr = v; + } + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(Format)) + Format = "yyyy-MM-dd"; + + Cob ??= CobHelper.GetLatestCob(); + CobRangeEnd ??= CobHelper.GetLatestCob(); + } + + private Task SelectCob(DateRange d) + { + if (d == null) + { + CobStr = null; + CobEndStr = null; + return OnCobRangeChanged.InvokeAsync(null); + } + + if (!UseCobRange) + d.End = d.Start; + + CobStr = d.Start.ToString(Format); + CobEndStr = d.End.ToString(Format); + return OnCobRangeChanged.InvokeAsync(d); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/DragDropList.razor b/Rms.Risk.Mango.Pivot.UI/Forms/DragDropList.razor new file mode 100644 index 0000000..32c8918 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/DragDropList.razor @@ -0,0 +1,98 @@ +@typeparam TNode + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
    + @if (Value != null) + { + foreach (var item in Value) + { + if (item != null) + { +
  • + @ItemTemplate?.Invoke(item) +
  • + } + else + { +
  • NULL
  • + } + } + } +
+ +@code + { + [Parameter] public List? Value { get; set; } + [Parameter] public EventCallback> ValueChanged { get; set; } + [Parameter] public RenderFragment? ItemTemplate { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string LiClass { get; set; } = "drag-drop-li"; + + private int _currentIndex; + + private void StartDrag(TNode item) + { + _currentIndex = GetIndex(item); + } + + private int GetIndex(TNode item) => Value?.IndexOf(item) ?? -1; + + private async void Drop(TNode item) + { + try + { + if ( item == null || Value == null ) + return; + + var index = GetIndex(item); + if (index < 0) + return; + + // get current item + var current = Value[_currentIndex]; + // remove TNode from current index + Value.RemoveAt(_currentIndex); + Value.Insert(index, current); + + // update current selection + _currentIndex = index; + + await InvokeAsync(StateHasChanged); + await ValueChanged.InvokeAsync(Value); + } + catch (Exception) + { + // ignore + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormAutocompleteTextBox.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormAutocompleteTextBox.razor new file mode 100644 index 0000000..af3e176 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormAutocompleteTextBox.razor @@ -0,0 +1,76 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ @if (!string.IsNullOrEmpty(Name)) + { +
+ +
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + + @foreach (var s in Suggestions.Take(Math.Min(500, Suggestions.Count))) + { + + } + + +
+
+ +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + private string _suggestionsId = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public string Value + { + get; + set + { + if (field == value) + return; + + field = value; + ValueChanged.InvokeAsync(field); + } + } = ""; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public List Suggestions { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + private bool IsDisabled => !Enabled; + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormButton.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormButton.razor new file mode 100644 index 0000000..a2357a5 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormButton.razor @@ -0,0 +1,39 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code { + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool IsPrimary { get; set; } + [Parameter] public string Icon { get; set; } = ""; + [Parameter] public string Name { get; set; } = "OK"; + [Parameter] public string Class { get; set; } = "mr-1"; + [Parameter] public EventCallback OnClick { get; set; } + + private string ButtonClass => IsPrimary ? $"btn-primary {Class}" : $"btn-secondary {Class}"; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormCodeEditor.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormCodeEditor.razor new file mode 100644 index 0000000..6406225 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormCodeEditor.razor @@ -0,0 +1,189 @@ +@using System.Diagnostics + +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + + @if (string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ +
+ } + else + { +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } +
+
+ +
+ +
+
+
+
+ } +
+ +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] + public string Text + { + get; + set + { + if (field == value) + return; + field = value; + + if (_noUpdate) + return; + + try + { + JsRuntime.InvokeVoidAsync("DashboardUtils.CodeEditor_SetValue", _scriptEditorReference, value); + } + catch (Exception) + { + // ignore + } + } + } = ""; + + [Parameter] public EventCallback TextChanged { get; set; } + [Parameter] public string MediaType { get; set; } = "text/plain"; + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string Name { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + + [Parameter] + public bool Readonly + { + get; + set + { + field = value; + SetReadonlyMode(); + } + } = false; + + + private ElementReference _scriptEditorReference; + private DotNetObjectReference? _objRef = null; + private bool _initialized = false; + private bool _noUpdate = false; + + public Task InsertAtCursor(string text) + => JsRuntime.InvokeVoidAsync("DashboardUtils.CodeEditor_InsertTextAtCursor", _scriptEditorReference, text).AsTask(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + _objRef = DotNetObjectReference.Create(this); + + await JsRuntime.InvokeVoidAsync( "DashboardUtils.LoadCodeEditor", + _id, + MediaType, + _scriptEditorReference, + _objRef, + "UpdateScriptField", + Readonly ); + _initialized = true; + + await InvokeAsync(StateHasChanged); + + var sw = Stopwatch.StartNew(); + + await JsRuntime.InvokeVoidAsync("DashboardUtils.CodeEditor_SetValue", _scriptEditorReference, Text); + + sw.Stop(); + } + private async void SetReadonlyMode() + { + try + { + if (_objRef == null) + { + return; + } + + await JsRuntime.InvokeVoidAsync("DashboardUtils.CodeEditor_SetParam", _scriptEditorReference, "readOnly", Readonly); + } + catch (Exception) + { + // ignore + } + } + + [JSInvokable("UpdateScriptField")] + public async Task UpdateScriptField(string codeValue) + { + if (Text == codeValue) + return; + + try + { + _noUpdate = true; + Text = codeValue; + } + finally + { + _noUpdate = false; + } + + await TextChanged.InvokeAsync(codeValue); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormFileUploadButton.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormFileUploadButton.razor new file mode 100644 index 0000000..2579698 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormFileUploadButton.razor @@ -0,0 +1,41 @@ +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + +
+ +@code { + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool IsPrimary { get; set; } + [Parameter] public string Icon { get; set; } = "icon-upload-sm"; + [Parameter] public string Name { get; set; } = "Upload"; + [Parameter] public string Class { get; set; } = ""; + [Parameter] public EventCallback OnFileUploaded { get; set; } + + private readonly string _id = $"h{Random.Shared.Next():X8}"; + + private async Task OnLoad() + { + await JsRuntime.InvokeVoidAsync("eval", [$"document.getElementById('{_id}').click()"]); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckBox.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckBox.razor new file mode 100644 index 0000000..5a02965 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckBox.razor @@ -0,0 +1,88 @@ +@if (string.IsNullOrWhiteSpace(Name)) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ + + + +} +else +{ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + + + +
+
+} + +@code { + [Parameter] public string Name { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public string Class { get; set; } = ""; + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string LabelOn { get; set; } = "on"; + [Parameter] public string LabelOff { get; set; } = "off"; + + private bool IsDisabled => !Enabled; + private readonly string _id = $"h{Random.Shared.Next()}"; + + [Parameter] public bool Value + { + get; + set + { + if (field == value) + return; + field = value; + ValueChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckList.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckList.razor new file mode 100644 index 0000000..b11a20a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemCheckList.razor @@ -0,0 +1,168 @@ +@typeparam T + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + + @if (!string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ } + +
+ @{ + var groupedValues = GetGroupedValues(); + foreach ( var g in groupedValues ) + { + if ( groupedValues.Count > 1) + { +
@g.Name
+ } + @foreach (var v in g.Values) + { + if (v.Value?.Equals(v) ?? false) + { + @GetSelectedOption(v) + } + else + { + @GetOption(v) + } + } + } + } +
+
+ + + + +@code { + + public class SelectableItem(T val) + { + public bool Selected + { + get; + set + { + if (field == value) + return; + field = value; + SelectedChanged.InvokeAsync(field); + } + } + public T Value { get; } = val; + public EventCallback SelectedChanged { get; set; } + } + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public List Values + { + get; + set + { + if (field?.Equals(value) ?? false) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } = []; + + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string ListClass { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public Func IsSelectable { get; set; } = _ => true; + [Parameter] public Func GetText { get; set; } = x => x?.ToString() ?? ""; + [Parameter] public RenderFragment? OptionTemplate { get; set; } + [Parameter] public RenderFragment? SelectedOptionTemplate { get; set; } + + private string _id = $"h{Random.Shared.Next():X8}"; + + private class Group + { + public string Name { get; set; } = string.Empty; + public List Values { get; set; } = new (); + } + + private List GetGroupedValues() + { + var groups = new List(); + Group? currentGroup = null; + + foreach (var v in Values) + { + if (!IsSelectable(v.Value)) + { + // Start a new group with this name + currentGroup = new () { Name = GetText(v.Value) }; + groups.Add(currentGroup); + } + else + { + if (currentGroup == null) + { + // If no group has been started, create a default group + currentGroup = new () { Name = "Not grouped" }; + groups.Add(currentGroup); + } + currentGroup.Values.Add(v); + } + } + + return groups; + } + + private RenderFragment GetOption(SelectableItem v) + { + if (OptionTemplate != null) + { + return + @
+ + @OptionTemplate(v.Value) +
; + } + else + { + return + @
+ +
@GetText(v.Value)
+
; + } + } + + private RenderFragment GetSelectedOption(SelectableItem v) => GetOption(v); + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemDate.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemDate.razor new file mode 100644 index 0000000..8c1b9e4 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemDate.razor @@ -0,0 +1,110 @@ +@using BlazorDateRangePicker + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + + + +
+   + @pickerContext.FormattedRange @(string.IsNullOrEmpty(pickerContext.FormattedRange) ? Placeholder : "") + +
+
+
+
+
+ + + +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public DateOnly? StartDate + { + get; + set + { + if (field == value) + return; + field = value; + StartDateChanged.InvokeAsync(value); + } + } + + [Parameter] + public DateOnly? EndDate + { + get; + set + { + if (field == value) + return; + field = value; + EndDateChanged.InvokeAsync(value); + } + } + + [Parameter] public EventCallback StartDateChanged { get; set; } + [Parameter] public EventCallback EndDateChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool SelectDateRange { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public Func DaysEnabledFunction { get; set; } = _ => true; + + private DateTimeOffset? StartDateOffset + { + get => StartDate == null ? null : new DateTimeOffset(StartDate.Value, TimeOnly.MinValue, TimeSpan.Zero); + set + { + DateOnly? v = value != null ? DateOnly.FromDateTime(value.Value.Date) : null; + StartDate = v; + } + } + + private DateTimeOffset? EndDateOffset + { + get => EndDate == null ? null : new DateTimeOffset(EndDate.Value, TimeOnly.MinValue, TimeSpan.Zero); + set + { + DateOnly? v = value != null ? DateOnly.FromDateTime(value.Value.Date) : null; + EndDate = v; + } + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEmail.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEmail.razor new file mode 100644 index 0000000..fd476ff --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEmail.razor @@ -0,0 +1,80 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + +
+
+ + + +@code { + + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public List Value + { + get; + set + { + if (field.SequenceEqual(value)) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = []; + + [Parameter] public EventCallback> ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + + private string Url + { + get => string.Join(";",Value.Select( x=>x.ToString()).OrderBy( x => x )); + set + { + if (value == string.Join(";", Value.Select( x=>x.ToString()).OrderBy( x => x ))) + return; + Value = value + .Split( ";", StringSplitOptions.RemoveEmptyEntries ) + .OrderBy( x => x ) + .Select( x => new Uri(x ) ) + .ToList() + ; + } + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEnum.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEnum.razor new file mode 100644 index 0000000..ace9adc --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemEnum.razor @@ -0,0 +1,79 @@ +@typeparam TEnum + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + +
+
+ + + +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public TEnum Value + { + get; + set + { + if (field?.Equals(value) ?? false) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = default!; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + private string _id = $"h{Random.Shared.Next():X8}"; +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemList.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemList.razor new file mode 100644 index 0000000..5ea2361 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemList.razor @@ -0,0 +1,201 @@ +@typeparam T + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + +
+ + @if (!string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } +
+
+ @{ + var groupedValues = GetGroupedValues(); + if (groupedValues.Count == 1) + { + foreach (var v in groupedValues[0].Values) + { +
+ @RenderItem(v) +
+ } + } + else + { + foreach (var g in groupedValues) + { +
@g.Name
+
+ @foreach (var v in g.Values) + { +
+ @RenderItem(v) +
+ } +
+ } + } + } +
+
+
+
+ + + + +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public T Value + { + get; + set + { + if (value == null || (field?.Equals(value) ?? false)) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = default!; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public IReadOnlyCollection Values { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public Func IsSelectable { get; set; } = _ => true; + [Parameter] public Func GetText { get; set; } = x => x?.ToString() ?? ""; + [Parameter] public RenderFragment? OptionTemplate { get; set; } + [Parameter] public RenderFragment? SelectedOptionTemplate { get; set; } + + private string _id = $"h{Random.Shared.Next():X8}"; + + private string FormGroupClass => string.IsNullOrWhiteSpace(Name) ? Class : $"form-group {Class}"; + + private class Group + { + public string Name { get; set; } = string.Empty; + public List Values { get; set; } = new (); + } + + private List GetGroupedValues() + { + var groups = new List(); + Group? currentGroup = null; + + foreach (var v in Values) + { + if (!IsSelectable(v)) + { + // Start a new group with this name + currentGroup = new Group { Name = v?.ToString() ?? string.Empty }; + groups.Add(currentGroup); + } + else + { + if (currentGroup == null) + { + // If no group has been started, create a default group + currentGroup = new () { Name = "Not grouped" }; + groups.Add(currentGroup); + } + currentGroup.Values.Add(v); + } + } + + return groups; + } + + private void SelectItem(T value) + { + if (Enabled && IsSelectable(value)) + { + Value = value; + } + } + + private RenderFragment RenderItem(T value) + { + var selected = Value?.Equals(value) ?? false; + if ( selected ) + { + if (SelectedOptionTemplate != null) + { + return @
@SelectedOptionTemplate(value)
; + } + else if (OptionTemplate != null) + { + return @
@OptionTemplate(value)
; + } + else + { + return @
@GetText(value)
; + } + } + + if (OptionTemplate != null) + { + return @
@OptionTemplate(value)
; + } + else + { + return @
@GetText(value)
; + } + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemNameValue.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemNameValue.razor new file mode 100644 index 0000000..1594afb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemNameValue.razor @@ -0,0 +1,239 @@ +@typeparam TVal where TVal : new() + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ +
+ @if (!IsDisabled) + { + + } +
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + + + + + + + + + + @if (Enabled) + { + + + + } + + +
+
+ + + +@code { + + public class NameValuePair + { + public string Name { get; set; } = ""; + public TVal Value { get; set; } = new(); + } + + [Parameter] public string Name { get; set; } = ""; + [Parameter] public List Value { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool Multiline { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public Func GetValue { get; set; } = x => x.Value?.ToString() ?? ""; + [Parameter] public Action SetValue { get; set; } = DefaultSetValue; + [Parameter] public Action Changed { get; set; } = () => { }; + + private static void DefaultSetValue(NameValuePair x, string val) + { + var newVal = Convert.ChangeType(val, typeof(TVal)); + if (newVal is TVal tv) + x.Value = tv; + + } + + private bool IsDisabled => !Enabled; + + private bool IsUpDisabled(NameValuePair data) + { + var index = GetIndex(data); + return index <= 0; + } + + private int GetIndex(NameValuePair data) + { + var index = Value.IndexOf(data); + if ( index >= 0 ) + return index; + + var dataRef = Value.FirstOrDefault(x => x.Name == data.Name); + if (dataRef == null) + return -1; + index = Value.IndexOf(dataRef); + + return index; + } + + private Task Up(NameValuePair data) + { + var index = GetIndex(data); + if (index <= 0) + return Task.CompletedTask; + + var dataRef = Value[index]; + Value.RemoveAt(index); + Value.Insert(index - 1, dataRef); + + Changed(); + return InvokeAsync(StateHasChanged); + } + + private bool IsDownDisabled(NameValuePair data) + { + var index = GetIndex(data); + return index >= Value.Count - 1; + } + + private Task Down(NameValuePair data) + { + var index = GetIndex(data); + if (index >= Value.Count - 1) + return Task.CompletedTask; + + var dataRef = Value[index]; + Value.RemoveAt(index); + Value.Insert(index + 1, dataRef); + + Changed(); + return InvokeAsync(StateHasChanged); + } + + private Task Delete(NameValuePair data) + { + Value.Remove(data); + + Changed(); + return InvokeAsync(StateHasChanged); + } + + private Task SetValueInternal(ChangeEventArgs e, NameValuePair data) + { + SetValue(data, e.Value?.ToString() ?? ""); + + var dataRef = GetIndex(data); + if ( dataRef < 0 ) + return InvokeAsync(StateHasChanged); + + SetValue(Value[dataRef],e.Value?.ToString() ?? ""); + + Changed(); + return InvokeAsync(StateHasChanged); + } + + private Task SetNameInternal(ChangeEventArgs e, NameValuePair data) + { + data.Name = e.Value?.ToString() ?? ""; + + var dataRef = GetIndex(data); + if (dataRef < 0) + return InvokeAsync(StateHasChanged); + + + Value[dataRef].Name = e.Value?.ToString() ?? ""; + + Changed(); + return InvokeAsync(StateHasChanged); + } + + private Task AddNewKeyValue() + { + Value.Add(new ()); + + Changed(); + return InvokeAsync(StateHasChanged); + } + + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemPassword.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemPassword.razor new file mode 100644 index 0000000..0114f1c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemPassword.razor @@ -0,0 +1,71 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + + +
+
+ + + +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public string Value + { + get; + set + { + if (field == value) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = ""; + + private string Password + { + get => string.IsNullOrWhiteSpace(Value) ? "" : "*******"; + set => Value = value; + } + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = "Enter password here..."; + [Parameter] public string? Icon { get; set; } = "icon-unlock-outline-sm"; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public string InputType { get; set; } = "password"; + [Parameter] public string Class { get; set; } = ""; +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemSelect.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemSelect.razor new file mode 100644 index 0000000..43c3e6d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemSelect.razor @@ -0,0 +1,205 @@ +@typeparam T + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ @if (!string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + @if ( Editable ) + { + + @{ + var groupedValues = GetGroupedValues(); + if (groupedValues.Count == 1) + { + foreach (var v in groupedValues[0].Values) + { + if (Value?.Equals(v) ?? false) + { + + } + else + { + + } + } + } + else + { + foreach (var g in groupedValues) + { + + @foreach (var v in g.Values) + { + if (Value?.Equals(v) ?? false) + { + + } + else + { + + } + } + + } + } + } + + + } + else + { + + } +
+
+ + + + +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public T Value + { + get; + set + { + if (value == null || (field?.Equals(value) ?? false)) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = default!; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public IReadOnlyCollection Values { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool Editable { get; set; } + [Parameter] public Func IsSelectable { get; set; } = _ => true; + [Parameter] public Func GetText { get; set; } = x => x?.ToString() ?? ""; + + private string _id = $"h{Random.Shared.Next():X8}"; + private string _dataListId = $"h{Random.Shared.Next():X8}"; + + private string FormGroupClass => string.IsNullOrWhiteSpace(Name) ? Class : $"form-group {Class}"; + + private string ValueStr + { + get => Value?.ToString() ?? ""; + set + { + var obj = Values.FirstOrDefault(x => x?.ToString() == value); + if (obj != null) + Value = obj; + } + } + + private class Group + { + public string Name { get; set; } = string.Empty; + public List Values { get; set; } = new (); + } + + private List GetGroupedValues() + { + var groups = new List(); + Group? currentGroup = null; + + foreach (var v in Values) + { + if (!IsSelectable(v)) + { + // Start a new group with this name + currentGroup = new () { Name = GetText(v) }; + groups.Add(currentGroup); + } + else + { + if (currentGroup == null) + { + // If no group has been started, create a default group + currentGroup = new Group { Name = "Not grouped" }; + groups.Add(currentGroup); + } + currentGroup.Values.Add(v); + } + } + + return groups; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemStringList.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemStringList.razor new file mode 100644 index 0000000..07c60c2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemStringList.razor @@ -0,0 +1,72 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public List Value + { + get; + set + { + if (field.SequenceEqual(value)) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = []; + + [Parameter] public EventCallback> ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool Multiline { get; set; } + [Parameter] public int Rows { get; set; } = 5; + [Parameter] public int Cols { get; set; } = 80; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public string InputType { get; set; } = "text"; + [Parameter] public string Class { get; set; } = ""; + + private string Text + { + get => string.Join(";", Value.Select( x=>x.ToString()).Distinct().OrderBy( x => x )); + set + { + if (value == string.Join(";", Value.Select( x=>x.ToString()).OrderBy( x => x ))) + return; + Value = value + .Split( ";", StringSplitOptions.RemoveEmptyEntries ) + .Distinct() + .OrderBy( x => x ) + .ToList() + ; + } + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemText.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemText.razor new file mode 100644 index 0000000..86a1b4c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemText.razor @@ -0,0 +1,75 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + @if (Multiline) + { + + } + else + { + + } +
+
+ + + +@code { + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public string Value + { + get; + set + { + if (field == value) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = ""; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool Multiline { get; set; } + [Parameter] public int Rows { get; set; } = 5; + [Parameter] public int Cols { get; set; } = 80; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public string InputType { get; set; } = "text"; + [Parameter] public string Class { get; set; } = ""; +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormItemUri.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemUri.razor new file mode 100644 index 0000000..16a3656 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormItemUri.razor @@ -0,0 +1,75 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+
+ +
+
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + +
+
+ + + +@code { + private Uri? _value; + private string _id = $"h{Random.Shared.Next():X8}"; + + [Parameter] public string Name { get; set; } = ""; + [Parameter] + public Uri? Value + { + get => _value; + set + { + if (_value == value) + return; + _value = value; + ValueChanged.InvokeAsync(value); + } + } + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + private string Url + { + get => Value?.ToString() ?? ""; + set + { + if (value == (_value?.ToString() ?? "")) + return; + Value = new(value); + } + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormJson.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormJson.razor new file mode 100644 index 0000000..ac5526d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormJson.razor @@ -0,0 +1,56 @@ +@using MongoDB.Bson + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code { + + [Parameter] + public object Value + { + get; + set + { + if (field == value ) + return; + field = value ?? ""; + + var json = field switch + { + string s => s, + BsonDocument bson => bson.ToJson(new() { Indent = true }), + IEnumerable res => res.ToJson(new() { Indent = true }), + null => "", + _ => System.Text.Json.JsonSerializer.Serialize(field, new System.Text.Json.JsonSerializerOptions + { + WriteIndented = true + }) + }; + Json = $"```json\n{json}\n```"; + StateHasChanged(); + } + } = ""; + + [Parameter] public string Class { get; set; } = ""; + + private string Json { get; set; } = ""; + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormListBox.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormListBox.razor new file mode 100644 index 0000000..cf30412 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormListBox.razor @@ -0,0 +1,84 @@ +@typeparam T + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ @if (!string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + +
+
+ + + + +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public T Value + { + get; + set + { + if (field?.Equals(value) ?? false) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = default!; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public IReadOnlyCollection Values { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + private string _id = $"h{Random.Shared.Next():X8}"; +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormMarkdown.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormMarkdown.razor new file mode 100644 index 0000000..b0e361c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormMarkdown.razor @@ -0,0 +1,72 @@ +@using Markdig +@using Markdown.ColorCode + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + +
+ @MarkupText +
+
+ +@code { + + [Parameter] + public string Text + { + get; + set + { + if (field == value ) + return; + field = value; + + MarkupText = new(Markdown.ToHtml(field, _htmlPipeline)); + StateHasChanged(); + } + } = ""; + + [Parameter] public string Class { get; set; } = ""; + + private MarkupString MarkupText { get; set; } + + private readonly MarkdownPipeline _htmlPipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseColorCode() + .Build() + ; + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormTextAutocomplete.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormTextAutocomplete.razor new file mode 100644 index 0000000..eefe05c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormTextAutocomplete.razor @@ -0,0 +1,232 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ + + + @if (!string.IsNullOrWhiteSpace(Name)) + { +
+ +
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } +
+ @if (Icon != null) + { +
+
+
+ +
+
+
+ } + @if (IsMultiline) + { + @* ReSharper disable once CSharpWarnings::CS8974 *@ + +
+ +
+ } + else + { + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + } +
+
+ @if (ShowDropdown) + { +
    + @foreach (var value in FilteredValues) + { +
  • @value
  • + } +
+ } +
+ +@code { + + [Parameter] public string Name { get; set; } = ""; + + [Parameter] + public string Value + { + get; + set + { + if (field == value) + return; + field = value; + ValueChanged.InvokeAsync(value); + } + } = ""; + + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public IReadOnlyCollection Values { get; set; } = []; + [Parameter] public string? Placeholder { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool ShowDropdown { get; set; } + [Parameter] public EventCallback ShowDropdownChanged { get; set; } + [Parameter] public bool IsMultiline { get; set; } = false; + [Parameter] public int Rows { get; set; } = 1; + + private bool IsExpanded { get; set; } = false; + + private void ToggleExpand() + { + IsExpanded = !IsExpanded; + _rows = IsExpanded ? 10 : Rows; // Adjust the number of rows when expanded + + ShowDropdown = false; + ShowDropdownChanged.InvokeAsync(ShowDropdown); + + StateHasChanged(); + } + + private readonly string _id = $"h{Random.Shared.Next():X8}"; + private string FormGroupClass => string.IsNullOrWhiteSpace(Name) ? Class : $"form-group {Class}"; + private List FilteredValues { get; set; } = []; + private string DropDownMenuClass => IsMultiline ? "dd-menu-multi" : "dd-menu"; + + private int _rows = 1; + + private void FilterValues(ChangeEventArgs e) + { + Value = e.Value?.ToString() ?? ""; + ApplyFiltering(); + } + + private void ApplyFiltering() + { + if (string.IsNullOrEmpty(Value)) + { + // show everything if the input is empty + FilteredValues = Values.ToList(); + } + else + { + FilteredValues = Values.Where(v => v.Contains(Value, StringComparison.OrdinalIgnoreCase)).ToList(); + } + ShowDropdown = FilteredValues.Any(); + ShowDropdownChanged.InvokeAsync(ShowDropdown); + StateHasChanged(); + } + + private void SelectValue(string value) + { + Value = value; + ValueChanged.InvokeAsync(value); + Value = value; + ShowDropdown = false; + ShowDropdownChanged.InvokeAsync(ShowDropdown); + } + + private Task OnFocus() + { + ApplyFiltering(); + return Task.CompletedTask; + } + + protected override void OnInitialized() + { + _rows = Rows; + } + + private void OnKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Escape" && ShowDropdown ) + { + ShowDropdown = false; + ShowDropdownChanged.InvokeAsync(ShowDropdown); + StateHasChanged(); + } + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Forms/FormToggleButton.razor b/Rms.Risk.Mango.Pivot.UI/Forms/FormToggleButton.razor new file mode 100644 index 0000000..0c7cc30 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Forms/FormToggleButton.razor @@ -0,0 +1,34 @@ +@typeparam T + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code { + [Parameter] public bool Enabled { get; set; } = true; + [Parameter] public bool IsPrimary { get; set; } + [Parameter] public Func GetIcon { get; set; } = _ => ""; + [Parameter] public T? Arg { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public EventCallback OnClick { get; set; } + + private string ButtonClass => IsPrimary ? $"btn-primary {Class}" : $"btn-secondary {Class}"; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/ChartJsHelperForPivot.cs b/Rms.Risk.Mango.Pivot.UI/Pivot/ChartJsHelperForPivot.cs new file mode 100644 index 0000000..6170ec6 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/ChartJsHelperForPivot.cs @@ -0,0 +1,370 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections; +using ChartJs.Blazor.Common.Axes; +using ChartJs.Blazor.Common.Enums; +using ChartJs.Blazor.LineChart; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.UI.Controls; +using Rms.Risk.Mango.Pivot.UI.Services; + +namespace Rms.Risk.Mango.Pivot.UI.Pivot; + +/// +/// Prepare LineChard config for the given Pivot definition and data. +/// +public class ChartHelperForPivot +{ + public bool IsLineChart(PivotDefinition pivotDef, IPivotedData pivotData ) + => GetLineChartColumns(pivotDef, pivotData, out _, out _, out _); + + public LineConfig ChartConfig { get; } = new() + { + Options = new() + { + Title = new() + { + Display = false, + Text = "Data graph" + }, + Scales = new() + { + XAxes = + [ + new CategoryAxis + { + ScaleLabel = new() + { + LabelString = "Date" + } + } + ], + YAxes = + [ + new LinearCartesianAxis + { + ScaleLabel = new() + { + LabelString = "Amount" + } + } + ] + }, + Tooltips = new() + { + Mode = InteractionMode.Nearest, + Intersect = true + }, + Hover = new() + { + Mode = InteractionMode.Nearest, + Intersect = true + }, + Responsive = true, + Legend = new() + { + Display = false, + Position = Position.Right, + Labels = new() + { + FontColor = Night.light + } + } + } + }; + + public void UpdateLineChart( + string name, + IReadOnlyCollection labels, + IReadOnlyCollection data + ) + { + if ( name == null || labels == null || data == null ) + return; + + ChartConfig.Data.Labels.Clear(); + ChartConfig.Data.Labels.Add( name ); + + ChartConfig.Data.Datasets.Clear(); + var color = Night.RandomColorString(); + + var currentDataSet = new LineDataset + { + Label = name, + BackgroundColor = color, + BorderColor = color, + PointBackgroundColor = color, + PointRadius = 3, + PointBorderWidth = 1, + ShowLine = true, + Fill = false, + PointHitRadius = 5, + SteppedLine = SteppedLine.False + }; + + currentDataSet.AddRange( data ); + + ChartConfig.Data.XLabels.Clear(); + foreach (var label in labels) + ChartConfig.Data.XLabels.Add(label); + + ChartConfig.Data.Datasets.Add( currentDataSet ); + + ChartConfig.Options.Legend ??= new(); + ChartConfig.Options.Legend.Display = true; + ChartConfig.Options.Legend.Position = Position.Bottom; + + ChartConfig.Options.Scales ??= new(); + ChartConfig.Options.Scales?.YAxes.Clear(); + + if ( data.Count == 0 ) + return; + + var dataMin = data.Min(); + var dataMax = data.Max(); + + ChartConfig.Options.Scales?.YAxes.Add( new LinearCartesianAxis + { + Ticks = new() + { + Min = dataMin - Math.Abs(dataMin)*0.01, // -1% + Max = dataMax + Math.Abs(dataMax)*0.01 // +1% + } + }); + } + + public void UpdateLineChart( + PivotDefinition pivotDef, + IPivotedData pivotData, + Func getFormat + ) + { + if (!GetLineChartColumns(pivotDef, pivotData, out var xCol, out var yCol, out var dataSetColumns)) + return; + + var comparer = new RowComparer(pivotData, xCol, dataSetColumns!); + + // sort row indexes by data set key ( all yCol ), then by X-axis label (xCol) + var orderedRows = Enumerable + .Range(0, pivotData.Count) + .OrderBy(x => x, comparer) + .ToArray() + ; + + var labelObjects = orderedRows + .Select(x => pivotData.Get(xCol, x)) + .Where(x => x != null) + .Distinct() + .OrderBy(x => x) + .ToArray() + ; + var labels =labelObjects + .Select( x => TableControl.ConvertToString(x, getFormat(pivotDef.LineChartXAxis!))) + .ToList() + ; + + if (labels.Count == 0) // impossible + return; + + var labelPos = labelObjects + .Select((x, i) => new KeyValuePair(x!,i)) + .ToDictionary(x => x.Key, x => x.Value) + ; + + ChartConfig.Data.Labels.Clear(); + foreach (var label in labels) + ChartConfig.Data.Labels.Add(label); + + ChartConfig.Data.Datasets.Clear(); + + LineDataset []? currentDataSet = null; + var currentKey = new object[dataSetColumns?.Count ?? 0]; + var rowKey = new object?[dataSetColumns?.Count ?? 0]; + var data = new List(Enumerable.Range(0, yCol!.Count).Select(_ => new object[labelObjects.Length])); + + var dataMin = double.MaxValue; + var dataMax = double.MinValue; + + foreach (var row in orderedRows) + { + for ( var i = 0; i < (dataSetColumns?.Count ?? 0); i++) + rowKey[i] = pivotData.Get(dataSetColumns![i].Item2, row!); + + // start new data set if row label (all yCols) changed + if (currentDataSet == null || !rowKey.SequenceEqual(currentKey)) + { + if (currentDataSet != null) + { + for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1) + { + currentDataSet[dataSetNo].AddRange(data[dataSetNo]); + ChartConfig.Data.Datasets.Add(currentDataSet[dataSetNo]); + } + data = [..Enumerable.Range(0, yCol.Count).Select(_ => new object[labelObjects.Length])]; + } + + Array.Copy(rowKey, currentKey, currentKey.Length); + + currentDataSet = new LineDataset[yCol.Count]; + for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1) + { + var color = Night.RandomColorString(); + currentDataSet[dataSetNo] = new() + { + Label = (yCol.Count > 1 ? $"{yCol[dataSetNo].Item1} - " : "") + string.Join(" - ", currentKey.Select(x => x.ToString())), + BackgroundColor = color, + BorderColor = color, + PointBackgroundColor = color, + PointRadius = 3, + PointBorderWidth = 1, + ShowLine = true, + Fill = pivotDef.LineChartFill, + PointHitRadius = 5, + SteppedLine = pivotDef.LineChartSteppedLine ? SteppedLine.True : SteppedLine.False + }; + } + } + + // add null if label is missing + var label = pivotData.Get(xCol, row); + if (label == null) + continue; + + var pos = labelPos[label]; + + for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1) + { + var val = pivotData.Get(yCol[dataSetNo].Item2, row); + if (val is double d) + { + if (d < dataMin) + dataMin = d; + if (d > dataMax) + dataMax = d; + } + + data[dataSetNo][pos] = val; + } + } + + if (currentDataSet != null) + { + for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1) + { + currentDataSet[dataSetNo].AddRange(data[dataSetNo]); + ChartConfig.Data.Datasets.Add(currentDataSet[dataSetNo]); + } + } + + ChartConfig.Options.Legend ??= new(); + ChartConfig.Options.Legend.Display = pivotDef.LineChartShowLegend; + ChartConfig.Options.Legend.Position = Position.Right; + + // ReSharper disable CompareOfFloatsByEqualityOperator + if ( dataMin == double.MaxValue || dataMax == double.MinValue ) + return; + + ChartConfig.Options.Scales ??= new(); + //ChartConfig.Options.Scales.YAxes??= new(); + ChartConfig.Options.Scales?.YAxes.Clear(); + ChartConfig.Options.Scales?.YAxes.Add( new LinearCartesianAxis + { + Ticks = new() + { + Min = dataMin - Math.Abs(dataMin)*0.01, // -1% + Max = dataMax + Math.Abs(dataMax)*0.01 // +1% + } + }); + // ReSharper restore CompareOfFloatsByEqualityOperator + + } + + private bool GetLineChartColumns( + PivotDefinition pivotDef, + IPivotedData pivotData, + out int xColumn, + out List>? yColumn, + out List>? dataSetColumns + ) + { + xColumn = -1; + yColumn = null; + dataSetColumns = null; + + if ( pivotDef?.MakeLineChart == null + || pivotData == null + || pivotData.Count == 0 + || !(pivotData.Headers?.Count > 1) + || string.IsNullOrWhiteSpace(pivotDef.LineChartXAxis) + || (pivotDef.LineChartYAxis?.Count ?? 0 ) <= 0 + ) + { + return false; + } + + var headersDict = pivotData.GetColumnPositions(); + + if (!headersDict.TryGetValue(pivotDef.LineChartXAxis, out var xCol)) + return false; + + var yCol = (pivotDef.LineChartYAxis ?? []) + .Select(x => headersDict.TryGetValue(x, out var col) ? new Tuple(x, col) : null) + .Where(x => x != null) + .ToList() + ; + + var dCol = (pivotDef.LineChartDataSetKeys ?? []) + .Select(x => headersDict.TryGetValue(x, out var col) ? new Tuple(x, col) : null) + .Where(x => x != null) + .ToList() + ; + + xColumn = xCol; + yColumn = yCol!; + dataSetColumns = dCol!; + return true; + } + + /// + /// Compare pivot rows by multiple columns + /// + private class RowComparer(IPivotedData pivot, int xCol, List> dataSetCols) : IComparer + { + private readonly IPivotedData _pivot = pivot; + private readonly int _xCol = xCol; + private readonly List> _dataSetCols = dataSetCols ?? []; + + public int Compare(int row1, int row2) + { + int c; + + // sort by data set (if any) + foreach (var (_, col) in _dataSetCols) + { + c = Comparer.Default.Compare(_pivot.Get(col, row1), _pivot.Get(col, row2)); + if (c != 0) + return c; + } + + // sort by label + c = Comparer.Default.Compare(_pivot.Get(_xCol, row1), _pivot.Get(_xCol, row2)); + return c; + } + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/ExtraFilterDefinition.cs b/Rms.Risk.Mango.Pivot.UI/Pivot/ExtraFilterDefinition.cs new file mode 100644 index 0000000..ffa6830 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/ExtraFilterDefinition.cs @@ -0,0 +1,345 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson.Serialization.Attributes; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Pivot.UI.Pivot; + +public class ExtraFilterDefinition +{ + public const string ControlTypeDropDown = "DropDown"; + public const string ControlTypeDatePicker = "DatePicker"; + public const string CurrentCollectionSignature = "[COLLECTION]"; + public const string Any = ""; + + [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] + [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] + public class FilterControl + { + public string ControlType { get; set; } = string.Empty; + public bool AllowMultiselect { get; set; } + public string DisplayName { get; set; } = string.Empty; + public string FieldName { get; set; } = string.Empty; + public string? DefaultValue { get; set; } + public string Format { get; set; } = string.Empty; + [BsonIgnoreIfNull] + public string? SelectorCollection { get; set; } = string.Empty; + [BsonIgnoreIfNull] + public string? SelectorQuery { get; set; } = string.Empty; + [BsonIgnoreIfNull] + public List Values { get; set; } = []; + } + + public class FilterValue + { + public bool MultipleSelected { get; set; } + public HashSet? Value { get; set; } + public string? RangeStart { get; set; } + public string? RangeEnd { get; set; } + } + + + // ReSharper disable once PropertyCanBeMadeInitOnly.Global + public List Filter { get; set; } = new (); + + public List ParseQueryParameters(Dictionary queryParameters) + { + List values = new(new FilterValue[Filter.Count]); + foreach (var filterDef in Filter.Select((x, i) => (Index: i, Filter: x))) + { + if (!queryParameters.TryGetValue(filterDef.Filter.FieldName, out var value) + || (filterDef.Filter.ControlType != ControlTypeDatePicker && filterDef.Filter.ControlType != ControlTypeDropDown) + ) + { + values[filterDef.Index] = new() + { + MultipleSelected = false, + }; + continue; + } + + if (filterDef.Filter.ControlType == ControlTypeDatePicker) + { + var vv = value.Split(',', StringSplitOptions.RemoveEmptyEntries); + if (vv.Length > 1) + { + values[filterDef.Index] = new() + { + MultipleSelected = true, + RangeStart = vv[0], + RangeEnd = vv[1] + }; + } + else + { + values[filterDef.Index] = new() + { + MultipleSelected = false, + RangeStart = vv[0], + RangeEnd = vv[0] + }; + } + } + else if (filterDef.Filter.ControlType == ControlTypeDropDown) + { + var vv = value.Split(',', StringSplitOptions.RemoveEmptyEntries); + values[filterDef.Index] = new() + { + MultipleSelected = vv.Length > 1, + Value = [..vv] + }; + } + } + + return values; + } + + public Dictionary CreateQueryParameters(List values) + { + var queryParameters = new Dictionary(); + + foreach (var filterDef in Filter.Select((x, i) => (Index: i, Filter: x))) + { + var filterValue = values[filterDef.Index]; + if (filterDef.Filter.ControlType == ControlTypeDatePicker) + { + if (!string.IsNullOrWhiteSpace(filterValue.RangeStart) && !string.IsNullOrWhiteSpace(filterValue.RangeEnd)) + { + queryParameters[filterDef.Filter.FieldName] = filterValue.RangeStart != filterValue.RangeEnd && !string.IsNullOrWhiteSpace(filterValue.RangeEnd) && !string.IsNullOrWhiteSpace(filterValue.RangeStart) + ? $"{filterValue.RangeStart},{filterValue.RangeEnd}" + : filterValue.RangeStart ?? filterValue.RangeEnd; + } + } + else if (filterDef.Filter.ControlType == ControlTypeDropDown) + { + if (filterValue.Value != null && filterValue.Value.Any() && !(filterValue.Value.Count == 1 && filterValue.Value.Contains(Any))) + { + queryParameters[filterDef.Filter.FieldName] = string.Join(',', filterValue.Value); + } + } + } + + return queryParameters; + } + + + public List ParseExtraFilter(FilterExpressionTree.ExpressionGroup extraFilter) + { + // Clear the current _values list + List values = new(new FilterValue[Filter.Count]); + + if (!extraFilter.Children.Any()) + { + for( var i = 0; i < Filter.Count; i++ ) + { + var fd = Filter[i]; + values[i] = new () + { + MultipleSelected = false, + Value = fd.ControlType == ControlTypeDropDown && !string.IsNullOrWhiteSpace(fd.DefaultValue) + ? [fd.DefaultValue] + : fd.AllowMultiselect + ? [ Any ] + : [], + RangeStart = fd.ControlType == ControlTypeDatePicker ? fd.DefaultValue : null, + RangeEnd = null + }; + } + + return values; + } + + foreach (var filterDef in Filter.Select((x, i) => (Index: i, Filter: x))) + { + var filterValue = new FilterValue(); + var matchingGroup = extraFilter.Children + .OfType() + .FirstOrDefault(group => group.Children.Any(child => + child is FilterExpressionTree.FieldExpression fieldExpr && + fieldExpr.Field == filterDef.Filter.FieldName)); + + if (filterDef.Filter.ControlType == ControlTypeDatePicker) + { + if (matchingGroup != null) + { + filterValue.MultipleSelected = matchingGroup.Children.Count > 1; + + if (filterValue.MultipleSelected) + { + foreach (var child in matchingGroup.Children.OfType()) + { + switch (child.Condition) + { + case FilterExpressionTree.FieldConditionType.GreaterThanOrEqualTo: + filterValue.RangeStart = child.Argument; + break; + case FilterExpressionTree.FieldConditionType.LessThanOrEqualTo: + filterValue.RangeEnd = child.Argument; + break; + } + } + } + else + { + var startExpr = matchingGroup.Children + .OfType() + .FirstOrDefault(child => child.Condition == FilterExpressionTree.FieldConditionType.EqualTo); + filterValue.RangeStart = startExpr?.Argument; + filterValue.RangeEnd = startExpr?.Argument; + } + } + } + else if (filterDef.Filter.ControlType == ControlTypeDropDown) + { + if (matchingGroup != null) + { + filterValue.MultipleSelected = matchingGroup is { Condition: FilterExpressionTree.ExpressionGroup.ConditionType.Or, Children.Count: > 1 }; + + var selectedValues = matchingGroup.Children + .OfType() + .Select(child => child.Argument) + .ToHashSet(); + + filterValue.Value = selectedValues.Any() ? selectedValues : [Any]; + } + } + + values[filterDef.Index] = filterValue; + } + return values; + } + + public FilterExpressionTree.ExpressionGroup CreateExtraFilter(List values) + { + // Convert _values into ExtraFilter using FilterDef as a guide. + // If filter type is ControlTypeDatePicker it should use RangeStart and RangeEnd (if MultipleSelected). + // If filter type is ControlTypeDropDown it should use Value and MultipleSelected. + // ExtraFilter starts with an AND group. If multiple filters are set, they should be added to this group. + // If individual filter has MultipleSelected set to true, it should create an OR group for the values of that filter. + + // Initialize the root AND group for the ExtraFilter + var rootGroup = new FilterExpressionTree.ExpressionGroup + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And + }; + + for (var i = 0; i < Filter.Count; i++) + { + var filterDef = Filter[i]; + var filterValue = values[i]; + + if (filterDef.ControlType == ControlTypeDatePicker) + { + // Handle DatePicker filter + var start = string.IsNullOrWhiteSpace(filterValue.RangeStart) ? filterValue.RangeEnd : filterValue.RangeStart; + var end = string.IsNullOrWhiteSpace(filterValue.RangeEnd) ? filterValue.RangeStart : filterValue.RangeEnd; + + if (start == null || end == null) + continue; + + var dateGroup = new FilterExpressionTree.ExpressionGroup + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And + }; + + if ( start == end ) + { + dateGroup.Children.Add(new FilterExpressionTree.FieldExpression + { + Field = filterDef.FieldName, + Condition = FilterExpressionTree.FieldConditionType.EqualTo, + Argument = start + }); + } + else + { + if (!string.IsNullOrWhiteSpace(start)) + { + dateGroup.Children.Add(new FilterExpressionTree.FieldExpression + { + Field = filterDef.FieldName, + Condition = FilterExpressionTree.FieldConditionType.GreaterThanOrEqualTo, + Argument = start + }); + } + if (!string.IsNullOrWhiteSpace(end)) + { + dateGroup.Children.Add(new FilterExpressionTree.FieldExpression + { + Field = filterDef.FieldName, + Condition = FilterExpressionTree.FieldConditionType.LessThanOrEqualTo, + Argument = end + }); + } + } + + if ( dateGroup.Children.Any() ) + rootGroup.Children.Add(dateGroup); + } + else if (filterDef.ControlType == ControlTypeDropDown) + { + // Handle DropDown filter + if (filterValue.Value != null && filterValue.Value.Any()) + { + if (filterValue.Value?.Count > 1) + { + var orGroup = new FilterExpressionTree.ExpressionGroup + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.Or + }; + + foreach (var value in filterValue.Value) + { + orGroup.Children.Add(new FilterExpressionTree.FieldExpression + { + Field = filterDef.FieldName, + Condition = FilterExpressionTree.FieldConditionType.EqualTo, + Argument = value + }); + } + + rootGroup.Children.Add(orGroup); + } + else if (filterValue.Value?.Count == 1 && filterValue.Value.First() != Any) + { + var andGroup = new FilterExpressionTree.ExpressionGroup + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And, + Children = + [ + new FilterExpressionTree.FieldExpression + { + Field = filterDef.FieldName, + Condition = FilterExpressionTree.FieldConditionType.EqualTo, + Argument = filterValue.Value.First() + } + ] + }; + + rootGroup.Children.Add(andGroup); + } + } + } + } + + return rootGroup; + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/FilterComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/FilterComponent.razor new file mode 100644 index 0000000..2c6fc0a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/FilterComponent.razor @@ -0,0 +1,221 @@ +@using System.ComponentModel +@using System.Diagnostics.CodeAnalysis +@using Rms.Risk.Mango.Pivot.Core.Models + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + +
+ @if (item is FilterExpressionTree.ExpressionGroup group) + { +
+ + + +
+
+ + + +
+
+
+ } + else if (item is FilterExpressionTree.FieldExpression expression) + { +
+ + + +
+ + + +
+
+ +
+
+
+
+ + } +
+
+
+
+ +@code { + + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string RowClass { get; set; } = ""; + [Parameter] public Dictionary AllFields { get; set; } = null!; + + [Parameter] public FilterExpressionTree.ExpressionGroup Filter { get; set; } = new (); + [Parameter] public EventCallback FilterChanged { get; set; } + + private FilterExpressionTree.IFilterExpression[] RootNodes => [Filter]; + + private IReadOnlyCollection AllFieldsColl => AllFields.Select(x => x.Key).OrderBy(x => x).ToList(); + + private TreeControl.TreeItemHandler _handler = null!; + + protected override void OnInitialized() + { + _handler = new() + { + ChildSelector = x => x.Children, + GetSelectedNode = () => null, + SetSelectedNode = _ => { }, + IsExpanded = _ => true, + SetIsExpanded = (_, _) => {}, + IsSelectable = _ => false + }; + } + + private static bool HasChildren(FilterExpressionTree.IFilterExpression arg) => arg is FilterExpressionTree.ExpressionGroup && arg.Children.Count > 0; + + + private void AddGroup(FilterExpressionTree.ExpressionGroup parent) + { + if (parent?.Children == null) + return; + parent.Children.Add( new FilterExpressionTree.ExpressionGroup() ); + InvokeAsync(UpdateFilter); + } + + private void AddCondition(FilterExpressionTree.ExpressionGroup parent) + { + if (parent?.Children == null) + return; + parent.Children.Add(new FilterExpressionTree.FieldExpression()); + InvokeAsync(UpdateFilter); + } + + private void DelCondition(FilterExpressionTree.IFilterExpression context) + { + static bool Del(FilterExpressionTree.IFilterExpression parent, FilterExpressionTree.IFilterExpression toDel) + { + if (parent?.Children == null) + return false; + + foreach (var child in parent.Children) + { + if (child.Equals(toDel)) + { + parent.Children.Remove(child); + return true; + } + + if (Del(child, toDel)) + return true; + } + + return false; + } + + Del(Filter, context); + InvokeAsync(UpdateFilter); + } + + public static string GetDescriptionAttr([NotNull] T source) + { + var fi = source!.GetType().GetField(source.ToString()!); + + var attributes = (DescriptionAttribute[]?)fi?.GetCustomAttributes( + typeof(DescriptionAttribute), false); + + if (attributes is {Length: > 0 }) + return attributes[0].Description; + return source.ToString() ?? ""; + } + + private async Task UpdateFilter() + { + try + { + await FilterChanged.InvokeAsync(Filter); + } + catch ( Exception ) + { + // ignore + } + } + + + private async Task ChangeOperation(FilterExpressionTree.ExpressionGroup group, FilterExpressionTree.ExpressionGroup.ConditionType conditionType) + { + group.Condition = conditionType; + await InvokeAsync(UpdateFilter); + } + + private async Task ChangeField(FilterExpressionTree.FieldExpression expression, string value) + { + expression.Field = value; + await InvokeAsync(UpdateFilter); + } + + private async Task ChangeOperation(FilterExpressionTree.FieldExpression expression, FilterExpressionTree.FieldConditionType conditionType) + { + expression.Condition = conditionType; + await InvokeAsync(UpdateFilter); + } + + private async Task ChangeArgument(FilterExpressionTree.FieldExpression expression, string? value) + { + expression.Argument = value ?? ""; + await InvokeAsync(UpdateFilter); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/MessageBoxQueryComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/MessageBoxQueryComponent.razor new file mode 100644 index 0000000..a50210c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/MessageBoxQueryComponent.razor @@ -0,0 +1,160 @@ +@using MongoDB.Bson +@using MongoDB.Bson.IO +@using Rms.Risk.Mango.Language + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+ + + + + @if ( ShowJson ) + { + + + + } + @if ( !string.IsNullOrWhiteSpace(ErrorMessage) ) + { + + + + } + + + + +
+ +@code +{ + private static readonly System.Text.Json.JsonSerializerOptions _prettyPrint = new() + { + WriteIndented = true + }; + + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + [Parameter] public string Text { get; set; } = ""; + [Parameter] public string TextJson { get; set; } = ""; + [Parameter] public bool ShowJson { get; set; } = true; + [Parameter] public string OkButtonName { get; set; } = "OK"; + [Parameter] public string MimeType { get; set; } = "text/x-afh"; + + private string ActivePage { get; set; } = "Script"; + + private string? ErrorMessage { get; set; } + + protected override void OnParametersSet() + { + if (!string.IsNullOrWhiteSpace(Text) && string.IsNullOrWhiteSpace(TextJson)) + { + UpdatePipelineJson(); + } + else if (string.IsNullOrWhiteSpace(Text) && !string.IsNullOrWhiteSpace(TextJson)) + { + UpdatePipelineScript(); + } + } + + private async Task OnOK() + { + await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + } + + private void UpdatePipelineJson() + { + try + { + var json = GetCombinedPipelineText(); + TextJson = json; + ActivePage = "Script"; + } + catch (Exception e) + { + ErrorMessage = e.ToString(); + ActivePage = "Error"; + } + + } + + private void UpdatePipelineScript() + { + if ( !JsonUtils.IsValidJson(TextJson) ) + { + Text = TextJson; + MimeType = "text/plain"; + TextJson = ""; + ShowJson = false; + return; + } + + try + { + var ast = LanguageParser.ParseAggregationJsonToAST("collection-name-here", TextJson); + Text = ast.AsText(); + } + catch (Exception e) + { + ErrorMessage = e.ToString(); + ActivePage = "Error"; + + try + { + // Attempt to parse as BsonDocument if the JSON is not valid for the AST parser + if (BsonDocument.TryParse(TextJson, out var bson)) + { + var newJson = bson.ToJson(new() { Indent = true, OutputMode = JsonOutputMode.RelaxedExtendedJson }); + var ast = LanguageParser.ParseAggregationJsonToAST("collection-name-here", newJson); + Text = ast.AsText(); + ActivePage = "Json"; + return; + } + } + catch (Exception ex) + { + ErrorMessage = ex.ToString(); + ActivePage = "Error"; + } + } + + } + + private string GetCombinedPipelineText() + { + var ast = LanguageParser.ParseScriptToAST(Text); + var json = ast.AsJson(); + + var result = json?.ToJsonString(_prettyPrint) ?? ""; + return result; + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/NavigationUnit.cs b/Rms.Risk.Mango.Pivot.UI/Pivot/NavigationUnit.cs new file mode 100644 index 0000000..f092f17 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/NavigationUnit.cs @@ -0,0 +1,30 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Pivot.UI.Pivot; + +public class NavigationUnit +{ + public GroupedPivot SelectedPivotNode { get; init; } = null!; + public List Pivots { get; init; } = []; + public FilterExpressionTree.ExpressionGroup? Filter { get; init; } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotComponent.razor new file mode 100644 index 0000000..0e79ef2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotComponent.razor @@ -0,0 +1,674 @@ +@using System.Text +@using Newtonsoft.Json +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@inject IPivotSharingService PivotSharingService +@inject IJSRuntime Js + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+ +
+ + +
+
+ + + + + + + + +
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +@if (LastRefresh != default || LastRefreshElapsed != TimeSpan.Zero) +{ +

Last refresh @LastRefresh.ToLongTimeString() took @LastRefreshElapsed.ToString("g")

+} + +@code +{ + private const string SavePivotDialogHeader = "Save Pivot"; + private const string SharePivotDialogHeader = "Share Pivot"; + + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Inject] public IUserService UserSession { get; set; } = null!; + + + + #region Parameters + // ================================================= PARAMETERS ======================================================== + // = = + // = = + // = = + // ================================================= PARAMETERS ======================================================== + + [Parameter] public IPivotTableDataSource.PivotType PivotType { get; set; } = IPivotTableDataSource.PivotType.Predefined; + [Parameter] public RenderFragment ExtraFilter { get; set; } = null!; + + [Parameter] + public IPivotedData? PivotData + { + get; + set + { + if (field == value) + return; + + field = value; + PivotDataChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback PivotDataChanged { get; set; } + + /// + /// Unlike Pivot which contains pivot that would be executed CurrentPivot holds definition + /// that is already shown and corresponding to PivotData. + /// Always set PivotData and CurrentPivot at the same time. + /// + [Parameter] + public PivotDefinition? CurrentPivot + { + get; + set + { + if (value == null || field == value) + return; + field = value; + CurrentPivotChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback CurrentPivotChanged { get; set; } + + [Parameter] public IPivotTableDataSource PivotService { get; set; } = null!; + [Parameter] public bool ShowCollection { get; set; } = true; + + [Parameter] public string? Collection + { + get; + set + { + if ( field == value ) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + { + CollectionChanged.InvokeAsync(field); + if (Collections.Count > 0 && SelectedCollectionNode != null) + SelectedPivotNode = SelectedCollectionNode.Pivots.FirstOrDefault(x => x.Text == Pivot); + } + } + } + + [Parameter] public EventCallback CollectionChanged { get; set; } + + [Parameter] + public string? Pivot + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + PivotChanged.InvokeAsync(field); + + if (Collections.Count > 0 && SelectedCollectionNode != null) + SelectedPivotNode = SelectedCollectionNode.Pivots.FirstOrDefault(x => x.Text == Pivot); + } + } + + [Parameter] public EventCallback PivotChanged { get; set; } + + [Parameter] + public DateTime LastRefresh + { + get; + set + { + if (field == value) + return; + field = value; + LastRefreshChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback LastRefreshChanged { get; set; } + + [Parameter] + public TimeSpan LastRefreshElapsed + { + get; + set + { + if (field == value) + return; + field = value; + LastRefreshElapsedChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback LastRefreshElapsedChanged { get; set; } + + [Parameter] + public Navigation? Navigation + { + get; + set + { + if (field == value) + return; + field = value; + NavigationChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback> NavigationChanged { get; set; } + + + [Parameter] public string? AllPivotsAccessPolicyName { get; set; } + + [Parameter] public Func GetExtraFilter { get; set; } = () => null; + + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + + [Parameter] + public bool UseCache + { + get => _useCache; + set + { + if (_useCache == value) + return; + _useCache = value; + UseCacheChanged.InvokeAsync(_useCache); + } + } + + [Parameter] public EventCallback UseCacheChanged { get; set; } + + [Parameter] + public int Rows + { + get; + set + { + if (field == value) + return; + field = value; + RowsChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } = 40; + + [Parameter] public EventCallback RowsChanged { get; set; } + + [Parameter, EditorRequired] public List Collections { get; set; } = []; + + // ReSharper restore UnusedMember.Local + // ReSharper restore UnusedAutoPropertyAccessor.Local + + // ================================================= END OF PARAMETERS ================================================= + // = = + // = = + // = = + // ================================================= END OF PARAMETERS ================================================= + #endregion + + public void NavigateTo(string collection, GroupedPivot pivot) + { + if (string.IsNullOrWhiteSpace(collection) || pivot.IsGroup) + return; + + Collection = collection; + Pivot = pivot.Pivot.Name; + SelectedPivotNode = pivot; + } + + private GroupedCollection? SelectedCollectionNode => Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == Collection); + + private GroupedPivot? SelectedPivotNode + { + get + { + if (field != null) + return field; + if (string.IsNullOrWhiteSpace(Pivot)) + return null; + field = SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Text == Pivot); + return field; + } + set + { + if ( field == value ) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } + + private bool IsExportEnabled { get; set; } + private bool IsShareDisabled => CurrentPivot == null; + + private bool _isFilterHidden = true; + private bool _useCache = true; + + public PivotTableComponent PivotTable { get; private set; } = null!; + private HashSet AllDataFields => SelectedCollectionNode?.DataFields ?? []; + private HashSet AllKeyFields => SelectedCollectionNode?.KeyFields ?? []; + + private Task OnCopyCsv() => PivotTable.CopyCsv(); + private Task OnExportCsv() => PivotTable.ExportCsv(Uri.EscapeDataString($"{SelectedPivotNode?.Pivot.Name}.csv")); + + private string FilterHiddenClass => _isFilterHidden ? "hidden" : ""; + + private Task ShowHideFilter() + { + _isFilterHidden = !_isFilterHidden; + + return InvokeAsync(StateHasChanged); + } + + private Dictionary GetAllFields() + { + if (SelectedCollectionNode?.FieldTypes != null) + return SelectedCollectionNode.FieldTypes + .ToDictionary( + x => x.Key, + x => x.Value.Type + ); + + + var res = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (AllKeyFields.Count > 0) + { + foreach (var k in AllKeyFields.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(string); + } + } + + if (AllDataFields.Count > 0) + { + foreach (var k in AllDataFields.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(double); + } + } + + return res; + } + + protected override void OnInitialized() + { + if (SelectedPivotNode == null) + { + SelectedPivotNode = SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Text == Pivot); + StateHasChanged(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender) + return; + + Navigation ??= new(PivotTable.Navigate); + + await InvokeAsync(StateHasChanged); + } + + private Task OnRefreshPivot() => PivotTable.RunPivot(); + private bool IsSaveDisabled => Collection == null || SelectedPivotNode == null || SelectedPivotNode.IsGroup || SelectedCollectionNode?.Pivots == null; + private bool IsDeleteDisabled => IsSaveDisabled || SelectedPivotNode?.Pivot.IsPredefined == true; + + private static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + private Task GetUserName() => Task.FromResult(UserSession.GetEmail()); + + protected async Task OnSave() + { + if (IsSaveDisabled) + return; + + var user = await GetUserName(); + if (string.IsNullOrWhiteSpace(user)) + return; + + var pivotDef = SelectedPivotNode!.Pivot; + + if (pivotDef.IsPredefined) + { + var answer = await ModalDialogUtils.ShowConfirmationDialogWithInput( + Modal, + SavePivotDialogHeader, + $"Do you want to save Pivot \"{pivotDef.Name}\" to group \"{pivotDef.Group}\"?" + + "
If so, what's the magic word?" + + "

Note that you can always make your own copy by pressing Save As button.

", + "Magic word" + ); + if ( string.IsNullOrWhiteSpace(answer) ) + return; + + var magic = Base64Encode(DateTime.Now.ToString( "yyyy-MM-dd" )); + if ( answer != magic ) + { + await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, "Nope!" ); + return; + } + + } + else + { + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + SavePivotDialogHeader, + $"Do you want to save Pivot \"{pivotDef.Name}\" to group \"{pivotDef.Group}\"?" + ); + if ( res.Cancelled ) + return; + } + + // update filter + pivotDef.DrilldownFilter = ""; + + await PivotService.UpdatePivotAsync(Collection!, pivotDef, user, new CancellationTokenSource(5000).Token ); + + await ModalDialogUtils.ShowInfoDialog( + Modal, + SavePivotDialogHeader, + $"Pivot \"{pivotDef.Name}\" updated." + ); + } + + protected async Task OnShare() + { + if (Collection == null || CurrentPivot == null) + { + await ModalDialogUtils.ShowInfoDialog(Modal, SharePivotDialogHeader, "Please select collection and pivot to share."); + return; + } + + var def = new SharedPivotDef + { + PivotDef = CurrentPivot!, + Collection = Collection!, + ExtraFilter = GetExtraFilter()?.ToJson(GetAllFields()) ?? "", + SharedBy = await GetUserName(), + SharedAtUTC = DateTime.UtcNow + }; + + await SharePivot(def); + } + + protected async Task OnDelete() + { + if (IsDeleteDisabled) + return; + + var myself = await GetUserName(); + if (string.IsNullOrWhiteSpace(myself)) + return; + + var pivotDef = SelectedPivotNode!.Pivot; + var user = SelectedPivotNode.Pivot.Owner; + + if (!user.Equals(myself, StringComparison.OrdinalIgnoreCase)) + { + await ModalDialogUtils.ShowInfoDialog( + Modal, + SavePivotDialogHeader, + $"Pivot \"{pivotDef.Name}\" can only be deleted by user \"{user}\"." + ); + return; + } + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + SavePivotDialogHeader, + $"Do you want to delete Pivot \"{pivotDef.Name}\" from group \"{pivotDef.Group}\" for user \"{user}\"?" + ); + if ( res.Cancelled ) + return; + + await PivotService.DeletePivotAsync(Collection!, pivotDef.Name, pivotDef.Group, user, new CancellationTokenSource(5000).Token ); + + Pivot = + SelectedCollectionNode!.Pivots.FirstOrDefault(x => x is { IsGroup: false, Pivot.Name: "Summary" })?.Text + ?? SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup)?.Text + ; + + await ModalDialogUtils.ShowInfoDialog( + Modal, + SavePivotDialogHeader, + $"Pivot \"{pivotDef.Name}\" deleted." + ); + } + + protected async Task OnSaveAs() + { + if (IsSaveDisabled) + return; + + var user = await GetUserName(); + if (string.IsNullOrWhiteSpace(user)) + return; + + var pivotDef = SelectedPivotNode!.Pivot; + + var groups = new[] {PivotDefinition.UserPivotsGroup} + .Concat(SelectedCollectionNode!.Pivots + .Where(x => x is { IsGroup: false, Pivot.IsPredefined: true }) + .Select(x => x.Pivot.Group) + .Distinct() + ) + .ToArray() + ; + + var res = await PivotSaveAsComponent.ShowDialog(Modal, pivotDef, groups); + if (res == null) + return; + + var (name, group, answer) = res; + + if (string.IsNullOrWhiteSpace(name)) + return; + + var needMagic = group != PivotDefinition.UserPivotsGroup; + var magic = Base64Encode(DateTime.Now.ToString( "yyyy-MM-dd" )); + if ( needMagic && answer != magic) + { + await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, "Nope!" ); + return; + } + + pivotDef = SelectedPivotNode.Pivot.Clone(); + pivotDef.Name = name; + pivotDef.Group = group; + pivotDef.Owner = user; + // update filter + pivotDef.DrilldownFilter = ""; + + await PivotService.UpdatePivotAsync(Collection!, pivotDef, user, new CancellationTokenSource(5000).Token); + + Pivot = + SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup && x.Pivot.Group == pivotDef.Group && x.Pivot.Name == pivotDef.Name)?.Text + ?? SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup)?.Text + ; + + await ModalDialogUtils.ShowInfoDialog( + Modal, + SavePivotDialogHeader, + $"Pivot \"{pivotDef.Name}\" (group \"{pivotDef.Group}\") updated." + ); + + } + + private Task SharePivot(SharedPivotDef data) + => ModalDialogUtils.SafeCall(Modal, "Share Pivot", () => SharePivotUnsafe(data)); + + private ValueTask GetCurrentUrlViaJs() => Js.InvokeAsync( + "eval", + "window.location.href"); + + private async Task SharePivotUnsafe(SharedPivotDef data) + { + var json = JsonConvert.SerializeObject(data, Formatting.None); + var bytes = Encoding.UTF8.GetBytes(json); + + await using var mem = new MemoryStream(); + mem.Write(bytes); + mem.Position = 0; + + var guid = await PivotSharingService.SharePivot(data); + + var url = await MakeSharedUrl(guid); + + await ModalDialogUtils.ShowInfoDialog( + Modal, + SharePivotDialogHeader, + "

This function allows you to share the exact pivot you've just ran even if you made any modification to it.

" + + $"

Pivot \"{data.PivotDef.Name}\" with all modifications applied will be available using this URL:
{url}.

" + + "

Please copy this URL and send it using instant messaging or by E-Mail." + + "This URL will be valid for 7 days.

" + ); + + } + + private async Task MakeSharedUrl(string guid) + { + var url = await GetCurrentUrlViaJs(); + var idx = url.IndexOf('?'); + if (idx >= 0) + url = url[..idx]; + url += $"?shared={guid}"; + return url; + } + + public Task NavigateToSharedPivot(string guidStr) + => ModalDialogUtils.SafeCall(Modal, "Error running shared pivot", () => NavigateToSharedPivotUnsafe(guidStr)); + + + private async Task NavigateToSharedPivotUnsafe(string guidStr) + { + var sharedPivot = await PivotSharingService.GetSharedPivot(guidStr); + if (sharedPivot == null) + { + await InvokeAsync(StateHasChanged); + return; + } + + + Collection = sharedPivot.Collection; + + var extraFilter = FilterExpressionTree.ParseJson(sharedPivot.ExtraFilter ?? "{}"); + await InvokeAsync(() => PivotTable.RunPivot(sharedPivot.PivotDef, extraFilter)); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotDrilldownComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotDrilldownComponent.razor new file mode 100644 index 0000000..1f45e48 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotDrilldownComponent.razor @@ -0,0 +1,183 @@ +@using Rms.Risk.Mango.Pivot.Core + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+
+
+ +
+ + + + +
+
+
+ + @if (CurrentDrilldownDef != null) + { +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ } +
+ +@code { + [Parameter] public string Class { get; set; } = ""; + + [Parameter] + public PivotDefinition? Pivot + { + get; + set + { + if (field == value) + return; + + field = value; + CurrentDrilldownDef = field?.Drilldown.FirstOrDefault(); + InvokeAsync(StateHasChanged); + } + } = null!; + + [Parameter] public List AllPivots { get; set; } = []; + + + private PivotDefinition.DrilldownDef? CurrentDrilldownDef { get; set; } + private string CurrentField => CurrentDrilldownDef?.ColumnName ?? ""; + + private string _rowClass = ""; + + private Task SelectDrilldown(PivotDefinition.DrilldownDef? d) + { + if (d == null) + return Task.CompletedTask; + + CurrentDrilldownDef = d; + return InvokeAsync(StateHasChanged); + } + + private Task SelectDrilldownPivot(string? pivotName) + { + if (CurrentDrilldownDef == null) + return Task.CompletedTask; + CurrentDrilldownDef.DrilldownPivot = pivotName ?? Pivot?.Name ?? ""; + return InvokeAsync(StateHasChanged); + } + + private Task OnDelete() + { + if (IsDeleteDisabled || Pivot == null) + return Task.CompletedTask; + + Pivot.Drilldown.Remove(CurrentDrilldownDef!); + + CurrentDrilldownDef = Pivot.Drilldown.FirstOrDefault(); + return InvokeAsync(StateHasChanged); + } + + public bool IsDeleteDisabled => (Pivot?.Drilldown?.Count ?? 0) == 0; + + private Task OnAdd() + { + if (Pivot == null) + return Task.CompletedTask; + + var d = new PivotDefinition.DrilldownDef + { + ColumnName = "Type column name here", + DrilldownPivot = Pivot.Name + }; + Pivot.Drilldown.Add(d); + CurrentDrilldownDef = d; + return InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterComponent.razor new file mode 100644 index 0000000..2a782bd --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterComponent.razor @@ -0,0 +1,84 @@ +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +@code { + + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string RowClass { get; set; } = ""; + [Parameter] public Dictionary AllFields { get; set; } = null!; + + [Parameter] + public PivotDefinition? Pivot + { + get; + set + { + if (field == value) + return; + field = value; + + Filter = string.IsNullOrWhiteSpace(Pivot?.Filter) + ? new() + : FilterExpressionTree.ParseJson(Pivot.Filter) + ; + + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback FilterChanged { get; set; } + + private FilterExpressionTree.ExpressionGroup Filter + { + get; + set + { + field = value; + InvokeAsync(UpdateFilter); + } + } = new(); + + private async Task UpdateFilter() + { + if (Pivot == null) + return; + + try + { + Pivot.Filter = Filter.ToJson(AllFields); + await FilterChanged.InvokeAsync(Pivot.Filter); + } + catch ( Exception ) + { + // ignore + } + + await FilterChanged.InvokeAsync(Pivot.Filter); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterView.cs b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterView.cs new file mode 100644 index 0000000..14ea3d5 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotFilterView.cs @@ -0,0 +1,43 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core; + +namespace Rms.Risk.Mango.Pivot.UI.Pivot; + +public class PivotFilteredView : IPivotedData +{ + private readonly List _filteredRows; + + public PivotFilteredView(List filteredRows) + { + if ((filteredRows?.Count ?? 0) == 0) + throw new ApplicationException($"{nameof(filteredRows)} must be a non empty List"); + + _filteredRows = filteredRows!; + } + + public string Id { get; set; } = ""; + + public IReadOnlyCollection Headers => _filteredRows[0].PivotData.Headers; + public int Count => _filteredRows.Count; + public object? Get(int col, int row) => _filteredRows[row].PivotData.Get(col, _filteredRows[row].Row); + public Type GetColumnType(int col) => _filteredRows[0].PivotData.GetColumnType(col); + + public IPivotedData Filter(Func filter) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotGenericExtraFilterControl.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotGenericExtraFilterControl.razor new file mode 100644 index 0000000..245f798 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotGenericExtraFilterControl.razor @@ -0,0 +1,365 @@ +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@inject NavigationManager NavigationManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ @if ( FilterDef.Filter.Count > 0 && _values.Count == FilterDef.Filter.Count ) + { + foreach (var filter in FilterDef.Filter.Select((x,i) => (Position: i, Filter: x))) + { +
+ @switch (filter.Filter.ControlType) + { + case ExtraFilterDefinition.ControlTypeDropDown: + +
+
+ + @if (filter.Filter.AllowMultiselect) + { + + } +
+
+
+
+
+ +
+
+
+ @if (!_values[filter.Position].MultipleSelected) + { + + + } + else + { + + + @* ReSharper disable CSharpWarnings::CS8604 *@ + @* ReSharper disable CSharpWarnings::CS8619 *@ + + } + @* ReSharper restore CSharpWarnings::CS8619 *@ + @* ReSharper restore CSharpWarnings::CS8604 *@ +
+
+ + break; + + case ExtraFilterDefinition.ControlTypeDatePicker: + + + + break; + } +
+ } + } +
+ +@code { + + [Parameter] public string Class { get; set; } = ""; + + [Parameter] + public string Collection + { + get; + set + { + if (field == value) + return; + field = value; + + var isThereAnythingToUpdate = FilterDef.Filter.Any(fd => fd.Values.Count == 0 || fd.SelectorCollection == ExtraFilterDefinition.CurrentCollectionSignature); + if (isThereAnythingToUpdate ) + InvokeAsync(() => UpdateAvailableSelectorValues(true)); + } + } = ""; + + [Parameter] + public ExtraFilterDefinition FilterDef + { + get; + set + { + if ( field == value ) + return; + field = value; + InvokeAsync(() => UpdateAvailableSelectorValues()); + } + } = new(); + + [Parameter] public FilterExpressionTree.ExpressionGroup ExtraFilter { get; set; } = new(); + [Parameter] public EventCallback ExtraFilterChanged { get; set; } + [Parameter] public IPivotTableDataSource? PivotService { get; set; } + [Parameter] public bool LoadFilterDef { get; set; } + + private List _values = new(); + private Dictionary _queryParameters = new(); + private string _extraFilterJson = ""; + + protected override void OnInitialized() + { + if (_queryParameters.Count == 0 && NavigationManager != null) + { + // Parse query string parameters + var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); + if (!string.IsNullOrEmpty(uri.Query)) + { + _queryParameters = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()); + } + } + } + + protected override async Task OnParametersSetAsync() + { + if ( _values.Count == 0 ) + await ParseExtraFilter(); // Ensure extra filter is parsed after loading selector values + else + await UpdateExtraFilter(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if ( !firstRender ) + return; + + if (LoadFilterDef && PivotService != null && FilterDef.Filter.Count == 0) + await ReloadFilterDefinition(); + } + + private string _loadedForCollection = ""; + + private async Task ReloadFilterDefinition() + { + if ( + await ReloadFilterDefinition(Collection) + || await ReloadFilterDefinition("Global-Meta") + ) + { + await InvokeAsync(StateHasChanged); + } + } + + private async Task ReloadFilterDefinition(string collection) + { + if (!LoadFilterDef || PivotService == null || string.IsNullOrWhiteSpace(collection) || _loadedForCollection == collection) + return false; + + try + { + KeyValuePair[] filterDict = [new("_id", "ExtraFilter")]; + + var json = await PivotService.GetDocumentAsync(collection, filterDict, null); + if (string.IsNullOrWhiteSpace(json) || json.StartsWith("Not found")) + return false; + + var newDef = JsonUtils.FromJson(json); + if (newDef == null) + return false; + FilterDef = newDef; + _loadedForCollection = collection; + return true; + } + catch (Exception) + { + return false; + } + } + + private async Task ParseExtraFilter() + { + if ( ExtraFilter.IsEmpty && _queryParameters.Count > 0 ) + _values = FilterDef.ParseQueryParameters(_queryParameters); + else + _values = FilterDef.ParseExtraFilter(ExtraFilter); + await UpdateExtraFilter(); // Changed to await to ensure async execution + await InvokeAsync(StateHasChanged); + } + + private Task UpdateExtraFilter() + { + var ef = FilterDef.CreateExtraFilter(_values); + if (ef.Condition != FilterExpressionTree.ExpressionGroup.ConditionType.And) + { + // Add additional logic if needed for conditions other than 'And' + ef = new() + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And, + Children = [ef] + }; + } + + var json = ef.ToJson([]); + if ( json == _extraFilterJson ) + return Task.CompletedTask; + + _extraFilterJson = json; + + ExtraFilter.Children.Clear(); + ExtraFilter.Children.AddRange(ef.Children); + + return ExtraFilterChanged.InvokeAsync(ExtraFilter); + } + + private async Task UpdateAvailableSelectorValues(bool force = false) + { + var updated = false; + var filtersToUpdate = FilterDef.Filter.Where(fd => force || fd.Values.Count == 0); + foreach (var fd in filtersToUpdate) + { + updated |= await LoadSelectorValues(fd); + } + + if ( updated ) + await InvokeAsync(StateHasChanged); + } + + private async Task LoadSelectorValues(ExtraFilterDefinition.FilterControl fd) + { + var collection = fd.SelectorCollection; + if (string.IsNullOrWhiteSpace(collection) || collection == ExtraFilterDefinition.CurrentCollectionSignature) + collection = Collection; + + if ( fd.Values.Count > 0 + || PivotService == null + || string.IsNullOrWhiteSpace(collection) + || collection == ExtraFilterDefinition.CurrentCollectionSignature + || string.IsNullOrWhiteSpace(fd.SelectorQuery) + || string.IsNullOrWhiteSpace(fd.FieldName) + ) + { + return false; + } + + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + var pivot = new PivotDefinition + { + Name = $"__Load {fd.FieldName}", + PivotType = PivotTypeEnum.CustomQuery, + CustomQuery = fd.SelectorQuery + }; + + var values = await PivotService.PivotAsync(collection, pivot, null, false, token: cts.Token); + + var col = values.Headers.ToList().IndexOf(fd.FieldName); + if (col == -1) // Updated to check for -1 instead of null + throw new ApplicationException($"Field '{fd.FieldName}' not found in the result set."); + + for( var row = 0; row < values.Count; row++ ) + { + var v = values.Get(col, row); + var s = v switch + { + DateTime dt => dt.ToString(string.IsNullOrWhiteSpace(fd.Format) ? "yyyy-MM-dd" : fd.Format), + DateOnly d0 => d0.ToString(string.IsNullOrWhiteSpace(fd.Format) ? "yyyy-MM-dd" : fd.Format), + _ => v?.ToString() + }; + + if (!string.IsNullOrWhiteSpace(s) && !fd.Values.Contains(s)) + fd.Values.Add(s); + } + fd.Values.Sort(); + return true; + } + catch (Exception ex) + { + throw new ApplicationException($"Error fetching distinct values for field '{fd.FieldName}' from collection '{collection}': {ex.Message}", ex); + } + } + + private bool IsDateAvailable(DateTimeOffset dateTimeOffset, List filterValues, string format) + { + if ( filterValues.Count == 0) + return true; + var fmt = string.IsNullOrWhiteSpace(format) ? "yyyy-MM-dd" : format; + var dateStr = dateTimeOffset.ToString(fmt); + return filterValues.Contains(dateStr); + } + + private Task OnSelectedValueChanged(HashSet arg, int pos) + { + _values[pos].Value = arg; + return UpdateExtraFilter(); + } + + private Task SelectElement(string d, int pos) + { + _values[pos].Value = [d]; + return UpdateExtraFilter(); + } + + + private Task SelectStart(string? x, int pos) + { + _values[pos].RangeStart = x; + return UpdateExtraFilter(); + } + + private Task SelectEnd(string? y, int pos) + { + _values[pos].RangeEnd = y; + return UpdateExtraFilter(); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotKeysComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotKeysComponent.razor new file mode 100644 index 0000000..77fb312 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotKeysComponent.razor @@ -0,0 +1,323 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ +
+ +
+ + +
+ + +
+
+
+
+ +@code { + + [Parameter] public IReadOnlyCollection AllKeys { get; set; } = null!; + [Parameter] public string[] Value { get; set; } = []; + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string LabelClass { get; set; } = ""; + + internal class ItemWithSelection + { + public required string Text { get; init; } + + public bool IsSelected + { + get; + set + { + if (field == value) + return; + + field = value; + + // despite 'init' this happens right at the init stage too, so guard against null. + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + Changed?.Invoke(Text, IsSelected); + } + } + + public required Action Changed { get; init; } + + public override string ToString() => $"[{(IsSelected ? "X" : "")}] {Text}"; + + public override bool Equals(object? obj) + { + if (obj is not ItemWithSelection other) + return false; + return IsSelected == other.IsSelected && Text.Equals(other.Text, StringComparison.Ordinal); + } + + public override int GetHashCode() => HashCode.Combine(Text); + } + + private List Items + { + get; + set + { + if (!ReferenceEquals(value, field)) + { + if (Same(value, field)) + return; + field = value; + } + + OnChanged(null, false); + } + } = []; + + private List FilteredItems + { + get; + set + { + if (!ReferenceEquals(value, field)) + { + if (Same(value, field)) + return; + field = value; + if (!ReferenceEquals(field, Items)) + Items = ReorderItems(Items, field); + } + } + } = []; + + private string SearchString + { + get; + set + { + if (field == value) + return; + + field = value; + ApplySearch(); + } + } = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var v = SyncToItems(Value); + if (!Same(v, Items)) + { + Items = v; + ApplySearch(); + await InvokeAsync(StateHasChanged); + } + + await base.OnAfterRenderAsync(firstRender); + } + + private List SyncToItems(string[] value) + { + var valueSet = new HashSet(value); + var allSet = new HashSet(AllKeys); + + var selected = value + .Where(x => allSet.Contains(x)) + .Select(x => new ItemWithSelection {Text = x, IsSelected = true, Changed = OnChanged}) + ; + + var theRest = AllKeys + .Where(x => !valueSet.Contains(x)) + .OrderBy(x => x) + .Select(x => new ItemWithSelection {Text = x, IsSelected = false, Changed = OnChanged}) + ; + + return [..selected.Concat(theRest)]; + } + + private void OnChanged(string? _, bool __) + { + var v = SyncToValue(Items); + if (Same(v, Value)) + return; + Value = v; + ValueChanged.InvokeAsync(Value); + } + + private static bool Same(string[] v1, string[] v2) + { + if (v1.Length != v2.Length) + return false; + + return !v1.Where((t, i) => t != v2[i]).Any(); + } + + private static bool Same(List v1, List v2) + { + if (v1.Count != v2.Count) + return false; + + return !v1.Where((t, i) => t.Text != v2[i].Text || t.IsSelected != v2[i].IsSelected).Any(); + } + + private static string [] SyncToValue(List items) + { + var selected = items + .Where(x => x.IsSelected) + .Select(x => x.Text) + .ToArray() + ; + return selected; + } + + private void ApplySearch() + { + if (string.IsNullOrWhiteSpace(SearchString)) + { + if (ReferenceEquals(FilteredItems, Items) ) + return; + + Items = ReorderItems(Items, FilteredItems); + + FilteredItems = Items; + return; + } + + FilteredItems = Items + .Where(x => x.Text.Contains(SearchString, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + InvokeAsync(StateHasChanged); + } + + private static List ReorderItems( + IReadOnlyList items, + IReadOnlyList filteredItems + ) + { + try + { + var result = ReorderItemsUnsafe(items, filteredItems); + return result; + } + catch (Exception) + { + if (items is List l) + return l; + return [..items]; + } + } + + /// + /// Reorders 'items' so that the subset 'filteredItems' appears in the given (new) order, + /// while preserving the relative order of all other items. + /// Fix: Handles backward moves without prematurely emitting intervening others. + /// + /// + /// Generated by GPT-5. Tests are in Tests.Rms.Risk.Mango.PivotKeysComponentTests. + /// + /// Envelope-based algorithm: + /// + /// 1. Determine slice spanning min/max original indices of filtered items. + /// 2. Reorder only within that slice. + /// 3. Defer emitting "others" when a filtered item moves backward, preventing premature placement (fixing failing backward-move test). + /// 4. Emit others only between forward-moving adjacent filtered items; append remaining others at end of slice. Preserves relative order of non-filtered items and enforces new filtered order. + /// + /// + internal static List ReorderItemsUnsafe( + IReadOnlyList items, + IReadOnlyList filteredItems + ) + { + if (items == null) throw new ArgumentNullException(nameof(items)); + if (filteredItems.Count == 0) + { + if (items is List l) + return l; + return [.. items]; + } + + var indexMap = new Dictionary(items.Count); + for (var i = 0; i < items.Count; i++) + indexMap[items[i]] = i; + + // Validate subset. + if (filteredItems.Any(f => !indexMap.ContainsKey(f))) + throw new ArgumentException("filteredItems must be a strict subset of items.", nameof(filteredItems)); + + // Envelope covering all filtered original indices. + var minIndex = filteredItems.Min(f => indexMap[f]); + var maxIndex = filteredItems.Max(f => indexMap[f]); + + // Slice. + var segment = items.Skip(minIndex).Take(maxIndex - minIndex + 1).ToList(); + + var filteredSet = new HashSet(filteredItems); + var othersInSegment = segment.Where(x => !filteredSet.Contains(x)).ToList(); + + var resultSegment = new List(segment.Count); + var othersPos = 0; + var prevFilteredOrigIndex = -1; + + foreach (var f in filteredItems) + { + var currIndex = indexMap[f]; + + if (prevFilteredOrigIndex != -1 && currIndex > prevFilteredOrigIndex) + { + // Forward move: emit others whose original index is strictly between previous and current filtered. + while (othersPos < othersInSegment.Count) + { + var o = othersInSegment[othersPos]; + var oIdx = indexMap[o]; + if (oIdx > prevFilteredOrigIndex && oIdx < currIndex) + { + resultSegment.Add(o); + othersPos++; + } + else if (oIdx <= prevFilteredOrigIndex) + { + // Already passed earlier range; skip (should not happen with ordered list, but safe). + othersPos++; + } + else + break; + } + } + // Backward move: do not emit intervening others now. + resultSegment.Add(f); + prevFilteredOrigIndex = currIndex; + } + + // Append remaining others (those not yet emitted). + while (othersPos < othersInSegment.Count) + resultSegment.Add(othersInSegment[othersPos++]); + + // Reconstruct full list: before segment + new segment + after segment. + var before = items.Take(minIndex); + var after = items.Skip(maxIndex + 1); + return [.. before, .. resultSegment, .. after]; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavButtonsControl.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavButtonsControl.razor new file mode 100644 index 0000000..abd8017 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavButtonsControl.razor @@ -0,0 +1,112 @@ +@using Rms.Risk.Mango.Pivot.Core.Models + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ + + +
+
+ + + + + +
+
+
+ + +
+
+ +
+
+ +@code { + [Parameter] public string Class { get; set; } = "form-row"; + [Parameter] public Navigation Navigation { get; set; } = null!; + + [Parameter] public bool IsRefreshEnabled { get; set; } + [Parameter] public bool IsExportEnabled { get; set; } + + [Parameter] public EventCallback RefreshPivotTriggered { get; set; } + [Parameter] public EventCallback CopyCsvTriggered { get; set; } + [Parameter] public EventCallback ExportCsvTriggered { get; set; } + + [Parameter] + public bool UseCache + { + get; + set + { + if (field == value) + return; + field = value; + UseCacheChanged.InvokeAsync(field); + } + } = true; + + [Parameter] public EventCallback UseCacheChanged { get; set; } + + private bool IsBackwardDisabled => !Navigation?.CanGoBackward ?? true; + + private async Task OnBackward() + { + if (Navigation.Back()) + await InvokeAsync(StateHasChanged); + return false; + } + + private bool IsForwardDisabled => !Navigation?.CanGoForward ?? true; + + private async Task OnForward() + { + if (Navigation.Forward()) + await InvokeAsync(StateHasChanged); + return false; + } + + private bool IsRefreshDisabled => !IsRefreshEnabled; + private bool IsExportDisabled => !IsExportEnabled; + + private Task OnRefreshPivot() => RefreshPivotTriggered.InvokeAsync(); + private Task OnCopyCsv() => CopyCsvTriggered.InvokeAsync(); + private Task OnExportCsv() => ExportCsvTriggered.InvokeAsync(); + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavigatorComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavigatorComponent.razor new file mode 100644 index 0000000..ac2c99c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotNavigatorComponent.razor @@ -0,0 +1,172 @@ +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + + + + @if ( ExtraFilter != null ) + { + @ExtraFilter + } + + @if (ShowRows) + { +
+ +
+ } + + +
+ + +@code +{ + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Class { get; set; } = "form-row"; + [Parameter] public bool ShowCollection { get; set; } = true; + [Parameter] public bool ShowPivot { get; set; } = true; + [Parameter] public bool ShowRows { get; set; } = true; + + [Parameter] public RenderFragment? ExtraFilter { get; set; } + + [Parameter] + public int Rows + { + get; + set + { + if (field == value) + return; + + field = value; + RowsChanged.InvokeAsync(field); + } + } = 40; + + [Parameter] + public List Collections { get; set;} = []; + + [Parameter] + public bool UseCache + { + get; + set + { + if (field == value) + return; + field = value; + UseCacheChanged.InvokeAsync(field); + } + } = true; + + [Parameter] public EventCallback RowsChanged { get; set; } + [Parameter] public EventCallback UseCacheChanged { get; set; } + + [Parameter] public EventCallback RefreshPivotTriggered { get; set; } + [Parameter] public EventCallback CopyCsvTriggered { get; set; } + [Parameter] public EventCallback ExportCsvTriggered { get; set; } + + [Parameter] + public string? Collection + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + + CollectionChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } = null!; + + [Parameter] public EventCallback CollectionChanged { get; set; } + + [Parameter] + public string? Pivot + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + + PivotChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback PivotChanged { get; set; } + + [Parameter] public bool IsExportEnabled { get; set; } + [Parameter] public bool IsRefreshEnabled { get; set; } = true; + + [Parameter] public Navigation Navigation { get; set; } = null!; + + private static readonly List _rowValues = Enumerable.Range(0, 8).Select(x => x * 5 + 25).ToList(); + private GroupedCollection? SelectedCollectionNode => Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == Collection); + private GroupedPivot? SelectedPivotNode => SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Text == Pivot); + + private bool IsRefreshEnabledLocal => + ((ShowPivot && SelectedPivotNode is { IsGroup: false} ) || !ShowPivot) + && ((ShowCollection && SelectedCollectionNode is { IsGroup: false } ) || !ShowCollection) + && IsRefreshEnabled + ; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender) + return; + + Collection ??= Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == Collection && !x.IsGroup)?.CollectionNameWithPrefix + ?? Collections.FirstOrDefault(x => !x.IsGroup)?.CollectionNameWithPrefix; + + var pivotName = Pivot ?? "Summary"; + Pivot = SelectedCollectionNode?.Pivots.FirstOrDefault(x => !x.IsGroup && x.Pivot.Name == pivotName)?.Text + ?? SelectedCollectionNode?.Pivots.FirstOrDefault(x => !x.IsGroup)?.Text; + + StateHasChanged(); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotOptionsComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotOptionsComponent.razor new file mode 100644 index 0000000..191920d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotOptionsComponent.razor @@ -0,0 +1,379 @@ +@using Rms.Risk.Mango.Pivot.Core + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ @if ( _pivot != null ) + { +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + @if ( _pivot.Make2DPivot ) + { +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ } + +
+
+ + +
+
+ @if ( _pivot.MakeLineChart ) + { +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ } + } +
+ +@code { + [Parameter] public string Class { get; set; } = ""; + + [Parameter] + public PivotDefinition? Pivot + { + get => _pivot; + set + { + if (_pivot == value) + return; + + _pivot = value; + InvokeAsync(StateHasChanged); + } + } + + private PivotDefinition? _pivot; + private string _rowClass = ""; + + private string RenameColumns + { + get => string.Join( + "\n", + (_pivot?.RenameColumn ?? new Dictionary(StringComparer.OrdinalIgnoreCase)) + .Select(x => new { Key = x.Key?.Trim(), Value = x.Value?.Trim() }) + .Where(x => !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value)) + .Select(x => $"{x.Key}={x.Value}") + ); + set + { + if (_pivot == null || ColumnOrder == value) + return; + + _pivot.RenameColumn = (value ?? "") + .Replace("\r", "") + .Split("\n") + .Select(x => x.Split("=")) + .Where(x => x.Length == 2 && !string.IsNullOrWhiteSpace(x[0]) && !string.IsNullOrWhiteSpace(x[1])) + .ToDictionary(x => x[0].Trim(), x => x[1].Trim()) + ; + } + } + + + private string ColumnOrder + { + get => string.Join("\n", (_pivot?.ColumnsOrder ?? []).Select(x => x?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))); + set + { + if (_pivot == null || ColumnOrder == value) + return; + _pivot.ColumnsOrder ??= []; + _pivot.ColumnsOrder.Clear(); + _pivot.ColumnsOrder.AddRange((value ?? "") + .Replace("\r", "") + .Split("\n") + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + ); + } + } + + private string Pivot2DRows + { + get => string.Join("\n", (_pivot?.Pivot2DRows ?? []).Select(x => x?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))); + set + { + if (_pivot == null || Pivot2DRows == value) + return; + _pivot.Pivot2DRows ??= []; + _pivot.Pivot2DRows.Clear(); + _pivot.Pivot2DRows.AddRange((value ?? "") + .Replace("\r", "") + .Split("\n") + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + ); + } + } + + private string Pivot2DCol + { + get => _pivot?.Pivot2DColumn ?? ""; + set + { + if (_pivot == null || _pivot.Pivot2DColumn == value) + return; + _pivot.Pivot2DColumn = value; + } + } + + private string Pivot2DData + { + get => _pivot?.Pivot2DData ?? ""; + set + { + if (_pivot == null || _pivot.Pivot2DData == value) + return; + _pivot.Pivot2DData = value; + } + } + + private string Pivot2DDataTypeColumn + { + get => _pivot?.Pivot2DDataTypeColumn ?? ""; + set + { + if (_pivot == null || _pivot.Pivot2DDataTypeColumn == value) + return; + _pivot.Pivot2DDataTypeColumn = value; + } + } + + private string LineChartXAxis + { + get => _pivot?.LineChartXAxis ?? ""; + set + { + if (_pivot == null) + return; + _pivot.LineChartXAxis = value; + } + } + + private string LineChartYAxis + { + get => string.Join("\n", _pivot?.LineChartYAxis ?? Enumerable.Empty()); + set + { + if (_pivot == null) + return; + _pivot.LineChartYAxis ??= []; + _pivot.LineChartYAxis.Clear(); + _pivot.LineChartYAxis.AddRange(value + .Replace("\r", "") + .Split("\n") + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + ); + } + } + + private string LineChartAdditionalKeys + { + get => string.Join("\n",_pivot?.LineChartDataSetKeys ?? []); + set + { + if (_pivot == null) + return; + + var s = value + .Replace("\r", "") + .Split("\n") + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + ; + + _pivot.LineChartDataSetKeys ??= []; + _pivot.LineChartDataSetKeys.Clear(); + _pivot.LineChartDataSetKeys.AddRange(s); + } + } + + private bool LineChartShowLegend + { + get => _pivot?.LineChartShowLegend ?? false; + set + { + if (_pivot == null) + return; + _pivot.LineChartShowLegend = value; + } + } + + private bool LineChartSteppedLine + { + get => _pivot?.LineChartSteppedLine ?? false; + set + { + if (_pivot == null) + return; + _pivot.LineChartSteppedLine = value; + } + } + + private bool LineChartFill + { + get => _pivot?.LineChartFill ?? false; + set + { + if (_pivot == null) + return; + _pivot.LineChartFill = value; + } + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotQueryComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotQueryComponent.razor new file mode 100644 index 0000000..a599986 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotQueryComponent.razor @@ -0,0 +1,238 @@ +@using Rms.Risk.Mango.Pivot.Core +@using System.Threading +@using Rms.Risk.Mango.Pivot.Core.Models + +@inject IJSRuntime _jsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+
+ +
+ +
+
+
+ + @if ( PivotDef != null ) + { +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ } + + @if (PivotService != null) + { +
+
+ +
+
+ } + +
+ +@*
*@ +@*
*@ +@*
*@ +@*
*@ + +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + [Parameter] public string Class { get; set; } = ""; + [Parameter] + public PivotDefinition? Pivot + { + get => _pivot; + set + { + if (_pivot == value) + return; + + _pivot = value; + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public IPivotTableDataSource? PivotService { get; set; } + [Parameter] public string? Collection { get; set; } + [Parameter] public FilterExpressionTree.ExpressionGroup? ExtraFilter { get; set; } + + [Parameter] public Func> GetQueryText { get; set; } = (_, _, _, _) => Task.FromResult(""); + + private string SimpleClass => PivotDef?.PivotType == PivotTypeEnum.SimpleAggregation ? "" : "d-none"; + private string CustomClass => PivotDef?.PivotType is PivotTypeEnum.CustomQuery or PivotTypeEnum.AggregationForHumans ? "" : "d-none"; + private string _rowClass = ""; + + private string PivotTypeName => PivotDef?.PivotType switch + { + PivotTypeEnum.CustomQuery => "Custom query", + PivotTypeEnum.SimpleAggregation => "Simple aggregation", + PivotTypeEnum.AggregationForHumans => "Aggregation for Humans", + _ => "unsupported" + }; + + private PivotDefinition? _pivot; + private PivotDefinition? PivotDef => _pivot; + + // private DotNetObjectReference _objRef; + // private ElementReference _simpleBeforeGrouping; + // private ElementReference _simpleAdditionalGrouping; + // private ElementReference _simpleAfterGrouping; + // private ElementReference _customQuery; + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // _objRef = DotNetObjectReference.Create(this); + // + // await _jsRuntime.InvokeVoidAsync("DashboardUtils.LoadCodeEditor", "SimpleBeforeGrouping", "application/json", _simpleBeforeGrouping, _objRef, "UpdateBeforeGroupingField", false); + // await _jsRuntime.InvokeVoidAsync("DashboardUtils.LoadCodeEditor", "SimpleAdditionalGrouping", "application/json", _simpleAdditionalGrouping, _objRef, "UpdateAdditionalGroupingField", false); + // await _jsRuntime.InvokeVoidAsync("DashboardUtils.LoadCodeEditor", "SimpleAfterGrouping", "application/json", _simpleAfterGrouping, _objRef, "UpdateAfterGroupingField", false); + // await _jsRuntime.InvokeVoidAsync("DashboardUtils.LoadCodeEditor", "CustomQuery", "application/json", _customQuery, _objRef, "UpdateCustomQueryField", false); + + return InvokeAsync(StateHasChanged); + } + + return Task.CompletedTask; + } + + private Task SelectPivotType(PivotTypeEnum t) + { + PivotDef!.PivotType = t; + return InvokeAsync(StateHasChanged); + } + + [JSInvokable(nameof(UpdateBeforeGroupingField))] + public Task UpdateBeforeGroupingField(string text) + { + if (PivotDef == null || PivotDef.BeforeGrouping == text) + return Task.CompletedTask; + PivotDef.BeforeGrouping = text; + return InvokeAsync(StateHasChanged); + } + + [JSInvokable(nameof(UpdateAdditionalGroupingField))] + public Task UpdateAdditionalGroupingField(string text) + { + if (PivotDef == null || PivotDef.WithinGrouping == text) + return Task.CompletedTask; + PivotDef.WithinGrouping = text; + return InvokeAsync(StateHasChanged); + } + + [JSInvokable(nameof(UpdateCustomQueryField))] + public Task UpdateCustomQueryField(string text) + { + if (PivotDef == null || PivotDef.CustomQuery == text) + return Task.CompletedTask; + PivotDef.CustomQuery = text; + return InvokeAsync(StateHasChanged); + } + + + private async Task RevealTheQuery() + { + if (PivotService == null || string.IsNullOrWhiteSpace(Collection) || Pivot == null) + return; + + try + { + var text = GetQueryText == null + ? await PivotService.GetQueryTextAsync(Collection, Pivot, ExtraFilter) + : await GetQueryText(Collection, Pivot, ExtraFilter, CancellationToken.None) + ; + + await ShowQueryDialog(Modal, "Query", text); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Exception", ex); + } + } + + private async Task ShowQueryDialog(IModalService service, string header, string text) + { + var parameters = new ModalParameters(); + if (!string.IsNullOrWhiteSpace(text)) + parameters.Add(Pivot!.PivotType != PivotTypeEnum.AggregationForHumans ? "TextJson" : "Text", text); + parameters.Add("ShowJson", Collection?.StartsWith("BFG:") != true); + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + var form = service.Show(header, parameters, options); + await form.Result; + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotRow.cs b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotRow.cs new file mode 100644 index 0000000..db60b4c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotRow.cs @@ -0,0 +1,57 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Dynamic; +using Rms.Risk.Mango.Pivot.Core; + +namespace Rms.Risk.Mango.Pivot.UI.Pivot; + +public class PivotRow(IPivotedData data, + int row, + Dictionary _columnPositions, + Func _getDescriptor, + Func _getFieldDescriptor) : DynamicObject +{ + public IPivotedData PivotData { get; } = data; + public int Row { get; } = row; + + public PivotColumnDescriptor? GetColumnDescriptor(string columnName) => _getDescriptor(columnName); + public PivotFieldDescriptor? GetFieldDescriptor (string columnName) => _getFieldDescriptor(columnName); + + public override IEnumerable GetDynamicMemberNames() => PivotData.Headers; + + public override bool TryGetMember(GetMemberBinder binder, out object? result) + { + var found = _columnPositions.TryGetValue(binder.Name, out var col); + + result = found ? PivotData.Get(col, Row) : null; + return found; + } + + public string GetFormat(string column) => _getDescriptor(column)?.Format ?? ""; + + public bool ShouldShowTotals(string column) + { + var desc = _getFieldDescriptor(column); + if ( desc != null && desc.Purpose != PivotFieldPurpose.Data ) + return false; + + var colDesc = _getDescriptor(column); + return colDesc?.ShowTotals ?? true; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSaveAsComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSaveAsComponent.razor new file mode 100644 index 0000000..e2d4a3a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSaveAsComponent.razor @@ -0,0 +1,249 @@ +@using Rms.Risk.Mango.Pivot.Core + +@if (!string.IsNullOrWhiteSpace(Message)) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +

@Message

+ @if (RequestMagicWord) + { +

Since you are saving to non-user group, you need to provide the magic word.

+ } + +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+
+ + +
+ + + + +
+
+
+
+ + @if (RequestMagicWord) + { +
+
+
+ + +
+ +
+
+
+
+ + } + +
+} +
+ +
+ +@code +{ + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + private string Message => $"Do you want to save Pivot \"{Name.Parameter}\" to group \"{(Group.Parameter == NewGroupSignature ? NewGroupName.Parameter : Group.Parameter)}\"?"; + + public class StringParameter + { + public string Parameter { get; set; } = ""; + } + + [Parameter] + public StringParameter Name + { + get => _name; + set + { + if (_name == value) + return; + _name = value; + NameChanged.InvokeAsync(_name.Parameter); + } + } + + [Parameter] public EventCallback NameChanged { get; set; } + + [Parameter] + public StringParameter Group + { + get => _group; + set + { + if (_group == value) + return; + _group = value; + GroupChanged.InvokeAsync(_group.Parameter); + } + } + + [Parameter] public EventCallback GroupChanged { get; set; } + + [Parameter] + public StringParameter Magic + { + get => _magic; + set + { + if (_magic == value) + return; + _magic = value; + MagicChanged.InvokeAsync(_magic.Parameter); + } + } + + [Parameter] public EventCallback MagicChanged { get; set; } + + [Parameter] public string[] Groups { get; set; } = []; + + private bool RequestMagicWord => Group.Parameter != PivotDefinition.UserPivotsGroup; + private const string NewGroupSignature = ""; + private string NewGroupClass => Group.Parameter == NewGroupSignature ? "" : "d-none"; + + private StringParameter NewGroupName + { + get => _newGroupName; + set + { + if (_newGroupName == value) + return; + _newGroupName = value; + NewGroupNameChanged.InvokeAsync(_magic.Parameter); + } + } + + private EventCallback NewGroupNameChanged{ get; set; } + + private Task OnOK() + { + return BlazoredModal.CloseAsync( + ModalResult.Ok( + Tuple.Create( + Name.Parameter, + Group.Parameter == NewGroupSignature + ? NewGroupName.Parameter + : Group.Parameter, + RequestMagicWord + ? Magic.Parameter + : "" + ) + ) + ); + } + + private void SelectGroup(string item) + { + Group.Parameter = item; + //await InvokeAsync(StateHasChanged); + } + + private static StringParameter _name = new(); + private static StringParameter _group = new(); + private static StringParameter _newGroupName = new(); + private static StringParameter _magic = new(); + + /// + /// Display Pivot SaveAs dialog + /// + /// + /// + /// + /// Tuple (name, group, magic) or null if cancelled + public static async Task?> ShowDialog( + IModalService service, + PivotDefinition pivotDef, + string [] groups) + { + _name = new(); + _group = new(); + _newGroupName = new(); + _magic = new(); + + _name.Parameter = pivotDef.Name ?? ""; + _group.Parameter = pivotDef.Group ?? PivotDefinition.UserPivotsGroup; + _newGroupName.Parameter = "New group"; + + var parameters = new ModalParameters + { + { nameof(Name), _name }, + { nameof(Group), _group }, + { nameof(Groups), new[] {NewGroupSignature}.Concat(groups).ToArray() } + }; + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + var form = service.Show("Pivot SaveAs", parameters, options); + var res = await form.Result; + + if (res.Cancelled) + return null; + + return res.Data as Tuple; + } + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSelectorComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSelectorComponent.razor new file mode 100644 index 0000000..43d61a2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSelectorComponent.razor @@ -0,0 +1,166 @@ +@using Rms.Risk.Mango.Pivot.Core + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ @if (ShowCollection) + { +
+
+ + +
+
+ } + @if (ShowPivot) + { +
+
+ + +
+
+ } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + [Parameter] public bool ShowCollection { get; set; } = true; + [Parameter] public bool ShowPivot { get; set; } = true; + [Parameter] public string Class { get; set; } = "form-row"; + + [Parameter] + public string? Collection + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + + SyncSelectedNodes(); + + CollectionChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + private void SyncSelectedNodes() + { + if (Collections.Count == 0 || string.IsNullOrWhiteSpace(Collection)) + return; + + SelectedCollectionNode = Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == Collection); + + if (string.IsNullOrWhiteSpace(Pivot)) + return; + + SelectedPivotNode = SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Pivot?.Name == Pivot); + } + + [Parameter] public EventCallback CollectionChanged { get; set; } + + [Parameter] + public string? Pivot + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + SyncSelectedNodes(); + + PivotChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback PivotChanged { get; set; } + + [Parameter] + public List Collections + { + get; + set + { + if (field == value) + return; + + field = value; + + if ( value.Count > 0 ) + { + SyncSelectedNodes(); + InvokeAsync(StateHasChanged); + } + } + } = []; + + private GroupedCollection? SelectedCollectionNode + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + Collection = field?.CollectionNameWithPrefix; + } + } + + private GroupedPivot? SelectedPivotNode + { + get; + set + { + if (field == value || value == null) + return; + + field = value; + Pivot = field?.Pivot?.Name; + } + } + + + private static bool IsSelectableGroup(GroupedCollection? arg) => arg?.IsGroup == false; + private static bool IsSelectablePivot(GroupedPivot? arg) => arg?.IsGroup == false; + + + protected override void OnParametersSet() + { + SyncSelectedNodes(); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSettingsControl.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSettingsControl.razor new file mode 100644 index 0000000..524f710 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotSettingsControl.razor @@ -0,0 +1,150 @@ +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] public GroupedCollection? SelectedCollectionNode { get; set; } + + [Parameter] + public PivotDefinition? Pivot + { + get; + set + { + if (field == value) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public Func GetExtraFilter { get; set; } = () => null; + [Parameter] public IPivotTableDataSource PivotService { get; set; } = null!; + [Parameter] public bool Vertical { get; set; } + + private string Filter + { + get => Pivot?.Filter ?? ""; + set + { + if ( Pivot != null ) + Pivot.Filter = value; + } + } + + private HashSet AllDataFields => SelectedCollectionNode?.DataFields ?? []; + private HashSet AllKeyFields => SelectedCollectionNode?.KeyFields ?? []; + private List AllPivots => SelectedCollectionNode?.Pivots ?? []; + + private string[] CurrentPivotKeyFields + { + get => Pivot?.KeyFields ?? []; + set + { + if (Pivot != null ) + Pivot.KeyFields = value; + } + } + + private string[] CurrentPivotDataFields + { + get => Pivot?.DataFields ?? []; + set + { + if ( Pivot != null ) + Pivot.DataFields = value; + } + } + + private string? SelectedCollectionName => SelectedCollectionNode is { IsGroup: false } + ? SelectedCollectionNode.CollectionNameWithPrefix + : null; + + private Dictionary GetAllFields() + { + if (SelectedCollectionNode?.FieldTypes != null) + return SelectedCollectionNode.FieldTypes + .ToDictionary( + x => x.Key, + x => x.Value.Type + ); + + var res = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if ((AllKeyFields?.Count ?? 0) > 0) + { + foreach (var k in AllKeyFields!.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(string); + } + } + + if ((AllDataFields?.Count ?? 0) > 0) + { + foreach (var k in AllDataFields!.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(double); + } + } + + return res; + } + + private async Task GetQueryText(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default) + { + if (Pivot == null) + return ""; + + var text = await PivotService.GetQueryTextAsync(SelectedCollectionName!, Pivot, extraFilter, token); + return text; + } + + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/PivotTableComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotTableComponent.razor new file mode 100644 index 0000000..d72a498 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/PivotTableComponent.razor @@ -0,0 +1,1085 @@ +@using System.Diagnostics +@using System.Drawing +@using System.Dynamic +@using System.Reflection +@using System.Text.RegularExpressions +@using ChartJs.Blazor +@using ChartJs.Blazor.LineChart +@using log4net +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models + +@inject IJSRuntime Js +@inject NavigationManager NavigationManager +@inject IPivotSharingService PivotSharingService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ +
+
+ +@if (_pivotRefreshing) +{ +
+ Loading... +
+} +else if (_pivotRows is { Length: > 0 } + && (ShowIfEmpty || (!_pivotData?.Get(0, 0)?.ToString()?.Equals("No results") ?? false))) +{ + @if (!string.IsNullOrEmpty(Title)) + { +

@Title

+ } + @if (_pivotData?.Count == PivotMaxReturnedRows ) + { + + } + + @foreach (var field in _pivotRows[0].GetDynamicMemberNames()) + { + + var headerStyle = HeaderStyles.GetValueOrDefault(field, ""); + + + + + } + +} + +@code{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + public static int PivotMaxReturnedRows = 500_000; + + [Parameter] + public IPivotedData? PivotData + { + get => _pivotData; + set + { + if (_pivotData == value) + return; + + _pivotData = value; + _totals.Clear(); + + if (_pivotData == null) + { + _pivotRows = []; + InvokeAsync(StateHasChanged); + return; + } + + _shadowHeaders = _pivotData.GetColumnPositions(); + + _descriptorsCache.Clear(); + _fieldDescriptorsCache.Clear(); + + _pivotRows = Enumerable + .Range(0, _pivotData.Count ) + .Select(x => new PivotRow(_pivotData, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal)) + .ToArray() + ; + + _totals.Update(_pivotData!); + + try + { + ChartHelper.UpdateLineChart(CurrentPivot!, _pivotData, x => _pivotRows[0].GetFormat(x)); + } + catch (Exception) + { + // ignore + } + + PivotDataChanged.InvokeAsync(_pivotData); + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback PivotDataChanged { get; set; } + + /// + /// Unlike Pivot which contains pivot that would be executed CurrentPivot holds definition + /// that is already shown and corresponding to PivotData. + /// Always set PivotData and CurrentPivot at the same time. + /// + [Parameter] + public PivotDefinition? CurrentPivot + { + get; + set + { + if (value == null || field == value) + return; + field = value; + CurrentPivotChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback CurrentPivotChanged { get; set; } + + + [Parameter] public GroupedCollection? SelectedCollectionNode { get; set; } + + [Parameter] + public GroupedPivot? SelectedPivotNode + { + get => field; + set + { + if (field == value) + return; + field = value; + Filter = string.IsNullOrWhiteSpace(field?.Pivot.Filter) + ? new() + : FilterExpressionTree.ParseJson(field.Pivot.Filter) + ; + } + } + + [Parameter] public int Rows { get; set; } = 35; + [Parameter] public FilterExpressionTree.ExpressionGroup? ExtraFilter { get; set; } + [Parameter] public bool IsExportEnabled + { + get => !(CurrentPivot == null || _pivotRows is not { Length: > 0 }); + // ReSharper disable once ValueParameterNotUsed + set + { + // ignore + } + } + [Parameter] public EventCallback IsExportEnabledChanged { get; set; } + + [Parameter, EditorRequired] public IPivotTableDataSource PivotService { get; set; } = null!; + [Parameter] public Navigation? Navigation { get; set; } + [Parameter, EditorRequired] public List Collections { get; set; } = null!; + + + [Parameter] + public DateTime LastRefresh + { + get; + set + { + if (field == value) + return; + field = value; + LastRefreshChanged.InvokeAsync(field); + } + } + + [Parameter] + public TimeSpan LastRefreshElapsed + { + get; + set + { + if (field == value) + return; + field = value; + LastRefreshElapsedChanged.InvokeAsync(field); + } + } + + [Parameter] public EventCallback LastRefreshChanged { get; set; } + [Parameter] public EventCallback LastRefreshElapsedChanged { get; set; } + + [Parameter] public Func? GetPivotDefinition { get; set; } //by default DefaultGetPivotDefinition; + [Parameter] public Func?>> GetCustomDrilldown { get; set; }= (_,_) => Task.FromResult?>(null); + [Parameter] public Func GetColumnDescriptor { get; set; } = _ => null; + [Parameter] public Func GetFieldDescriptor { get; set; } = _ => null; + [Parameter] public Func> HandleCellClick { get; set; } =(_,_) => Task.FromResult(false); + [Parameter] public Func< + GroupedPivot /*pivot*/, + FilterExpressionTree.ExpressionGroup? /*userFilter*/, + List /*pivots*/, + NavigationUnit + > CreateNavigationUnit { get; set; } = DefaultCreateNavigationUnit; + [Parameter] public bool IsReadOnly { get; set; } + + [Parameter] public bool UseCache + { + get; + set + { + if (field == value) + return; + field = value; + UseCacheChanged.InvokeAsync(field); + } + } = true; + + [Parameter] public EventCallback UseCacheChanged { get; set; } + + [Parameter] public string Title { get; set; } = ""; + [Parameter] public bool ShowIfEmpty { get; set; } = true; + + [Parameter] public Dictionary HeaderStyles { get; set; } = []; + + private FilterExpressionTree.ExpressionGroup Filter { get; set; } = new(); + + + public Task CopyCsv() => CopyCsvInternal(); + public Task ExportCsv(string destFileName) => ExportCsvInternal(destFileName); + public Task RunPivot(bool transpose = false) => IsReadOnly ? Task.CompletedTask : RunPivotInternal(transpose); + + public void Navigate(PivotDefinition def, NavigationUnit data) + { + if (IsReadOnly) + return; + NavigateInternal(def, data); + } + + public Task ShowDefaultDocument(KeyValuePair[] fields) => IsReadOnly ? Task.CompletedTask : ShowDefaultDocumentInternal(fields); + + + private PivotRow[] ? _pivotRows; + private IPivotedData ? _pivotData; + private readonly MinMaxCache _totals = new(); + private string ? _prevCollection; + private GroupedCollection ? _currentCollection; + private DrilldownSupport ? _drilldownSupport; + private bool _pivotRefreshing; + private PivotColumnDescriptor[] ? _descriptors; + private Dictionary ? _fieldTypes; + private List _filteredRows = []; + private readonly DelayedExecution _delayedUpdate = new(TimeSpan.FromMilliseconds(1500)); + private Dictionary _shadowHeaders = []; + + private bool ShowTotals => SelectedPivotNode?.Pivot.ShowTotals ?? false; + private IMinMaxCache? VisibleTotals => ShowTotals ? _totals : null; + private List Pivots => SelectedCollectionNode?.Pivots ?? []; + private HashSet AllDataFields => SelectedCollectionNode?.DataFields ?? []; + private HashSet AllKeyFields => SelectedCollectionNode?.KeyFields ?? []; + private LineConfig ChartConfig => ChartHelper.ChartConfig; + private ChartHelperForPivot ChartHelper { get; } = new(); + + private string ChartClass => !ChartHelper.IsLineChart(CurrentPivot!, _pivotData!) + ? "d-none" + : "" + ; + + private string ColumnClass + { + get + { + //This is a little awkward, usually the table control will auto wrap long headers when they contain spaces, but we have some pivots currently that have + //a combination of long columns with and without spaces. Wrapping looks weird here, so we want to turn off wrapping if we can + //detect any column which has a long name without spaces + + if (_pivotRows != null && + (_pivotRows[0].GetDynamicMemberNames().Count() <= 16 + || _pivotRows[0].GetDynamicMemberNames().Any(n => n.Length >= 10 && !n.Contains(" ")) + ) + ) + return "table-nowrap"; + else + return string.Empty; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender || IsReadOnly) + return; + + try + { + Navigation ??= new(Navigate); + + _drilldownSupport = new(Collections) + { + ShowDocument = ShowDefaultDocument, + MessageBoxShow = m => ModalDialogUtils.ShowInfoDialog(Modal, "Pivot", m), + MessageBoxShowYesNo = async (m, t) => + { + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, t, m); + return !res.Cancelled; + }, + ShowException = ex => ModalDialogUtils.ShowExceptionDialog(Modal, "Pivot", ex), + GetPivotDefinition = GetPivotDefinition ?? DefaultGetPivotDefinition, + GetCustomDrilldown = GetCustomDrilldown + }; + + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching collections from pivot service", e); + } + } + + private PivotDefinition? DefaultGetPivotDefinition(string pivotName, string _ /*collectionName*/) => + _currentCollection?.Pivots?.FirstOrDefault(x => x?.Pivot?.Name?.Equals(pivotName, StringComparison.OrdinalIgnoreCase) ?? false)?.Pivot; + + private async Task OnCellClick(DynamicObject row, string fieldName) + { + try + { + if (HandleCellClick != null && await HandleCellClick(row, fieldName)) + return true; + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error drilling down into {fieldName}", e); + } + + if ( IsReadOnly || _drilldownSupport == null) + return true; + + if (CurrentPivot == null) + return false; + + var displayToRealName = new Dictionary(StringComparer.OrdinalIgnoreCase); + if ( CurrentPivot.RenameColumn?.Count > 0 && PivotData is ArrayBasedPivotData { HeadersMap: { } } apd) + { + // check for renamed columns + foreach ( var item in apd.HeadersMap ) + displayToRealName[item.DesplayHeader] = item.OrigHeader; + } + + var res = await _drilldownSupport.Drilldown( + PivotService!, + SelectedCollectionNode!.CollectionNameWithPrefix, + fieldName, + displayToRealName, + ((PivotRow)row).GetDynamicMemberNames().ToArray(), + CurrentPivot, + x => TableControl.GetDynamicMember(row, x), + AllDataFields, + AllKeyFields + ); + + if (res == null) + return false; + + var (destCollection, destPivot) = res; + + if (destCollection != SelectedCollectionNode.CollectionNameWithPrefix) + throw new NotImplementedException("destCollection != Collection"); + + try + { + _pivotRefreshing = true; + await InvokeAsync(StateHasChanged); + + await RunPivot(destPivot, Filter); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); + } + finally + { + _pivotRefreshing = false; + await InvokeAsync(StateHasChanged); + } + return true; + } + + private async Task ShowDefaultDocumentInternal(KeyValuePair[] fields) + { + var filter = ExtraFilter; + var doc = await PivotService.GetDocumentAsync(_currentCollection!.CollectionNameWithPrefix, fields, filter); + + await ModalDialogUtils.ShowTextDialog( + Modal, + "Pivot", + doc, + "Press F11 to enter fullscreen mode. Use ESC to exit it." + ); + } + + private bool IsRefreshEnabled => + ! IsReadOnly + && !string.IsNullOrWhiteSpace(SelectedCollectionNode?.CollectionNameWithPrefix) + && SelectedPivotNode?.Pivot != null + ; + + private async Task RunPivotInternal(bool transpose) + { + if (!IsRefreshEnabled) + return; + + _log.Debug($"OnRefreshPivot Collection=\"{SelectedCollectionNode?.CollectionNameWithPrefix}\" Pivot=\"{SelectedPivotNode?.Pivot?.Name}\" ExtraFilter=\"{ExtraFilter}\""); + + try + { + var pivot = SelectedPivotNode?.Pivot; + if (pivot == null) + { + await ModalDialogUtils.ShowInfoDialog( + Modal, + "Pivot", + "Pivot is not found.", + new() + { + {"Pivot" , SelectedPivotNode?.Pivot?.Name}, + {"Collection" , SelectedCollectionNode?.CollectionNameWithPrefix }, + {"ExtraFilter", ExtraFilter?.ToString() } + }); + return; + } + + // clear previous back/forward stack + Navigation?.Clear(); + + _pivotRefreshing = true; + await InvokeAsync(StateHasChanged); + + await RunPivotInternal(pivot, Filter, transpose: transpose); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); + } + finally + { + _pivotRefreshing = false; + await InvokeAsync(StateHasChanged); + } + } + + public async Task RunPivot(PivotDefinition pivotDef, FilterExpressionTree.ExpressionGroup? userFilter, bool addToNavigation = true, bool transpose = false) + { + if (IsReadOnly) + return; + + try + { + _pivotRefreshing = true; + await InvokeAsync(StateHasChanged); + + await RunPivotInternal(pivotDef, userFilter, addToNavigation, transpose); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); + } + finally + { + _pivotRefreshing = false; + await InvokeAsync(StateHasChanged); + } + } + + /* + ___________________________________________________________________________________________________________ + + + + _____ _____ _ _ + | __ \ | __ (_) | | + | |__) | _ _ __ | |__) |__ _____ | |_ + | _ / | | | '_ \| ___/ \ \ / / _ \| __| + | | \ \ |_| | | | | | | |\ V / (_) | |_ + |_| \_\__,_|_| |_|_| |_| \_/ \___/ \__| + + + ___________________________________________________________________________________________________________ + */ + private async Task RunPivotInternal( + PivotDefinition pivotDef, + FilterExpressionTree.ExpressionGroup? userFilter, + bool addToNavigation = true, + bool transpose = false +) + { + // userFilter is a combination of drilldown filters and pivot-defined filter + // i.e. it excluding an ExtraFilter (the one that set in the UI) + + if (IsReadOnly) + return; + if (pivotDef == null) + throw new ArgumentNullException(nameof(pivotDef)); + if (!await WaitForSelectedCollection()) + throw new ApplicationException("SelectedCollectionNode is null"); + + var sw = Stopwatch.StartNew(); + var p = Process.GetCurrentProcess(); + var memBefore = p.WorkingSet64; + + pivotDef = pivotDef.Clone(); + _descriptorsCache.Clear(); + + if (_prevCollection != SelectedCollectionNode!.CollectionNameWithPrefix) + { + _descriptors = SelectedCollectionNode.ColumnDescriptors; + _fieldTypes = SelectedCollectionNode.FieldTypes; + _prevCollection = SelectedCollectionNode.CollectionNameWithPrefix; + } + + var fieldTypes = GetAllFields(); + + pivotDef.Filter = CombineFilters( pivotDef.Filter, userFilter, fieldTypes); + + var data = await PivotService.PivotAsync(SelectedCollectionNode.CollectionNameWithPrefix, pivotDef, ExtraFilter, !UseCache); + + if (data.Count == PivotMaxReturnedRows) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Pivot Results Truncated", + $"Pivot results are limited to returning a maximum of {PivotMaxReturnedRows} rows"); + } + + CurrentPivot = pivotDef; + + UseCache = true; // reset UseCache + _currentCollection = SelectedCollectionNode; + + if ((pivotDef.RenameColumn?.Count ?? 0) > 0) + RenameColumns(data, pivotDef.RenameColumn!, false); + + data = Make2DPivot(CurrentPivot, data); + if (transpose && data.Count > 0 && data.Get(0, 0)?.ToString()?.Equals("No results") != true) + data = Transpose(data); + + PivotData = data; + _totals.Clear(); + _filteredRows = []; + + if (addToNavigation) + { + Navigation?.Add(pivotDef, CreateNavigationUnit(SelectedPivotNode!, userFilter, Pivots)); + } + + sw.Stop(); + + LastRefresh = DateTime.Now; + LastRefreshElapsed = sw.Elapsed; + + await LastRefreshChanged .InvokeAsync(LastRefresh); + await IsExportEnabledChanged .InvokeAsync(IsExportEnabled); + + p = Process.GetCurrentProcess(); + var memAfter = p.WorkingSet64; + + _log.Debug($"Received Rows={PivotData.Count} Pivot=\"{pivotDef.Name}\" Filter=\"{userFilter}\" MemBefore={NumbersUtils.ToHumanReadable(memBefore)} " + + $"MemAfter={NumbersUtils.ToHumanReadable(memAfter)} " + + $"MemDiff={NumbersUtils.ToHumanReadable(memAfter - memBefore)} User=\"{PivotService.User}\""); + + } + + private async Task WaitForSelectedCollection() + { + // SelectedCollectionNode can be not set yet if the parent component is setting it + // asynchronously. Wait for a while. + // This is actually a race condition. Parent initialing Collections loading in background while in the code + // we already think that Collections are loaded and SelectedCollectionNode is set. + if (SelectedCollectionNode != null) + return true; + + var timeout = TimeSpan.FromSeconds(10); + try + { + var cts = new CancellationTokenSource(timeout); + while (SelectedCollectionNode == null && !cts.IsCancellationRequested) + await Task.Delay(100, cts.Token); + } + catch (OperationCanceledException) + { + // ignore + } + return SelectedCollectionNode != null; + } + + private string CombineFilters(string pivotFilter, FilterExpressionTree.ExpressionGroup? userFilter, Dictionary fieldTypes) + { + if (string.IsNullOrWhiteSpace(pivotFilter)) + return userFilter?.ToJson(fieldTypes) ?? ""; + + if (userFilter?.IsEmpty ?? true) + return pivotFilter; + + var baseFilter = FilterExpressionTree.ParseJson(pivotFilter); + var resTree = new FilterExpressionTree.ExpressionGroup() + { + Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And, + }; + resTree.Children.Add(baseFilter); + resTree.Children.Add(userFilter); + + return resTree.ToJson(fieldTypes); + } + + private void RenameColumns(IPivotedData data, Dictionary pivotDefRenameColumn, bool capitaliseColumns) + { + if ( data is not ArrayBasedPivotData d ) + throw new ApplicationException("ArrayBasedPivotData expected"); + + var rx = pivotDefRenameColumn.Select(x => new {Rx = new Regex(x.Key), Src=x.Key, Dest = x.Value}).ToArray(); + + d.UpdateHeaders(header => + { + foreach (var item in rx) + { + var m = item.Rx.Match(header); + if (!m.Success) + continue; + var dest = Regex.Replace(header, item.Src, item.Dest); + return dest; + } + + if (capitaliseColumns) + { + if (char.IsLower(header[0]) && header.Length > 1) + { + return header[0].ToString().ToUpper() + header[1..]; + } + } + + return header; + }); + } + + private static NavigationUnit DefaultCreateNavigationUnit(GroupedPivot pivot, FilterExpressionTree.ExpressionGroup? userFilter, List pivots ) + => new () + { + Filter = userFilter, + Pivots = pivots, + SelectedPivotNode = pivot + }; + + private class TransposedData : IPivotedData + { + private readonly IPivotedData _originalData; + + private readonly string[] _invertedOrigHeaders; + + public TransposedData(IPivotedData originalData) + { + _originalData = originalData; + + _invertedOrigHeaders = _originalData.Headers.ToArray(); + + Headers = new [] {_invertedOrigHeaders[0]} + .Concat( + Enumerable.Range(0, originalData.Count) + .Select(x => originalData.Get(0, x)?.ToString() ?? "") + ) + .ToList(); + + Id = $"{originalData.Id}-Transposed"; + } + + public string Id { get; set; } + + public IReadOnlyCollection Headers { get; } + + public int Count => _originalData.Headers.Count-1; + + public object? Get(int col, int row) + { + if (col == 0) + { + if (row < 0 || row >= _invertedOrigHeaders.Length - 1) + return "???"; + return _invertedOrigHeaders[row + 1]; + } + + return _originalData.Get(row+1, col-1); + } + + public Type GetColumnType(int col) => typeof(double); + + public IPivotedData Filter(Func filter) => new TransposedData(_originalData.Filter(filter)); + } + + private IPivotedData Transpose(IPivotedData data) => new TransposedData(data); + + private async void NavigateInternal(PivotDefinition def, NavigationUnit data) + { + try + { + if (IsReadOnly) + return; + try + { + _pivotRefreshing = true; + + Filter = data.Filter ?? new FilterExpressionTree.ExpressionGroup(); + SelectedPivotNode = data.SelectedPivotNode; + + await InvokeAsync(StateHasChanged); + + await RunPivot(def, null, false); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); + } + finally + { + _pivotRefreshing = false; + await InvokeAsync(StateHasChanged); + } + } + catch (Exception e) + { + _log.Error(e.Message, e); + } + } + + + private readonly Dictionary _descriptorsCache = []; + private readonly Dictionary _fieldDescriptorsCache = []; + + private readonly PivotColumnDescriptor _defaultPivotColumnDescriptor = new() + { + Format = "N0", + NameRegexString = ".*", + Background = Color.FromName(Night.Background), + AlternateBackground = Color.FromName(Night.BackgroundLight) + }; + + private TableControl.SortModeType SortMode { get; set; } + private string? SortColumn { get; set; } + + private PivotColumnDescriptor GetColumnDescriptorInternal(string columnName) + { + if (_descriptorsCache.TryGetValue(columnName, out var desc)) + return desc; + + _descriptors ??= SelectedCollectionNode?.ColumnDescriptors; + + desc = GetColumnDescriptor(columnName) + ?? _descriptors?.FirstOrDefault(x => x.NameRegex.IsMatch(columnName)) + ?? _defaultPivotColumnDescriptor; + + _descriptorsCache[columnName] = desc; + return desc; + } + + private PivotFieldDescriptor? GetFieldDescriptorInternal(string columnName) + { + if (_fieldDescriptorsCache.TryGetValue(columnName, out var desc)) + return desc; + + if ( SelectedCollectionNode == null ) + return null; + + _fieldTypes ??= SelectedCollectionNode.FieldTypes; + + desc = GetFieldDescriptor?.Invoke(columnName); + if (desc == null) + _fieldTypes?.TryGetValue(columnName, out desc); + + _fieldDescriptorsCache[columnName] = desc; + return desc; + } + + + private static IPivotedData Make2DPivot(PivotDefinition def, IPivotedData data) + { + if (!def.Make2DPivot) + return data; + + + var rhArray = (def.Pivot2DRows ?? []) + .Select(x => x.Replace("\r", "").Trim()) + .ToArray() + ; + + var rowHeaders = new HashSet(rhArray); + + if ((def.Pivot2DRows?.Count ?? 0) == 0 + || rowHeaders.Contains(def.Pivot2DColumn) + || rowHeaders.Contains(def.Pivot2DData) + || def.Pivot2DColumn == def.Pivot2DData) + return data; + + try + { + var data2d = new TransposedPivotData(data, def.Pivot2DColumn, rhArray, def.Pivot2DData, CancellationToken.None); + return data2d; + } + catch (Exception) + { + return data; + } + } + + private Dictionary GetAllFields() + { + if (SelectedCollectionNode?.FieldTypes != null) + return SelectedCollectionNode.FieldTypes + .ToDictionary( + x => x.Key, + x => x.Value.Type + ); + + var res = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if ((AllKeyFields?.Count ?? 0) > 0) + { + foreach (var k in AllKeyFields!.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(string); + } + } + + if ((AllDataFields?.Count ?? 0) > 0) + { + foreach (var k in AllDataFields!.Where(k => !res.ContainsKey(k))) + { + res[k] = typeof(double); + } + } + + // if ((_pivotData?.Length ?? 0) > 0) + // { + // foreach (var k in _pivotData[0].GetDynamicMemberNames()) + // { + // if (!res.ContainsKey(k)) + // res[k] = typeof(double); + // } + // } + + return res; + } + + private async Task CopyCsvInternal() + { + if (!IsExportEnabled) + return; + + try + { + var csv = _pivotRows![0].PivotData.CopyToCsv(); + await Js.InvokeVoidAsync("DashboardUtils.CopyToClipboard", csv); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error copying pivot data", e); + } + } + + private async Task ExportCsvInternal(string destFileName) + { + if (!IsExportEnabled) + return; + + try + { + await using var writer = new StringWriter(); + _pivotRows![0].PivotData.WriteToCsv(writer); + var data = writer.ToString(); + + var url = await PivotSharingService.ExportToCsv( + destFileName, + data + ); + + @* var url = await DownloadController.GetDownloadLink( *@ + @* _storage, *@ + @* _passwordManager, *@ + @* _singleUseTokenService, *@ + @* fileName => *@ + @* { *@ + @* _pivotRows![0].PivotData.WriteToCsv(fileName); *@ + @* return Task.CompletedTask; *@ + @* }, *@ + @* destFileName *@ + @* ); *@ + + await Js.InvokeVoidAsync("open", $"{NavigationManager.BaseUri}{url}", "_blank"); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error exporting pivot data", e); + } + } + + // ReSharper disable once UnusedMember.Local + private List FilteredRows + { + get => _filteredRows; + set + { + _filteredRows = value; + _totals.Update(_pivotData!, _filteredRows.OfType().Select( x => x.Row).ToArray()); + + if (_filteredRows.Count == 0 || !(CurrentPivot?.MakeLineChart ?? false)) + return; + + _delayedUpdate.Run(UpdateFilteredChart, "UpdateFilteredChart"); + } + } + + private Task UpdateFilteredChart(CancellationToken token) + { + try + { + + if (token.IsCancellationRequested) + return Task.CompletedTask; + + var filteredPivot = new PivotFilteredView(_filteredRows.Cast().ToList()); + + if (token.IsCancellationRequested) + return Task.CompletedTask; + + ChartHelper.UpdateLineChart(CurrentPivot!, filteredPivot, x => _pivotRows![0].GetFormat(x)); + } + catch (Exception) + { + // ignore + } + return InvokeAsync(StateHasChanged); + } + + public static string GetCellStyle(dynamic row, TableColumnControl col) + { + if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) ) + return ""; + + var desc = r.GetColumnDescriptor(col.Field); + if ( desc == null ) + return ""; + + // for some reason comparison with 0x00000 and Color.Black are not working + if ( desc.Background is { R: 0, G: 0, B: 0 }) + return ""; + + return $"background-color:#{desc.Background.A:x2}{desc.Background.R:x2}{desc.Background.G:x2}{desc.Background.B:x2}"; + } + + private string GetCellClassCallback(dynamic row, TableColumnControl col) + { + var cellClass = ""; + + // make a copy as GetFieldDescriptor may take noticeable time + var cp = CurrentPivot; + + if ( cp == null || cp.HighlightTopPercent <= 0.0 ) + return cellClass; + + if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) ) + return cellClass; + + var mm = _totals.TryGet(col.Name); + if ( mm == null ) + return cellClass; + + var desc = r.GetFieldDescriptor(col.Field); + if ( desc != null && desc.Purpose != PivotFieldPurpose.Data ) + return cellClass; + + double val = GetCellValue(row, col.Field); + + cellClass = val switch + { + > 0 when val > mm.MaxValue - mm.MaxValue * cp.HighlightTopPercent / 100.0 => + $"{cellClass} glow-pos", + < 0 when val < mm.MinValue - mm.MinValue * cp.HighlightTopPercent / 100.0 => + $"{cellClass} glow-neg", + _ => cellClass + }; + + // NaN here most likely means string column + if ( !double.IsNaN(val) && IsLastImportant(r, col.Name, mm) ) + cellClass += " glow-last"; + + return cellClass; + } + + private double GetCellValue(PivotRow row, string col) + { + if ( !_shadowHeaders.TryGetValue(col, out var idx) ) + return double.NaN; + + var val = row.PivotData.Get(idx, row.Row); + return val switch + { + double d => d, + int i => i, + _ => double.NaN + }; + } + + private bool IsLastImportant(PivotRow row, string colName, MinMax mm) + { + if ( SortColumn != colName || SortMode != TableControl.SortModeType.DescendingAbsolute) + return false; + + var sum = 0.0; + var target = mm.AbsTotal * CurrentPivot!.HighlightTopPercent / 100.0; + + foreach ( var r in FilteredRows.OfType() ) + { + var val = GetCellValue(r, colName); + if ( double.IsNaN(val) ) + return false; + + sum += Math.Abs(val); + + if ( sum >= target ) + return row.Row == r.Row; + } + + return false; + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotComponent.razor new file mode 100644 index 0000000..18f1383 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotComponent.razor @@ -0,0 +1,139 @@ +@using System.Drawing +@using Rms.Risk.Mango.Pivot.Core + +@if (_pivotRows is { Length: > 0 }) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ + + + @foreach (var field in _pivotRows[0].GetDynamicMemberNames()) + { + + + + } + +} + +@code{ + + [Parameter] + public IPivotedData? PivotData + { + get; + set + { + if (field == value) + return; + + field = value; + _totals.Clear(); + + if (field == null) + { + _pivotRows = []; + InvokeAsync(StateHasChanged); + return; + } + + _shadowHeaders = field.GetColumnPositions(); + + _descriptorsCache.Clear(); + + _pivotRows = Enumerable + .Range(0, field.Count) + .Select(x => new PivotRow(field, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal)) + .ToArray() + ; + + _totals.Update(field); + + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public int Rows { get; set; } = 35; + [Parameter] public bool ShowTotals { get; set; } + + + private PivotRow[] _pivotRows = []; + private MinMaxCache _totals = new(); + private Dictionary _shadowHeaders = []; + + private readonly Dictionary _descriptorsCache = new(); + + private TableControl.SortModeType SortMode { get; set; } + private string? SortColumn { get; set; } + private IMinMaxCache? VisibleTotals => ShowTotals ? _totals : null; + + private readonly PivotColumnDescriptor _defaultPivotColumnDescriptor = new() + { + Format = "N0", + NameRegexString = ".*", + Background = Color.FromName(Night.Background), + AlternateBackground = Color.FromName(Night.BackgroundLight) + }; + + private PivotColumnDescriptor GetColumnDescriptorInternal(string columnName) + { + if (_descriptorsCache.TryGetValue(columnName, out var desc)) + return desc; + + desc = _defaultPivotColumnDescriptor; + + _descriptorsCache[columnName] = desc; + return desc; + } + + private string ColumnClass + { + get + { + //This is a little awkward, usually the table control will auto wrap long headers when they contain spaces, but we have some pivots currently that have + //a combination of long columns with and without spaces. Wrapping looks weird here, so we want to turn off wrapping if we can + //detect any column which has a long name without spaces + + if ( _pivotRows != null && + (_pivotRows[0].GetDynamicMemberNames().Count() <= 16 + || _pivotRows[0].GetDynamicMemberNames().Any( n => n.Length >= 10 && !n.Contains( " " ) ) + ) + ) + { + return "table-nowrap"; + } + + return string.Empty; + } + } + + + private PivotFieldDescriptor? GetFieldDescriptorInternal( string columnName ) => null; + private string GetCellClassCallback( dynamic row, TableColumnControl col ) => ""; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotTable.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotTable.razor new file mode 100644 index 0000000..f966b08 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/ReadOnlyPivotTable.razor @@ -0,0 +1,441 @@ +@using System.Drawing +@using System.Dynamic +@using ChartJs.Blazor +@using ChartJs.Blazor.LineChart +@using Rms.Risk.Mango.Pivot.Core + +@inject IJSRuntime Js +@inject NavigationManager NavigationManager +@inject IPivotSharingService PivotSharingService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + + +
+
+ +
+
+ +@if (_pivotRows is { Length: > 0 } + && (ShowIfEmpty || (!PivotData.Get(0, 0)?.ToString()?.Equals("No results") ?? false))) +{ + @if (!string.IsNullOrEmpty(Title)) + { +

@Title

+ } + + @foreach (var field in _pivotRows[0].GetDynamicMemberNames()) + { + + var headerStyle = HeaderStyles.GetValueOrDefault(field, ""); + + + + + } + +} +
+ +@code{ + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Class { get; set; } = ""; + + [Parameter, EditorRequired] + public IPivotedData PivotData + { + get; + set + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (field == value || value == null) + return; + + field = value; + _totals.Clear(); + + _shadowHeaders = field.GetColumnPositions(); + + _descriptorsCache.Clear(); + _fieldDescriptorsCache.Clear(); + + _pivotRows = Enumerable + .Range(0, field.Count) + .Select(x => new PivotRow(field, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal)) + .ToArray() + ; + + _totals.Update(field); + + if (PivotDef != null) + { + try + { + ChartHelper.UpdateLineChart(PivotDef, field, x => _pivotRows[0].GetFormat(x)); + } + catch (Exception) + { + // ignore + } + } + + InvokeAsync(StateHasChanged); + } + } = new ArrayBasedPivotData([]); + + [Parameter] public PivotDefinition? PivotDef { get; set; } + [Parameter] public GroupedCollection? SelectedCollectionNode { get; set; } + [Parameter] public int Rows { get; set; } = 35; + [Parameter] public Func GetColumnDescriptor { get; set; } = _ => null; + [Parameter] public Func GetFieldDescriptor { get; set; } = _ => null; + [Parameter] public Func> HandleCellClick { get; set; } = (_, _) => Task.FromResult(false); + [Parameter] public string Title { get; set; } = ""; + [Parameter] public bool ShowIfEmpty { get; set; } = true; + [Parameter] public Dictionary HeaderStyles { get; set; } = []; + + [Parameter] + public bool IsExportEnabled + { + get => _pivotRows is { Length: > 0 }; + // ReSharper disable once ValueParameterNotUsed + set + { + // ignore + } + } + [Parameter] public EventCallback IsExportEnabledChanged { get; set; } + + + public Task CopyCsv() => CopyCsvInternal(); + public Task ExportCsv(string destFileName) => ExportCsvInternal(destFileName); + + private PivotRow[] _pivotRows = []; + private readonly MinMaxCache _totals = new(); + private PivotColumnDescriptor[] ? _descriptors; + private Dictionary ? _fieldTypes; + private List _filteredRows = []; + private readonly DelayedExecution _delayedUpdate = new(TimeSpan.FromMilliseconds(1500)); + private Dictionary _shadowHeaders = []; + + private bool ShowTotals => PivotDef?.ShowTotals ?? false; + private IMinMaxCache? VisibleTotals => ShowTotals ? _totals : null; + private LineConfig ChartConfig => ChartHelper.ChartConfig; + private ChartHelperForPivot ChartHelper { get; } = new(); + + private string ChartClass => PivotDef == null || !ChartHelper.IsLineChart(PivotDef, PivotData) + ? "d-none" + : "" + ; + + private string ColumnClass + { + get + { + //This is a little awkward, usually the table control will auto wrap long headers when they contain spaces, but we have some pivots currently that have + //a combination of long columns with and without spaces. Wrapping looks weird here, so we want to turn off wrapping if we can + //detect any column which has a long name without spaces + + if ((_pivotRows[0].GetDynamicMemberNames().Count() <= 16 + || _pivotRows[0].GetDynamicMemberNames().Any(n => n.Length >= 10 && !n.Contains(" ")) + ) + ) + return "table-nowrap"; + else + return string.Empty; + } + } + + private async Task OnCellClick(DynamicObject row, string fieldName) + { + try + { + await HandleCellClick(row, fieldName); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error drilling down into {fieldName}", e); + } + return true; + } + + private readonly Dictionary _descriptorsCache = []; + private readonly Dictionary _fieldDescriptorsCache = []; + + private readonly PivotColumnDescriptor _defaultPivotColumnDescriptor = new() + { + Format = "N0", + NameRegexString = ".*", + Background = Color.FromName(Night.Background), + AlternateBackground = Color.FromName(Night.BackgroundLight) + }; + + private TableControl.SortModeType SortMode { get; set; } + private string? SortColumn { get; set; } + + private PivotColumnDescriptor GetColumnDescriptorInternal(string columnName) + { + if (_descriptorsCache.TryGetValue(columnName, out var desc)) + return desc; + + _descriptors ??= SelectedCollectionNode?.ColumnDescriptors; + + desc = GetColumnDescriptor(columnName) + ?? _descriptors?.FirstOrDefault(x => x.NameRegex.IsMatch(columnName)) + ?? _defaultPivotColumnDescriptor; + + _descriptorsCache[columnName] = desc; + return desc; + } + + private PivotFieldDescriptor? GetFieldDescriptorInternal(string columnName) + { + if (_fieldDescriptorsCache.TryGetValue(columnName, out var desc)) + return desc; + + _fieldTypes ??= SelectedCollectionNode?.FieldTypes; + + desc = GetFieldDescriptor(columnName); + if (desc == null) + _fieldTypes?.TryGetValue(columnName, out desc); + + _fieldDescriptorsCache[columnName] = desc; + return desc; + } + + private async Task CopyCsvInternal() + { + if (!IsExportEnabled) + return; + + try + { + var csv = _pivotRows[0].PivotData.CopyToCsv(); + await Js.InvokeVoidAsync("DashboardUtils.CopyToClipboard", csv); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error copying pivot data", e); + } + } + + private async Task ExportCsvInternal(string destFileName) + { + if (!IsExportEnabled) + return; + + try + { + await using var writer = new StringWriter(); + _pivotRows[0].PivotData.WriteToCsv(writer); + var data = writer.ToString(); + + var url = await PivotSharingService.ExportToCsv( + destFileName, + data + ); + + @* var url = await DownloadController.GetDownloadLink( *@ + @* _storage, *@ + @* _passwordManager, *@ + @* _singleUseTokenService, *@ + @* fileName => *@ + @* { *@ + @* _pivotRows![0].PivotData.WriteToCsv(fileName); *@ + @* return Task.CompletedTask; *@ + @* }, *@ + @* destFileName *@ + @* ); *@ + + await Js.InvokeVoidAsync("open", $"{NavigationManager.BaseUri}{url}", "_blank"); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error exporting pivot data", e); + } + } + + // ReSharper disable once UnusedMember.Local + private List FilteredRows + { + get => _filteredRows; + set + { + _filteredRows = value; + _totals.Update(PivotData, _filteredRows.OfType().Select( x => x.Row).ToArray()); + + if (_filteredRows.Count == 0 || PivotDef is not { MakeLineChart: true }) + return; + + _delayedUpdate.Run(UpdateFilteredChart, "UpdateFilteredChart"); + } + } + + private Task UpdateFilteredChart(CancellationToken token) + { + try + { + + if (PivotDef == null || token.IsCancellationRequested) + return Task.CompletedTask; + + var filteredPivot = new PivotFilteredView(_filteredRows.Cast().ToList()); + + if (token.IsCancellationRequested) + return Task.CompletedTask; + + ChartHelper.UpdateLineChart(PivotDef, filteredPivot, x => _pivotRows[0].GetFormat(x)); + } + catch (Exception) + { + // ignore + } + return InvokeAsync(StateHasChanged); + } + + public static string GetCellStyle(dynamic row, TableColumnControl col) + { + if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) ) + return ""; + + var desc = r.GetColumnDescriptor(col.Field); + if ( desc == null ) + return ""; + + // for some reason comparison with 0x00000 and Color.Black are not working + if ( desc.Background is { R: 0, G: 0, B: 0 }) + return ""; + + return $"background-color:#{desc.Background.A:x2}{desc.Background.R:x2}{desc.Background.G:x2}{desc.Background.B:x2}"; + } + + private string GetCellClassCallback(dynamic row, TableColumnControl col) + { + var cellClass = ""; + + // make a copy as GetFieldDescriptor may take noticeable time + var highlightTopPercent = PivotDef?.HighlightTopPercent ?? 0.0; + + if (highlightTopPercent <= 0.0) + return cellClass; + + if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) ) + return cellClass; + + var mm = _totals.TryGet(col.Name); + if ( mm == null ) + return cellClass; + + var desc = r.GetFieldDescriptor(col.Field); + if ( desc != null && desc.Purpose != PivotFieldPurpose.Data ) + return cellClass; + + double val = GetCellValue(row, col.Field); + + cellClass = val switch + { + > 0 when val > mm.MaxValue - mm.MaxValue * highlightTopPercent / 100.0 => + $"{cellClass} glow-pos", + < 0 when val < mm.MinValue - mm.MinValue * highlightTopPercent / 100.0 => + $"{cellClass} glow-neg", + _ => cellClass + }; + + // NaN here most likely means string column + if ( !double.IsNaN(val) && IsLastImportant(r, col.Name, mm) ) + cellClass += " glow-last"; + + return cellClass; + } + + private double GetCellValue(PivotRow row, string col) + { + if ( !_shadowHeaders.TryGetValue(col, out var idx) ) + return double.NaN; + + var val = row.PivotData.Get(idx, row.Row); + return val switch + { + double d => d, + int i => i, + _ => double.NaN + }; + } + + private bool IsLastImportant(PivotRow row, string colName, MinMax mm) + { + if ( SortColumn != colName || SortMode != TableControl.SortModeType.DescendingAbsolute) + return false; + + var sum = 0.0; + var target = mm.AbsTotal * ( PivotDef?.HighlightTopPercent ?? 0.0 ) / 100.0; + + foreach ( var r in FilteredRows.OfType() ) + { + var val = GetCellValue(r, colName); + if ( double.IsNaN(val) ) + return false; + + sum += Math.Abs(val); + + if ( sum >= target ) + return row.Row == r.Row; + } + + return false; + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/Pivot/SimplePivotComponent.razor b/Rms.Risk.Mango.Pivot.UI/Pivot/SimplePivotComponent.razor new file mode 100644 index 0000000..b16dcc3 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Pivot/SimplePivotComponent.razor @@ -0,0 +1,215 @@ +@using System.Dynamic +@using System.Reflection +@using log4net +@using Rms.Risk.Mango.Pivot.Core.Models +@using Rms.Risk.Mango.Pivot.Core + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ +
+ +@code +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + [Parameter] + public IPivotTableDataSource? PivotService + { + get; + set + { + field = value; + UpdatePivot(); + } + } + + [Parameter] public Func> HandleCellClick { get; set; } = (_, _) => Task.FromResult(false); + [Parameter] public Func GetColumnDescriptor { get; set; } = _ => null; + + [Parameter] public bool EnableAutoUpdate { get; set; } = true; + [Parameter] public int Rows { get; set; } = 35; + + [Parameter] + public bool Transpose + { + get; + set + { + field = value; + UpdatePivot(); + } + } = false; + + [Parameter] public string Class { get; set; } = ""; + + [Parameter] + public string? CollectionName + { + get; + set + { + if (string.Equals(field, value)) + return; + + field = value; + UpdatePivot(); + } + } + + [Parameter] + public string? PivotName + { + get; + set + { + if (string.Equals(field, value)) + return; + + field = value; + UpdatePivot(); + } + } + + [Parameter] + public FilterExpressionTree.ExpressionGroup? ExtraFilter + { + get; + set + { + if (value == null) + { + field = null; + return; + } + + if (field == value) + return; + + field = value; + UpdatePivot(); + } + } + + [Parameter] + public List Collections + { + get; + set + { + field = value; + UpdatePivot(); + } + } = []; + + [Parameter] + public IPivotedData? PivotData + { + get; + set + { + if (field == value) + return; + + field = value; + PivotDataChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } + + [Parameter] public EventCallback PivotDataChanged { get; set; } + + + private readonly FilterExpressionTree.ExpressionGroup _noFilter = new(); + + private PivotTableComponent? PivotTable + { + get; + set + { + field = value; + UpdatePivot(); + } + } + + private async void UpdatePivot() + { + try + { + await InvokeAsync(StateHasChanged); + + if ( !EnableAutoUpdate ) + return; + + await Refresh(); + } + catch (Exception e) + { + _log.Error(e.Message, e); + } + } + + public async Task Refresh() + { + if (PivotName == null || CollectionName == null || PivotService == null || PivotTable == null) + return; + + if (Collections.Count == 0) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var collections = await PivotService.GetAllMeta(token: cts.Token); + Collections.Clear(); + Collections.AddRange(collections); + } + +#pragma warning disable BL0005 + + // WARNING: using hack here to set PivotTable.SelectedXXX parameters. + // Usual way via StateHasChanged is not working due to a race condition + // Somehow it related to multiple SimplePivotComponent on one page + + PivotTable.SelectedPivotNode = new() {Text = "", Pivot = new() }; + PivotTable.SelectedCollectionNode = Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == CollectionName); + if ( PivotTable.SelectedCollectionNode != null ) + PivotTable.SelectedPivotNode = PivotTable.SelectedCollectionNode.Pivots.FirstOrDefault(x => x.Pivot.Name == PivotName); + + await InvokeAsync(StateHasChanged); + + if (PivotTable.SelectedPivotNode == null) + return; +#pragma warning restore BL0005 + + await PivotTable.RunPivot(PivotTable.SelectedPivotNode.Pivot, ExtraFilter ?? _noFilter, addToNavigation: false); + + if ( Transpose ) + { + PivotData = SimpleTranspose.Transpose(PivotData); + } + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/README.md b/Rms.Risk.Mango.Pivot.UI/README.md new file mode 100644 index 0000000..db9e221 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/README.md @@ -0,0 +1,96 @@ +# Rms.Risk.Mango.Pivot.UI + +## Overview + +This package provides a collection of Blazor UI components designed for Pivot functionality and general form support. It aims to deliver a rich user interface experience for data pivoting and interactive forms, primarily for use within the Rms.Risk.Mango ecosystem. + +As indicated by its package tags (`pivot forge ui blazor razor shared form components`), this library is focused on: +- Pivot table and data visualization UI. +- UI elements for "Forge" platform. +- General Blazor and Razor shared components. +- Components to facilitate form building. + +## Features + +* **Pivot UI Components**: Specialized Blazor components for creating and interacting with pivot tables and related data visualizations. +* **Form Support Components**: A comprehensive set of reusable Blazor components to build forms, including inputs, validation helpers, and more. +* **Shared UI Elements**: Common UI elements and utilities to ensure a consistent look and feel. +* **Static Assets**: Includes necessary static assets (CSS/JS) in its `wwwroot` folder, accessible via `_content/Rms.Risk.Mango.Pivot.UI/`. + +## Installation + +To use Rms.Risk.Mango.Pivot.UI in your Blazor project, install the NuGet package. + +Using the .NET CLI: +```bash +dotnet add package Rms.Risk.Mango.Pivot.UI +``` + +Or via the NuGet Package Manager Console in Visual Studio: +``` +Install-Package Rms.Risk.Mango.Pivot.UI +``` + +## Usage + +After installing the package, you will typically need to: + +1. **Import Namespaces**: + Add the relevant `@using` statements to your `_Imports.razor` file or directly in your `.razor` components where the UI elements are used. For example: + ```razor + @using Rms.Risk.Mango.Pivot.UI.Components // Example, actual namespaces may vary + @using Rms.Risk.Mango.Pivot.UI.Forms // Example, actual namespaces may vary + ``` + +2. **Register Services (if applicable)**: + If the library exposes services that need to be registered (e.g., for Blazored.Modal), add them to your `Program.cs` (for .NET 6+ Blazor apps) or `Startup.cs` (for older versions). + ```csharp + // In Program.cs + // builder.Services.AddBlazoredModal(); // Example for a dependency + // Potentially other services from this library + ``` + +3. **Include CSS/JS**: + If the library has global CSS or JS files necessary for its components, you might need to link them in your main layout file (`_Layout.cshtml` or `MainLayout.razor`) or `index.html`. Static assets from this RCL are served from `_content/Rms.Risk.Mango.Pivot.UI/`. + For example: + ```html + + ``` + (Note: The specific file names like `styles.css` are illustrative.) + +## Dependencies + +This package utilizes the following external NuGet packages: + +* **BlazorDateRangePicker**: For date range selection UI. +* **Blazored.Modal**: For modal dialog functionality. +* **ChartJs.Blazor.Fork**: For rendering charts using Chart.js. + +## Bundled Libraries + +The following project dependencies are included directly within this NuGet package (due to `PrivateAssets="All"` setting) and do not need to be installed separately by the consuming project: + +* `Rms.Risk.Mango.Language.csproj` +* `Rms.Risk.Mango.Pivot.Core.csproj` + +This packaging strategy ensures that the specific versions of these core and language libraries used during the development of `Rms.Risk.Mango.Pivot.UI` are the ones used at runtime, simplifying dependency management for consumers. + +## Building the Package + +This project is configured to automatically generate a NuGet package (`.nupkg`) and a symbols package (`.snupkg`) upon building the project in Release configuration. + +Key MSBuild properties for packaging: +```xml +true +true +snupkg +portable +``` + +The project also includes custom MSBuild targets (`IncludeProjectReferencesWithPrivateAssetsAttributeInPackage` and `IncludeReferenceAssemblies`) to manage how project references and reference assemblies are included in the package. This ensures that dependencies marked with `PrivateAssets="All"` are bundled, and a `ref` assembly is correctly packaged to control the public API surface. + +## Contributing + +Please refer to the contribution guidelines of the Rms.Risk.Mango project if you wish to contribute. +Drop a email to mailto:forge@list.db.com. + diff --git a/Rms.Risk.Mango.Pivot.UI/Rms.Risk.Mango.Pivot.UI.csproj b/Rms.Risk.Mango.Pivot.UI/Rms.Risk.Mango.Pivot.UI.csproj new file mode 100644 index 0000000..bba3f49 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Rms.Risk.Mango.Pivot.UI.csproj @@ -0,0 +1,73 @@ + + + Library + true + + true + + true + + + snupkg + + portable + true + true + + This package includes UI for Pivot functionality. Additionally it includes many Blazor components for forms support. + pivot forge ui blazor razor shared form components + README.md + mango.png + + $(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage + $(TargetsForTfmSpecificContentInPackage);IncludeReferenceAssemblies + $(NoWarn);NU5131 + false + + .dll; .exe; .winmd; .json; .pri + + + + + + + + + + + + + + + + + + + + + + + <_projectReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'all'))" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rms.Risk.Mango.Pivot.UI/Services/ActionProgressCallback.cs b/Rms.Risk.Mango.Pivot.UI/Services/ActionProgressCallback.cs new file mode 100644 index 0000000..c6097f9 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/ActionProgressCallback.cs @@ -0,0 +1,22 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public delegate void ActionProgressCallback(string message, bool isError = false, object? extraInfo = null); + diff --git a/Rms.Risk.Mango.Pivot.UI/Services/CobHelper.cs b/Rms.Risk.Mango.Pivot.UI/Services/CobHelper.cs new file mode 100644 index 0000000..2f091ef --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/CobHelper.cs @@ -0,0 +1,98 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Globalization; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class CobHelper +{ + public const string CobFormat = "yyyy-MM-dd"; + + public static DateTime ConvertToDefault(DateTimeOffset? cob) => cob?.Date ?? GetLatestCob(); + + public static DateTimeOffset? ConvertStringToOffset(string? cobStr) + { + if ( !string.IsNullOrWhiteSpace(cobStr) && + ( + DateTime.TryParseExact(cobStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var res) + || DateTime.TryParseExact(cobStr, "dd-MM-yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out res) + || DateTime.TryParseExact(cobStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out res) + || DateTime.TryParseExact(cobStr, "dd-MM-yyyy HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out res) + ) + ) + { + return res; + } + + return null; + } + + public static DateTime ConvertToDefault(string? cobStr) + { + var cob = ConvertStringToOffset( cobStr ); + if ( cob != null ) + return cob.Value.DateTime; + return GetLatestCob(); + } + + public static DateTime GetLatestCob() + { + var timeInLondon = DateUtil.UTCToLondon( DateTime.UtcNow ); + + var mostRecentCob = (timeInLondon + TimeSpan.FromHours(7)).Date - TimeSpan.FromDays(1); + var cob = mostRecentCob; + + // ReSharper disable once PossibleInvalidOperationException + if (cob.DayOfWeek == DayOfWeek.Sunday) + cob = cob - TimeSpan.FromDays(2); + if (cob.DayOfWeek == DayOfWeek.Saturday) + cob = cob - TimeSpan.FromDays(1); + + return cob; + } + + public static List GetLastNCobDates(int n) + { + var latestCob = GetLatestCob(); + var cobDates = new List + { + latestCob + }; + for ( var day = 1; day < n; day++ ) + { + cobDates.Add(WeekdaysBackward(DateOnly.FromDateTime(latestCob), day).ToDateTime(TimeOnly.MinValue)); + } + + return cobDates; + } + + public static bool IsWeekendDay(DateOnly date) + => date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; + + public static DateOnly WeekdaysBackward(DateOnly baseDate, int days) + { + while (days > 0) + { + baseDate = baseDate.AddDays(-1); + if (!IsWeekendDay(baseDate)) + --days; + } + return baseDate; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Services/DatabaseStructureLoader.cs b/Rms.Risk.Mango.Pivot.UI/Services/DatabaseStructureLoader.cs new file mode 100644 index 0000000..61dfaba --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/DatabaseStructureLoader.cs @@ -0,0 +1,563 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class DatabaseStructureLoader +{ + public const string AuditCollection = "dbMango-Audit"; + + [JsonConverter(typeof(StringEnumConverter))] + public enum FieldSorting + { + Asc, Desc, Hashed,Text + } + + public class SyncStructureOptions + { + public bool DryRun { get; set; } = true; + public bool CreateCollections { get; set; } = true; + public bool CreateIndexes { get; set; } = true; + public bool RemoveCollections { get; set; } = true; + public bool RemoveIndexes { get; set; } = true; + } + + public class CollectionStructure + { + public string Name { get; set; } = string.Empty; + public bool IsSharded { get; set; } + public List Indexes { get; init; } = []; + + public override string ToString() + { + var indexStr = string.Join(", ", Indexes.Select(i => i.ToString())); + return $"Collection: {Name}, {(IsSharded ? "sharded, " : "")}Indexes: [{indexStr}]"; + } + } + + public class IndexStructure + { + public string Name { get; set; } = string.Empty; + public List> Key { get; set; } = []; + public bool Unique { get; set; } + public int? ExpireAfterSeconds { get; set; } + + public override string ToString() + { + var keyStr = string.Join(", ", Key.Select(kv => $"{kv.Key}: {kv.Value}")); + return $"Index: {Name}, Key: [{keyStr}], Unique: {Unique}, ExpireAfterSeconds: {ExpireAfterSeconds}"; + } + + /// + /// This method creating Json expected by IndexEditComponent + /// + public string ToJson() + { + var keyObj = new BsonDocument(); + foreach (var kv in Key) + { + object val = kv.Value switch + { + FieldSorting.Asc => 1, + FieldSorting.Desc => -1, + FieldSorting.Hashed => "hashed", + FieldSorting.Text => "text", + _ => 1 + }; + keyObj.Add(kv.Key, BsonValue.Create(val)); + } + + var doc = new BsonDocument + { + { "key", keyObj }, + { "name", Name } + }; + + if (Unique) + doc.Add("unique", true); + if (ExpireAfterSeconds.HasValue) + doc.Add("expireAfterSeconds", ExpireAfterSeconds.Value); + + return doc.ToJson(new() { Indent = true }); + } + } + + public static async Task LoadIndexes( IMongoDbDatabaseAdminService admin, string coll, CancellationToken token) + { + var res = await admin.RunCommand(new() + { + { "listIndexes", coll }, + { "comment", $"Backup structure for {admin.Database}.{coll}" } + }, token); + + var arr = res["cursor"]["firstBatch"].AsBsonArray; + var indexes = new List(); + foreach ( var item in arr ) + { + var doc = item.AsBsonDocument; + var name = doc.GetValue("name", "").AsString; + var index = new IndexStructure + { + Name = name, + Unique = doc.GetValue("unique", false).ToBoolean(), + ExpireAfterSeconds = doc.Contains("expireAfterSeconds") ? doc["expireAfterSeconds"].ToInt32() : null + }; + + if (doc.TryGetValue("key", out var keyDoc) && keyDoc.IsBsonDocument) + { + foreach (var keyElem in keyDoc.AsBsonDocument.Elements) + { + var sorting = + keyElem.Value.IsString && keyElem.Value.AsString == "text" + ? FieldSorting.Text + : keyElem.Value.IsString && keyElem.Value.AsString == "hashed" + ? FieldSorting.Hashed + : keyElem.Value.ToInt32() == 1 + ? FieldSorting.Asc + : FieldSorting.Desc; + index.Key.Add(new(keyElem.Name, sorting)); + } + + // _id is a special case + if ( index.Key is [{ Key: "_id", Value: FieldSorting.Asc or FieldSorting.Desc }_] ) + index.Unique = true; + } + + indexes.Add(index); + } + return indexes.ToArray(); + } + + public static async Task> LoadCollections(IMongoDbDatabaseAdminService db, IMongoDbDatabaseAdminService admin, CancellationToken token) + { + var command = new BsonDocument + { + { "listCollections", 1 }, + }; + + var result = await db.RunCommand(command); + var collections = result["cursor"]?["firstBatch"]?.AsBsonArray; + + var res = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var coll in (collections ?? []).OfType()) + { + var name = coll.GetValue("name", "").AsString; + if (name == AuditCollection || res.ContainsKey(name)) + continue; // Skip audit collection and already processed collections + + var indexes = await LoadIndexes(db, name, token); + + var collection = new CollectionStructure { Name = name }; + res[name] = collection; + collection.Indexes.AddRange(indexes); + } + + // Check if collections are sharded in parallel as it can be a lengthy operation + var tasks = res.Values.Select(async c => + { + c.IsSharded = await admin.IsSharded(db.Database, c.Name); + return c; + }); + + await Task.WhenAll(tasks); + + return res.Values + .OrderBy(x => x.Name) + .ToList() + ; + } + + public enum DiffType + { + Add, Remove, Modify + } + + public class CollectionStructureDiff : CollectionStructure + { + public DiffType Type { get; set; } = DiffType.Add; + } + + public class IndexStructureDiff : IndexStructure + { + public DiffType Type { get; set; } = DiffType.Add; + } + + + + public class StructureDifference + { + public List ToRemove { get; } = []; + public List ToAdd { get; } = []; + public List ToBeSharded { get; } = []; + public List ToBeUnSharded { get; } = []; + } + + public static StructureDifference GetStructureDifference(List currentCollections, List targetCollections) + { + var difference = new StructureDifference(); + + // Find collections to remove + foreach (var current in currentCollections + .Where(current => targetCollections.All(t => t.Name != current.Name)) + ) + { + // Collection does not exist in target, add to removal list + difference.ToRemove.Add(new () { Name = current.Name, Type = DiffType.Remove }); + } + + // Find collections to add/modify + foreach (var target in targetCollections) + { + if (currentCollections.All(c => c.Name != target.Name)) + { + // Collection does not exist in current, add to addition list + var newTarget = new CollectionStructureDiff + { + Name = target.Name, + Type = DiffType.Add, + Indexes = target.Indexes.Select(x => new IndexStructureDiff + { + Name = x.Name, + Key = x.Key.ToList(), + Unique = x.Unique, + ExpireAfterSeconds = x.ExpireAfterSeconds, + Type = DiffType.Add + }).ToList() + }; + difference.ToAdd.Add(newTarget); + } + else + { + // Check indexes for existing collections + var current = currentCollections.First(c => c.Name == target.Name); + var indexesToRemove = GetIndexesToRemove(current, target); + var indexesToAdd = GetIndexesToAdd(current, target); + + // Add indexes to remove + if (indexesToRemove.Count > 0) + { + difference.ToRemove.Add(new() + { + Name = current.Name, + Type = DiffType.Modify, + Indexes = indexesToRemove.Select(x => new IndexStructureDiff + { + Name = x.Name, + Key = x.Key.ToList(), + Unique = x.Unique, + ExpireAfterSeconds = x.ExpireAfterSeconds, + Type = DiffType.Remove + }).ToList() + }); + } + + // Add indexes to add + if (indexesToAdd.Count > 0) + { + difference.ToAdd.Add(new() + { + Name = target.Name, + Type = DiffType.Modify, + Indexes = indexesToAdd.Select(x => new IndexStructureDiff + { + Name = x.Name, + Key = x.Key.ToList(), + Unique = x.Unique, + ExpireAfterSeconds = x.ExpireAfterSeconds, + Type = DiffType.Add + }).ToList() + }); + } + } + } + + + foreach (var (target, source) in targetCollections + .Select(target => (Target : target, Source: currentCollections.FirstOrDefault(c => c.Name == target.Name))) + .Where(x => x.Source != null && x.Source.IsSharded != x.Target.IsSharded ) + ) + { + if ( source!.IsSharded ) + difference.ToBeUnSharded.Add( target.Name ); + else + difference.ToBeSharded.Add( target.Name ); + } + + return difference; + } + + + private static List GetIndexesToRemove(CollectionStructure current, CollectionStructure target) + { + var toRemove = new List(); + var targetIndexes = target.Indexes.ToDictionary(i => i.Name); + // Iterate through current indexes + foreach (var currentIndex in current.Indexes) + { + // Check if the index exists in the target collection + if (!targetIndexes.ContainsKey(currentIndex.Name)) + { + // Index does not exist in target, add to removal list + toRemove.Add(new() + { + Name = currentIndex.Name, + Key = currentIndex.Key.ToList(), + Unique = currentIndex.Unique, + ExpireAfterSeconds = currentIndex.ExpireAfterSeconds, + Type = DiffType.Remove + }); + } + } + return toRemove; + } + + private static List GetIndexesToAdd(CollectionStructure current, CollectionStructure target) + { + var indexesToAdd = new List(); + + // Create a dictionary of current indexes for quick lookup + var currentIndexes = current.Indexes.ToDictionary(i => i.Name); + + // Iterate through target indexes + foreach (var targetIndex in target.Indexes) + { + // Check if the index exists in the current collection + if (!currentIndexes.TryGetValue(targetIndex.Name, out var currentIndex)) + { + // Index does not exist, add it to the list + indexesToAdd.Add(new() + { + Name = targetIndex.Name, + Key = targetIndex.Key.ToList(), + Unique = targetIndex.Unique, + ExpireAfterSeconds = targetIndex.ExpireAfterSeconds, + Type = DiffType.Add + }); + continue; + } + + // Compare index properties to determine if it needs to be added + if (!AreIndexesEquivalent(currentIndex, targetIndex)) + { + // Index exists but is different, add it to the list + indexesToAdd.Add(new() + { + Name = targetIndex.Name, + Key = targetIndex.Key.ToList(), + Unique = targetIndex.Unique, + ExpireAfterSeconds = targetIndex.ExpireAfterSeconds, + Type = DiffType.Modify // or DiffType.Add based on your logic + }); + } + } + + return indexesToAdd; + } + + private static bool AreIndexesEquivalent(IndexStructure current, IndexStructure target) + { + // Compare keys + if (current.Key.Count != target.Key.Count || + !current.Key.SequenceEqual(target.Key)) + { + return false; + } + + // Compare other properties + return current.Unique == target.Unique && + current.ExpireAfterSeconds == target.ExpireAfterSeconds; + } + + public static List ParseCollections(string json) + { + return JsonUtils.FromJson>(json) ?? new List(); + } + + public static async Task> SyncStructure(IMongoDbDatabaseAdminService admin, StructureDifference difference, SyncStructureOptions? options = null) + { + options ??= new () + { + DryRun = false, + CreateCollections = true, + CreateIndexes = true, + RemoveCollections = true, + RemoveIndexes = true + }; + + var progress = new List<(bool Success, string Message)>(); + // Create missing collections + foreach (var collection in difference.ToAdd.OfType()) + { + try + { + if ( collection.Type == DiffType.Add ) + { + if ( options.CreateCollections ) + { + if ( !options.DryRun ) + { + var createCollectionCommand = new BsonDocument + { + { "create", collection.Name } + }; + + await admin.RunCommand(createCollectionCommand); + } + progress.Add((true, $"{collection.Name}: Collection created")); + } + } + + foreach (var index in collection.Indexes.OfType()) + { + try + { + if (!options.CreateIndexes) + continue; + + if ( !options.DryRun) + { + await CreateIndexes(admin, collection.Name, [index]); + } + progress.Add((true, $"{collection.Name}: Index {(index.Type == DiffType.Add ? "created":"updated")} '{index.Name}'")); + } + catch (Exception e) + { + progress.Add((false, $"{collection.Name}: Failed to {(index.Type == DiffType.Add ? "create":"update")} index '{index.Name}': {e.Message}")); + continue; + } + } + } + catch (Exception ex) + { + progress.Add((false, $"{collection.Name}: Failed to create collection: {ex.Message}")); + continue; + } + } + + // Remove indexes and collections marked as ToRemove + foreach (var collection in difference.ToRemove) + { + try + { + if ( collection.Type != DiffType.Remove) + { + // just delete specified indexes + foreach (var index in collection.Indexes) + { + try + { + if (!options.RemoveIndexes) + continue; + + if ( !options.DryRun ) + { + var dropIndexCommand = new BsonDocument + { + { "dropIndexes", collection.Name }, + { "indexes", index.Name } + }; + + await admin.RunCommand(dropIndexCommand); + } + progress.Add((true, $"Index '{index.Name}' on collection '{collection.Name}' removed.")); + } + catch (Exception e) + { + progress.Add((false, $"Failed to remove index '{index.Name}' on collection '{collection.Name}': {e.Message}")); + continue; + } + } + } + else + { + // delete the whole collection + if (options.RemoveCollections) + { + if (!options.DryRun) + { + var dropCollectionCommand = new BsonDocument + { + { "drop", collection.Name } + }; + + await admin.RunCommand(dropCollectionCommand); + } + progress.Add((true, $"Collection '{collection.Name}' and its indexes removed.")); + } + } + } + catch (Exception ex2) + { + progress.Add((false, $"Failed to {(collection.Type == DiffType.Remove ? "remove" : "remove indexes for" )} collection '{collection.Name}': {ex2.Message}")); + continue; + } + } + + return progress; + } + + public static async Task CreateIndexes( + IMongoDbDatabaseAdminService admin, + string collection, + IndexStructure[] indexes, + CancellationToken token = default + ) + { + var createIndexCommand = new BsonDocument + { + { "createIndexes", collection }, + { "indexes", new BsonArray + ( + indexes.Select( index => + // Create the index document + { + var d = new BsonDocument + { + { "key", new BsonDocument( + index.Key + .Select(kvp => new BsonElement(kvp.Key, kvp.Value switch + { + FieldSorting.Asc => 1, + FieldSorting.Desc => -1, + FieldSorting.Hashed => "hashed", + FieldSorting.Text => "text", + _ => throw new InvalidOperationException($"Unknown field sorting: {kvp.Value}") + }))) }, + { "name", index.Name }, + { "unique", index.Unique } + }; + + if ( index.ExpireAfterSeconds.HasValue ) + d["expireAfterSeconds"] = index.ExpireAfterSeconds.Value; + return d; + } + ) + ) + } + }; + + + await admin.RunCommand(createIndexCommand, token); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/DateUtil.cs b/Rms.Risk.Mango.Pivot.UI/Services/DateUtil.cs new file mode 100644 index 0000000..c45b23d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/DateUtil.cs @@ -0,0 +1,68 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public class DateUtil +{ + public static int DateToIntDate(DateTime date) + { + var dateStr = date.ToString("yyyy-MM-dd"); + var tokens = dateStr.Split('-'); + var joined = tokens[0] + tokens[1] + tokens[2]; + return int.Parse(joined); + } + + public static readonly DateTime UnixEpoch = new( + 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static DateTime FromMillisecondsSinceUnixEpoch(long milliseconds) => UnixEpoch.AddMilliseconds(milliseconds); + + public static ulong ToMillisecondsSinceUnixEpoch(DateTime dateTimeUtc) => (ulong)(dateTimeUtc - UnixEpoch).TotalMilliseconds; + + public static readonly DateTime LocalEpoch = new( + 1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + + public static DateTime FromMillisecondsSinceLocalEpoch(long milliseconds) => LocalEpoch.AddMilliseconds(milliseconds); + + public static long ToMillisecondsSinceLocalEpoch(DateTime dateTime) => (long)(dateTime - LocalEpoch).TotalMilliseconds; + + private static readonly TimeZoneInfo _londonTzInfo = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"); + + public static DateTime LondonToUTC( DateTime timeInLondonTz ) + { + Debug.Assert( timeInLondonTz.Kind == DateTimeKind.Unspecified ); + return TimeZoneInfo.ConvertTimeToUtc( timeInLondonTz, _londonTzInfo ); + } + + public static DateTime UTCToLondon( DateTime utcTime ) + { + Debug.Assert( utcTime.Kind == DateTimeKind.Utc ); + + var timeInLondonTz = TimeZoneInfo.ConvertTimeFromUtc( utcTime, _londonTzInfo ); + return timeInLondonTz; + } + + /// + /// Round down to the nearest second + /// + public static DateTime RoundTime( DateTime d ) => new(d.Year,d.Month,d.Day,d.Hour,d.Minute,d.Second, d.Kind); + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/DelayedExecution.cs b/Rms.Risk.Mango.Pivot.UI/Services/DelayedExecution.cs new file mode 100644 index 0000000..4cdcf41 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/DelayedExecution.cs @@ -0,0 +1,123 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Reflection; +using log4net; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +/// +/// Execute action only after a delay. Any subsequent calls will cancel the previous action and restart the timer. +/// +public class DelayedExecution +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + // ReSharper disable once FieldCanBeMadeReadOnly.Global + // ReSharper disable once ConvertToConstant.Global + // ReSharper disable once MemberCanBePrivate.Global + public static int DefaultDelayMSec = 3000; + + private readonly TimeSpan _delay; + private readonly Lock _syncObject = new(); + private CancellationTokenSource? _tokenSource; + + /// + /// Constructor + /// + /// + public DelayedExecution( TimeSpan delay = default) + { + if ( delay == TimeSpan.Zero ) + delay = TimeSpan.FromMilliseconds( DefaultDelayMSec ); + _delay = delay; + } + + /// + /// Execute action only after a delay. Any subsequent calls will cancel the previous action and restart the timer. + /// + public void Run( Action action, string description, CancellationToken token = default ) + { + lock ( _syncObject ) + { + if ( _tokenSource != null ) + { + _tokenSource.Cancel(); + _tokenSource = null; + } + + _tokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + var newToken = _tokenSource.Token; + Task.Run( async () => + { + try + { + await Task.Delay( _delay, newToken ); + if ( newToken.IsCancellationRequested ) + return; + action(newToken); + } + catch (TaskCanceledException) + { + // ignore + } + catch (Exception ex) + { + _log.Error($"Delayed execution Task=\"{description}\" failed with: {ex.Message}", ex); + } + }, newToken ); + } + } + + /// + /// Execute action only after a delay. Any subsequent calls will cancel the previous action and restart the timer. + /// + public void Run( Func action, string description, CancellationToken token = default ) + { + lock ( _syncObject ) + { + if ( _tokenSource != null ) + { + _tokenSource.Cancel(); + _tokenSource = null; + } + + _tokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + var newToken = _tokenSource.Token; + Task.Run( async () => + { + try + { + await Task.Delay(_delay, newToken); + if (newToken.IsCancellationRequested) + return; + await action(newToken); + } + catch (TaskCanceledException) + { + // ignore + } + catch (Exception ex) + { + _log.Error($"Delayed execution Task=\"{description}\" failed with: {ex.Message}", ex); + } + }, newToken ); + } + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/DumyDataSource.cs b/Rms.Risk.Mango.Pivot.UI/Services/DumyDataSource.cs new file mode 100644 index 0000000..3655fe2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/DumyDataSource.cs @@ -0,0 +1,54 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public class DummyDataSource : IPivotTableDataSource, IPivotTableDataSourceMetaProvider +{ + public Task InitAsync(string collectionName, bool skipCache, CancellationToken token = default) => Task.CompletedTask; + public string SourceId => "Dummy"; + public string Prefix => ""; + + public string User { get; set; } = string.Empty; + + public Task> GetAllMeta(bool force = false, CancellationToken token = default) => Task.FromResult(new List()); + public Task GetCollectionsAsync(CollectionType includeMeta = CollectionType.All, CancellationToken token = default) => Task.FromResult([]); + public Task GetCobDatesAsync(string collectionName, bool force = false, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Task GetDepartmentsAsync(string collectionName, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Task<(string, string)[]> GetDesksWithDepartmentAsync(string collectionName, CancellationToken token = default) => Task.FromResult(Array.Empty<(string, string)>()); + + public Task GetKeyFieldsAsync(string collectionName, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Task GetDrilldownKeyFieldsAsync(string collectionName, PivotFieldPurpose keyLevel, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Task GetDataFieldsAsync(string collectionName, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Task GetColumnDescriptorsAsync(string collectionName, CancellationToken token = default) => Task.FromResult(Array.Empty()); + public Dictionary GetFieldTypes(string collectionName) => []; + public Task GetDrilldownAsync(string collectionName, string name, string value = "\"\"", bool equals = false, CancellationToken token = default) => Task.FromResult(""); + public Task PivotAsync( + string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, bool skipCache, + string? userName = null, int maxFetchSize = -1, CancellationToken token = default) => Task.FromResult((IPivotedData)new ArrayBasedPivotData( Array.Empty() )); + public Task> GetPivotsAsync(string collectionName, IPivotTableDataSource.PivotType pivotType, + string? userName = null, CancellationToken token = default) => Task.FromResult( new List() ); + public Task UpdatePredefinedPivotsAsync(string collectionName, IEnumerable pivots, bool predefined = false, string? userName = null, CancellationToken token = default) => Task.CompletedTask; + public Task GetQueryTextAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default) => Task.FromResult(""); + public Task GetDocumentAsync(string collectionName, KeyValuePair[] keys, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default) => Task.FromResult(""); + public Task GetDocumentAsync(string collectionName, FilterExpressionTree.ExpressionGroup extraFilter, CancellationToken token = default) => Task.FromResult(""); + public Task DeletePivotAsync(string collectionName, string pivotName, string groupName, string? userName, CancellationToken token = default) => Task.CompletedTask; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/ExceptionHelpers.cs b/Rms.Risk.Mango.Pivot.UI/Services/ExceptionHelpers.cs new file mode 100644 index 0000000..32d948e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/ExceptionHelpers.cs @@ -0,0 +1,105 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text; +using System.Text.RegularExpressions; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class ExceptionHelpers +{ + public static string ToMarkdown(this Exception ex) + { + var sb = new StringBuilder(); + Append(ex, sb); + return sb.ToString(); + } + + private static readonly Regex _stackTraceRegex = new( + @"^\s*at\s+(?[\w\.']+)\.(?[\w`]+)(?(\[[^)]*\])?(\([^)]*\))?)(\s+in\s+(?.*)\:line\s+(?\d+))?", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private record ResultRec(string ClassName, string MethodName, string Args, string? FileName, string? LineNumber); + + private static ResultRec? ExtractStackTraceInfo(string stackTraceLine) + { + var match = _stackTraceRegex.Match(stackTraceLine); + if (match.Success) + { + var className = match.Groups["ClassName"].Value; + var methodName = match.Groups["MethodName"].Value; + var args = match.Groups["Args"].Value; + var fileName = match.Groups["FileName"].Value; + var lineNumber = match.Groups["LineNumber"].Value; + return new (className, methodName, args, fileName, lineNumber); + } + return null; + } + + private static void Append(Exception ex, StringBuilder sb) + { + while (true) + { + if (ex is AggregateException aggEx) + { + foreach (var inner in aggEx.InnerExceptions) + { + Append(inner, sb); + } + } + else + { + sb.AppendLine($"### {ex.Message}"); + sb.AppendLine($"#### {ex.GetType()}"); + + var stack = (ex.StackTrace ?? "") + .Replace("`", "'") + .Replace("\r", "") + .Split("\n", StringSplitOptions.RemoveEmptyEntries); + + foreach( var line in stack) + { + AppendStackLine(line, sb); + } + } + + if (ex.InnerException != null) + { + ex = ex.InnerException; + continue; + } + + break; + } + } + + public static void AppendStackLine(string line, StringBuilder sb) + { + var res = ExtractStackTraceInfo(line); + if (res == null) + { + sb.AppendLine($"* {line}"); + return; + } + + if (string.IsNullOrWhiteSpace(res.FileName)) + sb.AppendLine($"* *{res.ClassName}.{res.MethodName}{res.Args}*"); + else + sb.AppendLine($"* *{res.ClassName}*.`{res.MethodName}`*{res.Args}* `{res.FileName}` {res.LineNumber}"); + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Services/IPivotSharingService.cs b/Rms.Risk.Mango.Pivot.UI/Services/IPivotSharingService.cs new file mode 100644 index 0000000..650bf13 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/IPivotSharingService.cs @@ -0,0 +1,52 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public class SharedPivotDef +{ + public required string SharedBy { get; init; } + public required DateTime SharedAtUTC { get; init; } + public required string Collection { get; init; } + + public required PivotDefinition PivotDef { get; init; } + + // NewtonsoftJson can't deserialize interface, so using custom serialization instead + public string? /*FilterExpressionTree.ExpressionGroup*/ ExtraFilter { get; init; } +} + +public interface IPivotSharingService +{ + /// + /// Exports the pivot definition to a CSV file. + /// + /// URL that can be used for file retrieval + Task ExportToCsv(string destFileName, string fileContents); + /// + /// Publishes a pivot definition to the shared location. Other user can extract it using method. + /// + /// sharedPivotId for + Task SharePivot(SharedPivotDef def); + /// + /// Retrieves a shared pivot definition by its ID. See . + /// + /// ID from + Task GetSharedPivot(string sharedPivotId); +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/IUserService.cs b/Rms.Risk.Mango.Pivot.UI/Services/IUserService.cs new file mode 100644 index 0000000..3c56fea --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/IUserService.cs @@ -0,0 +1,52 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Security.Claims; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public interface IUserService +{ + /// + /// Get currently logged in user identity + /// + /// + ClaimsPrincipal GetUser(); + + /// + /// Is user authenticated? + /// + bool IsAuthenticated { get; } + + /// + /// Get email + /// + /// + // ReSharper disable once MemberCanBePrivate.Global + string GetEmail(); + + /// + /// Get any user identity claim + /// + /// + /// + // ReSharper disable once MemberCanBePrivate.Global + string? Get(string claimType); +} + + diff --git a/Rms.Risk.Mango.Pivot.UI/Services/JsonUtils.cs b/Rms.Risk.Mango.Pivot.UI/Services/JsonUtils.cs new file mode 100644 index 0000000..8cf1824 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/JsonUtils.cs @@ -0,0 +1,232 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#if MSJSON +using System.Text.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; +using Rms.Risk.Forge.Service.Client.Impl; +#else +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +#endif + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +#if MSJSON +#else +/// +/// Mimics System.Text.Json, but works for Newtonsoft.Json. +/// +public class JsonSerializerOptions +{ + /// + /// Converters list + /// + public List Converters { get; } = []; + /// + /// Indent output + /// + public bool WriteIndented { get; set; } +} +#endif +/// +/// Json serialization. Mostly to hide differences between Newtonsoft.Json and System.Text.Json. +/// +public static class JsonUtils +{ + /// + /// Text.Json does not expose default configuration (02.03.2020). + /// This is a reflection-based hack to set them properly. + /// + public static void ApplyJsonSerializerDefaultConfigHack() + { +#if MSJSON + var op = (JsonSerializerOptions)typeof( JsonSerializerOptions ) + .GetField( "s_defaultOptions", + System.Reflection.BindingFlags.Static | + System.Reflection.BindingFlags.NonPublic ).GetValue( null ); + op.IgnoreNullValues = true; + op.WriteIndented = false; + op.AllowTrailingCommas = true; + op.IgnoreReadOnlyProperties = true; + op.PropertyNameCaseInsensitive = true; + op.ReadCommentHandling = JsonCommentHandling.Skip; + op.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + op.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + op.Converters.Add( new KvpJsonConverterFactory() ); + //op.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; +#endif + } + + /// + /// Deserialize Json string into object. + /// + /// + /// + /// + /// + public static T? FromJson( string json, JsonSerializerOptions? options = null ) where T : class + { +#if MSJSON + return JsonSerializer.Deserialize( json, options ); +#else + if (json == null) + throw new ArgumentNullException(nameof(json)); + + var shortJson = json[..Math.Min(json.Length, 24)]; + if (shortJson.IndexOf("", StringComparison.OrdinalIgnoreCase) >= 0) + throw new ApplicationException("Expected JSON, but got HTML"); + if (shortJson.IndexOf("= 0) + throw new ApplicationException("Expected JSON, but got XML"); + + using var tr = new StringReader(json); + using var jsonReader = new JsonTextReader( tr ); + var js = new JsonSerializer(); + + if ( options?.Converters != null ) + { + foreach ( var conv in options.Converters ) + js.Converters.Add( conv ); + } + + return js.Deserialize( jsonReader ); +#endif + } + + /// + /// Check if string is a valid Json + /// + public static bool IsValidJson(string strInput) + { + if (string.IsNullOrWhiteSpace(strInput)) + return false; + + strInput = strInput.Trim(); + if (strInput.StartsWith("{") && strInput.EndsWith("}") || //For object + strInput.StartsWith("[") && strInput.EndsWith("]")) //For array + { + try + { + _ = JToken.Parse(strInput); + return true; + } + catch (JsonReaderException) + { + //Exception in parsing json + return false; + } + catch (Exception) //some other exception + { + return false; + } + } + else + { + return false; + } + } + + /// + /// Serialize an object to Json string. + /// + /// + /// + /// + public static string ToJson( object content, JsonSerializerOptions? options = null ) + { +#if MSJSON + return JsonUtils.ToJson( content ); +#else + using var tw = new StringWriter(); + using var jtw = new JsonTextWriter( tw ); + var js = new JsonSerializer(); + + if ( options?.Converters != null ) + { + foreach ( var conv in options.Converters ) + js.Converters.Add( conv ); + } + + js.Formatting = options?.WriteIndented ?? false + ? Formatting.Indented + : Formatting.None + ; + + js.Serialize( jtw, content ); + return tw.ToString(); +#endif + } + + /// + /// Ident json string + /// + public static string FormatJson(string? json) + { + try + { + if (json == null) + return "{}"; + var jo = FromJson(json); + if (jo == null) + return json; + return ToJson(jo, new() { WriteIndented = true }); + } + catch (Exception) + { + return json ?? "{}"; + } + } + + public static IReadOnlyCollection GetByPath(this JToken root, string path) + { + if (root == null || path == null) + throw new ArgumentNullException(); + + return root.SelectTokens(path).ToList(); + } + + public static JToken? GetOneByPath(this JToken root, string path) + { + if (root == null || path == null) + throw new ArgumentNullException(); + + var list = root.SelectTokens(path).ToList(); + return list.FirstOrDefault(); + } + + public static JToken ReplacePath(this JToken root, string path, T newValue) + { + if (root == null || path == null) + throw new ArgumentNullException(); + + foreach (var value in root.SelectTokens(path).ToList()) + { + if (value == root) + root = JToken.FromObject(newValue!); + else + value.Replace(JToken.FromObject(newValue!)); + } + + return root; + } + + public static string ReplacePath(string jsonString, string path, T newValue) + { + return JToken.Parse(jsonString).ReplacePath(path, newValue).ToString(); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/LocalStorage.cs b/Rms.Risk.Mango.Pivot.UI/Services/LocalStorage.cs new file mode 100644 index 0000000..3116b71 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/LocalStorage.cs @@ -0,0 +1,72 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.JSInterop; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class LocalStorage +{ + public static async Task LoadFromLocalStorage(this IJSRuntime runtime, string dataName, CancellationToken token = default) + { + try + { + var text = await runtime.InvokeAsync("localStorage.getItem", token, dataName); + return text; + } + catch (Exception) + { + return null; + } + } + + public static async Task LoadFromLocalStorage(this IJSRuntime runtime, string dataName, CancellationToken token = default) + where T : class + { + var json = await runtime.LoadFromLocalStorage(dataName, token); + if ( string.IsNullOrWhiteSpace(json) ) + return null; + var t = JsonUtils.FromJson(json); + return t; + } + + + public static async Task SaveToLocalStorage(this IJSRuntime runtime, string dataName, string data, CancellationToken token = default) + { + try + { + await runtime.InvokeAsync("localStorage.setItem", token, dataName, data); + } + catch (Exception) + { + // ignore + } + } + + public static async Task SaveToLocalStorage(this IJSRuntime runtime, string dataName, T? data, CancellationToken token = default) + { + if ( data == null ) + { + await runtime.SaveToLocalStorage(dataName, "", token); + return; + } + + var json = JsonUtils.ToJson(data, new () { WriteIndented = true } ); + await runtime.SaveToLocalStorage(dataName, json, token); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/NavigationManagerExtensions.cs b/Rms.Risk.Mango.Pivot.UI/Services/NavigationManagerExtensions.cs new file mode 100644 index 0000000..bd086d8 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/NavigationManagerExtensions.cs @@ -0,0 +1,63 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class NavigationManagerExtensions +{ + public static T? TryGetQueryString(this NavigationManager navManager, string key, T? defaultValue = default) + { + var uri = navManager.ToAbsoluteUri(navManager.Uri); + + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue(key, out var valueFromQueryString)) + { + if (typeof(T) == typeof(int) && int.TryParse(valueFromQueryString, out var valueAsInt)) + { + return (T)(object)valueAsInt; + } + + if (typeof(T) == typeof(string)) + { + return (T)(object)valueFromQueryString.ToString(); + } + + if (typeof(T) == typeof(decimal) && decimal.TryParse(valueFromQueryString, out var valueAsDecimal)) + { + return (T)(object)valueAsDecimal; + } + } + + return defaultValue; + } + + public static Dictionary GetQueryParameters( this NavigationManager navManager ) + { + var uri = navManager.ToAbsoluteUri(navManager.Uri); + var parsed = QueryHelpers.ParseQuery( uri.Query ); + var res = parsed.ToDictionary( + x => x.Key, + x => string.Join( ",", (IEnumerable)x.Value ) + ); + + return res; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Services/Night.cs b/Rms.Risk.Mango.Pivot.UI/Services/Night.cs new file mode 100644 index 0000000..dac13ec --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/Night.cs @@ -0,0 +1,86 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global + +using System.Globalization; + +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class Night +{ + public const string Background = "#27394f"; + public const string BackgroundLight = "#2e4057"; + public const string Headers = "#76c5fd"; + public const string Border = "#556880"; + public const string Link = "#45cbfd"; + public const string LinkHover = "#ffffff"; + public const string ButtonBg = "#1966b5"; + + public const string blue = "#1d5dc7"; + public const string indigo = "#6610f2"; + public const string purple = "#6f42c1"; + public const string pink = "#e83e8c"; + public const string red = "#840808"; + public const string orange = "#904918"; + public const string yellow = "#904918"; + public const string green = "#015402"; + public const string teal = "#147285"; + public const string cyan = "#2155a8"; + public const string white = "#1d2a3b"; + public const string gray = "#09121f"; + public const string gray_dark = "#1a2633"; + public const string primary = "#1d5dc7"; + public const string secondary = "#586d85"; + public const string success = "#016603"; + public const string info = "#138397"; + public const string warning = "#994d15"; + public const string danger = "#a10b0b"; + public const string light = "#FFFFFF"; + public const string dark = "#121c29"; + public const string breakpoint_xs = "0"; + public const string breakpoint_sm = "576px"; + public const string breakpoint_md = "768px"; + public const string breakpoint_lg = "992px"; + public const string breakpoint_xl = "1200px"; + public const string font_family_sans_serif = "Arial, sans_serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\""; + public const string font_family_monospace = "SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace\""; + + /// + /// Produces a string of the form 'rgba(r, g, b, alpha)' with random values for rgb and alpha > 0.3 + /// + /// + public static string RandomColorString() + { + var random = new Random(); + + var r = 1 + random.Next(byte.MaxValue); + var g = 1 + random.Next(byte.MaxValue); + var b = 1 + random.Next(byte.MaxValue); + var a = 0.3 + random.NextDouble() * 0.7; + + if (a > 1.0) + a = 1.0; + if (a < 0.3) + a = 0.3; + + return $"rgba({r}, {g}, {b}, {a.ToString(CultureInfo.InvariantCulture)})"; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Services/NumbersUtils.cs b/Rms.Risk.Mango.Pivot.UI/Services/NumbersUtils.cs new file mode 100644 index 0000000..5d68fa9 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Services/NumbersUtils.cs @@ -0,0 +1,40 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Pivot.UI.Services; + +public static class NumbersUtils +{ + public static string ToHumanReadable(long len, string? format = "0.##") + => ToHumanReadable(Convert.ToDouble(len), format); + + public static string ToHumanReadable(double len, string? format = "0.##") + { + string[] sizes = ["B", "KB", "MB", "GB", "TB"]; + var order = 0; + while (len >= 1024.0 && order < sizes.Length - 1) { + order++; + len /= 1024.0; + } + + // Adjust the format string to your preferences. For example "{0:0.#}{1}" would + // show a single decimal place, and no space. + var result = $"{len.ToString(format)} {sizes[order]}"; + return result; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Tree/TreeComponent.razor b/Rms.Risk.Mango.Pivot.UI/Tree/TreeComponent.razor new file mode 100644 index 0000000..3225c61 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Tree/TreeComponent.razor @@ -0,0 +1,125 @@ +@typeparam TItem + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + + + + + + + + + @if (AllowAddRootNode) + { + + } +
    + @foreach (var node in RootNodes) + { + + } +
+
+
+
+
+
+
+ +@code { + [Parameter] public List> RootNodes { get; set; } = []; + [Parameter] public EventCallback> OnNodeChanged { get; set; } + [Parameter] public bool AllowAddRootNode { get; set; } + [Parameter] public bool ReadOnly { get; set; } + + [Parameter] public RenderFragment>? LabelTemplate { get; set; } + [Parameter] public RenderFragment>? BodyTemplate { get; set; } + + [Parameter] + public TreeNode? SelectedNode + { + get; + set + { + if (!EqualityComparer?>.Default.Equals(field, value)) + { + field = value; + SelectedNodeChanged.InvokeAsync(value); + StateHasChanged(); + } + } + } = null; + + [Parameter] + public EventCallback?> SelectedNodeChanged { get; set; } + + private void AddRootNode() + { + var newNode = new TreeNode { Data = default }; // Or initialize with a default TItem + RootNodes.Add(newNode); + OnNodeChanged.InvokeAsync(newNode); + } + + private TreeNode? _currentlyEditingNode = null; + + private void SetCurrentlyEditingNode(TreeNode? node) + { + _currentlyEditingNode = node; + StateHasChanged(); // Crucial to propagate the change + } + + private void SetCurrentlySelectedNode(TreeNode? node) + { + SelectedNode = node; + } +} diff --git a/Rms.Risk.Mango.Pivot.UI/Tree/TreeNode.cs b/Rms.Risk.Mango.Pivot.UI/Tree/TreeNode.cs new file mode 100644 index 0000000..b641e95 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Tree/TreeNode.cs @@ -0,0 +1,43 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Components; + +namespace Rms.Risk.Mango.Pivot.UI.Tree; + +internal static class TreeNodeCounter +{ + private static int _count; + + public static int GetNew() => Interlocked.Increment(ref _count); +} + +public class TreeNode +{ + + public string Label { get; set; } = $"New Node {TreeNodeCounter.GetNew()}"; + public TItem? Data { get; set; } + public TreeNode? Parent { get; set; } + public List> Children { get; set; } = []; + public bool IsExpanded { get; set; } + + public RenderFragment>? LabelFragment { get; set; } + public RenderFragment>? ContentsFragment { get; set; } + + public override string ToString() => Label; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/Tree/TreeNodeComponent.razor b/Rms.Risk.Mango.Pivot.UI/Tree/TreeNodeComponent.razor new file mode 100644 index 0000000..974763a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/Tree/TreeNodeComponent.razor @@ -0,0 +1,320 @@ +@typeparam TItem + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+
+
+ @if (Node.Children.Any()) + { + + } + else if (LabelTemplate == null) + { +
+ } +
+ @if (!ReadOnly && IsThisNodeEditing) + { + + } + else + { + if (LabelTemplate != null) + { + @LabelTemplate(Node) + } + else + { + @Node.Label + } + } +
+ @if (!ReadOnly) + { +
+ + + + @if (CanMoveUp) + { + + } + @if (CanMoveDown) + { + + } +
+ } +
+ + @if (BodyTemplate != null) + { +
+ @BodyTemplate(Node) +
+ } +
+
+ @if (IsExpanded) + { +
    + @foreach (var child in Node.Children) + { + + } +
+ } +
+ +@code { + [Parameter] public TreeNode Node { get; set; } = null!; + [Parameter] public EventCallback> OnNodeChanged { get; set; } + + [Parameter] public RenderFragment>? LabelTemplate { get; set; } + [Parameter] public RenderFragment>? BodyTemplate { get; set; } + + [CascadingParameter(Name = "ReadOnly")] public bool ReadOnly { get; set; } + [CascadingParameter(Name = "CurrentlyEditingNodeRef")] public TreeNode? SharedCurrentlyEditingNode { get; set; } + [CascadingParameter(Name = "SetCurrentlyEditingNodeRef")] public Action?>? SetSharedCurrentlyEditingNode { get; set; } + [CascadingParameter(Name = "SelectedNodeRef")] public TreeNode? SharedCurrentlySelectedNode { get; set; } + [CascadingParameter(Name = "SetSelectedNodeRef")] public Action?>? SetSelectedNodeRef { get; set; } + + private string Label + { + get => Node.Label; + set + { + if (!ShouldApply) + return; + + Node.Label = value; + } + } + + private string SelectedClass => SharedCurrentlySelectedNode == Node ? "node-selected" : ""; + + private bool IsHovered { get; set; } + private bool ShouldApply { get; set; } + + private ElementReference _inputElementRef; + private bool _shouldFocusInput = false; + private string? _originalLabelBeforeEdit; + + private bool IsThisNodeEditing => !ReadOnly && SharedCurrentlyEditingNode == Node; + + private bool IsExpanded + { + get => Node.IsExpanded; + set + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (Node == null) + return; + Node.IsExpanded = value; + InvokeAsync(StateHasChanged); + } + } + + private bool CanMoveUp => Node.Parent != null && Node.Parent.Children.IndexOf(Node) > 0; + private bool CanMoveDown + { + get + { + if (Node.Parent == null) return false; + var children = Node.Parent.Children; + int index = children.IndexOf(Node); + return index >= 0 && index < children.Count - 1; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!ReadOnly && IsThisNodeEditing && _shouldFocusInput) + { + await _inputElementRef.FocusAsync(); + _shouldFocusInput = false; + } + } + + private void ToggleExpand() + { + Node.IsExpanded = !Node.IsExpanded; + } + + private async Task EditNode() + { + if (ReadOnly || SetSharedCurrentlyEditingNode == null) + return; // Cascading value not provided + + ShouldApply = true; + + if (SharedCurrentlyEditingNode == null) // Nothing else is being edited + { + _originalLabelBeforeEdit = Node.Label; // Store original label + SetSharedCurrentlyEditingNode.Invoke(Node); + _shouldFocusInput = true; + // StateHasChanged will be triggered by the parent when SharedCurrentlyEditingNode updates + } + else if (SharedCurrentlyEditingNode == Node) // This node is already editing, ensure focus + { + _shouldFocusInput = true; + // _originalLabelBeforeEdit should already be set from when editing started + await InvokeAsync(StateHasChanged); // Ensure OnAfterRenderAsync is triggered for focus + } + // If SharedCurrentlyEditingNode is another node, do nothing to enforce single edit + } + + private async Task SaveNode() + { + if ( ReadOnly) + return; // Do not save if in read-only mode + + if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == Node) + { + ShouldApply = true; + SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state + _originalLabelBeforeEdit = null; // Clear stored original label + } + await OnNodeChanged.InvokeAsync(Node); + + } + + private void AddChildNode() + { + if (ReadOnly) + return; // Do not save if in read-only mode + + var newNode = new TreeNode { Data = default, Parent = Node }; // Or initialize with a default TItem + Node.Children.Add(newNode); + IsExpanded = true; + OnNodeChanged.InvokeAsync(Node); // Notify parent that this node (Node) has changed (added a child) + // Optionally, immediately start editing the new node: + // if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == null) + // { + // SetSharedCurrentlyEditingNode.Invoke(newNode); + // _shouldFocusInput = true; // This would require newNode to be rendered and then focused. + // } + } + + private async Task DeleteNode() + { + if (ReadOnly) + return; // Do not save if in read-only mode + + if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == Node) + { + SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state if deleting the node being edited + } + Node.Parent?.Children.Remove(Node); + await OnNodeChanged.InvokeAsync(Node.Parent ?? Node); // Notify about change, pass parent or node itself if root + } + + private async Task MoveUp() + { + if (ReadOnly) + return; // Do not save if in read-only mode + + if (CanMoveUp) + { + var index = Node.Parent!.Children.IndexOf(Node); + (Node.Parent.Children[index - 1], Node.Parent.Children[index]) = (Node.Parent.Children[index], Node.Parent.Children[index - 1]); + await OnNodeChanged.InvokeAsync(Node.Parent); + } + } + + private async Task MoveDown() + { + if (ReadOnly) + return; // Do not save if in read-only mode + + if (CanMoveDown) + { + var index = Node.Parent!.Children.IndexOf(Node); + (Node.Parent.Children[index + 1], Node.Parent.Children[index]) = (Node.Parent.Children[index], Node.Parent.Children[index + 1]); + await OnNodeChanged.InvokeAsync(Node.Parent); + } + } + + private void OnMouseOver() + { + IsHovered = true; + } + + private void OnMouseOut() + { + IsHovered = false; + } + + private async Task HandleKeyDown(KeyboardEventArgs args) + { + if (ReadOnly) + return; // Do not save if in read-only mode + + if (args.Key == "Escape") + { + await CancelEdit(); + } + else if (args.Key == "Enter") + { + await SaveNode(); + } + // Other keys will be handled by the browser's default behavior for input fields + } + + private async Task CancelEdit() + { + if (ReadOnly) + return; // Do not save if in read-only mode + + ShouldApply = false; + if (SetSharedCurrentlyEditingNode == null || SharedCurrentlyEditingNode != Node) + return; + + if (_originalLabelBeforeEdit != null) + { + Node.Label = _originalLabelBeforeEdit; // Restore original label + } + + SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state + _originalLabelBeforeEdit = null; // Clear stored original label + await InvokeAsync(StateHasChanged); + } + + private async Task SelectNode() + { + SetSelectedNodeRef?.Invoke(Node); + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango.Pivot.UI/_Imports.razor b/Rms.Risk.Mango.Pivot.UI/_Imports.razor new file mode 100644 index 0000000..4dba598 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/_Imports.razor @@ -0,0 +1,35 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Rms.Risk.Mango.Pivot.UI.Pivot +@using Rms.Risk.Mango.Pivot.UI.Tree +@using Rms.Risk.Mango.Pivot.UI.Controls +@using Rms.Risk.Mango.Pivot.UI.Forms +@using Rms.Risk.Mango.Pivot.UI.Services +@using Blazored.Modal +@using Blazored.Modal.Services + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ diff --git a/Rms.Risk.Mango.Pivot.UI/globals.cs b/Rms.Risk.Mango.Pivot.UI/globals.cs new file mode 100644 index 0000000..ce9977a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/globals.cs @@ -0,0 +1,21 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.Rms.Risk.Mango")] \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/mango.png b/Rms.Risk.Mango.Pivot.UI/mango.png new file mode 100644 index 0000000..4f98cbd Binary files /dev/null and b/Rms.Risk.Mango.Pivot.UI/mango.png differ diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.css new file mode 100644 index 0000000..63b3016 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.css @@ -0,0 +1,32 @@ +.CodeMirror-dialog { + position: absolute; + left: 0; right: 0; + background: inherit; + z-index: 15; + padding: .1em .8em; + overflow: hidden; + color: inherit; +} + +.CodeMirror-dialog-top { + border-bottom: 1px solid #eee; + top: 0; +} + +.CodeMirror-dialog-bottom { + border-top: 1px solid #eee; + bottom: 0; +} + +.CodeMirror-dialog input { + border: none; + outline: none; + background: transparent; + width: 20em; + color: inherit; + font-family: monospace; +} + +.CodeMirror-dialog button { + font-size: 70%; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.js new file mode 100644 index 0000000..e1f96f7 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/dialog/dialog.js @@ -0,0 +1,163 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +// Open simple dialogs on top of an editor. Relies on dialog.css. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + function dialogDiv(cm, template, bottom) { + var wrap = cm.getWrapperElement(); + var dialog; + dialog = wrap.appendChild(document.createElement("div")); + if (bottom) + dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; + else + dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; + + if (typeof template == "string") { + dialog.innerHTML = template; + } else { // Assuming it's a detached DOM element. + dialog.appendChild(template); + } + CodeMirror.addClass(wrap, 'dialog-opened'); + return dialog; + } + + function closeNotification(cm, newVal) { + if (cm.state.currentNotificationClose) + cm.state.currentNotificationClose(); + cm.state.currentNotificationClose = newVal; + } + + CodeMirror.defineExtension("openDialog", function(template, callback, options) { + if (!options) options = {}; + + closeNotification(this, null); + + var dialog = dialogDiv(this, template, options.bottom); + var closed = false, me = this; + function close(newVal) { + if (typeof newVal == 'string') { + inp.value = newVal; + } else { + if (closed) return; + closed = true; + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + me.focus(); + + if (options.onClose) options.onClose(dialog); + } + } + + var inp = dialog.getElementsByTagName("input")[0], button; + if (inp) { + inp.focus(); + + if (options.value) { + inp.value = options.value; + if (options.selectValueOnOpen !== false) { + inp.select(); + } + } + + if (options.onInput) + CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); + if (options.onKeyUp) + CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); + + CodeMirror.on(inp, "keydown", function(e) { + if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } + if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { + inp.blur(); + CodeMirror.e_stop(e); + close(); + } + if (e.keyCode == 13) callback(inp.value, e); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(dialog, "focusout", function (evt) { + if (evt.relatedTarget !== null) close(); + }); + } else if (button = dialog.getElementsByTagName("button")[0]) { + CodeMirror.on(button, "click", function() { + close(); + me.focus(); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); + + button.focus(); + } + return close; + }); + + CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { + closeNotification(this, null); + var dialog = dialogDiv(this, template, options && options.bottom); + var buttons = dialog.getElementsByTagName("button"); + var closed = false, me = this, blurring = 1; + function close() { + if (closed) return; + closed = true; + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + me.focus(); + } + buttons[0].focus(); + for (var i = 0; i < buttons.length; ++i) { + var b = buttons[i]; + (function(callback) { + CodeMirror.on(b, "click", function(e) { + CodeMirror.e_preventDefault(e); + close(); + if (callback) callback(me); + }); + })(callbacks[i]); + CodeMirror.on(b, "blur", function() { + --blurring; + setTimeout(function() { if (blurring <= 0) close(); }, 200); + }); + CodeMirror.on(b, "focus", function() { ++blurring; }); + } + }); + + /* + * openNotification + * Opens a notification, that can be closed with an optional timer + * (default 5000ms timer) and always closes on click. + * + * If a notification is opened while another is opened, it will close the + * currently opened one and open the new one immediately. + */ + CodeMirror.defineExtension("openNotification", function(template, options) { + closeNotification(this, close); + var dialog = dialogDiv(this, template, options && options.bottom); + var closed = false, doneTimer; + var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; + + function close() { + if (closed) return; + closed = true; + clearTimeout(doneTimer); + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + } + + CodeMirror.on(dialog, 'click', function(e) { + CodeMirror.e_preventDefault(e); + close(); + }); + + if (duration) + doneTimer = setTimeout(close, duration); + + return close; + }); +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/autorefresh.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/autorefresh.js new file mode 100644 index 0000000..37014dc --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/autorefresh.js @@ -0,0 +1,47 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod) + else // Plain browser env + mod(CodeMirror) +})(function(CodeMirror) { + "use strict" + + CodeMirror.defineOption("autoRefresh", false, function(cm, val) { + if (cm.state.autoRefresh) { + stopListening(cm, cm.state.autoRefresh) + cm.state.autoRefresh = null + } + if (val && cm.display.wrapper.offsetHeight == 0) + startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250}) + }) + + function startListening(cm, state) { + function check() { + if (cm.display.wrapper.offsetHeight) { + stopListening(cm, state) + if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight) + cm.refresh() + } else { + state.timeout = setTimeout(check, state.delay) + } + } + state.timeout = setTimeout(check, state.delay) + state.hurry = function() { + clearTimeout(state.timeout) + state.timeout = setTimeout(check, 50) + } + CodeMirror.on(window, "mouseup", state.hurry) + CodeMirror.on(window, "keyup", state.hurry) + } + + function stopListening(_cm, state) { + clearTimeout(state.timeout) + CodeMirror.off(window, "mouseup", state.hurry) + CodeMirror.off(window, "keyup", state.hurry) + } +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.css new file mode 100644 index 0000000..58b2a8f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.css @@ -0,0 +1,7 @@ +.CodeMirror-fullscreen { + position: fixed; + top: 30px; left: 0; right: 0; bottom: 0; + height: auto; + z-index: 9; + max-height: 97% !important; +} diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.js new file mode 100644 index 0000000..eda7300 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/display/fullscreen.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("fullScreen", false, function(cm, val, old) { + if (old == CodeMirror.Init) old = false; + if (!old == !val) return; + if (val) setFullscreen(cm); + else setNormal(cm); + }); + + function setFullscreen(cm) { + var wrap = cm.getWrapperElement(); + cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, + width: wrap.style.width, height: wrap.style.height}; + wrap.style.width = ""; + wrap.style.height = "auto"; + wrap.className += " CodeMirror-fullscreen"; + document.documentElement.style.overflow = "hidden"; + cm.refresh(); + } + + function setNormal(cm) { + var wrap = cm.getWrapperElement(); + wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, ""); + document.documentElement.style.overflow = ""; + var info = cm.state.fullScreenRestore; + wrap.style.width = info.width; wrap.style.height = info.height; + window.scrollTo(info.scrollLeft, info.scrollTop); + cm.refresh(); + } +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/brace-fold.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/brace-fold.js new file mode 100644 index 0000000..654d1fb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/brace-fold.js @@ -0,0 +1,105 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("fold", "brace", function(cm, start) { + var line = start.line, lineText = cm.getLine(line); + var tokenType; + + function findOpening(openCh) { + for (var at = start.ch, pass = 0;;) { + var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1); + if (found == -1) { + if (pass == 1) break; + pass = 1; + at = lineText.length; + continue; + } + if (pass == 1 && found < start.ch) break; + tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); + if (!/^(comment|string)/.test(tokenType)) return found + 1; + at = found - 1; + } + } + + var startToken = "{", endToken = "}", startCh = findOpening("{"); + if (startCh == null) { + startToken = "[", endToken = "]"; + startCh = findOpening("["); + } + + if (startCh == null) return; + var count = 1, lastLine = cm.lastLine(), end, endCh; + outer: for (var i = line; i <= lastLine; ++i) { + var text = cm.getLine(i), pos = i == line ? startCh : 0; + for (;;) { + var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); + if (nextOpen < 0) nextOpen = text.length; + if (nextClose < 0) nextClose = text.length; + pos = Math.min(nextOpen, nextClose); + if (pos == text.length) break; + if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) { + if (pos == nextOpen) ++count; + else if (!--count) { end = i; endCh = pos; break outer; } + } + ++pos; + } + } + if (end == null || line == end) return; + return {from: CodeMirror.Pos(line, startCh), + to: CodeMirror.Pos(end, endCh)}; +}); + +CodeMirror.registerHelper("fold", "import", function(cm, start) { + function hasImport(line) { + if (line < cm.firstLine() || line > cm.lastLine()) return null; + var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); + if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); + if (start.type != "keyword" || start.string != "import") return null; + // Now find closing semicolon, return its position + for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { + var text = cm.getLine(i), semi = text.indexOf(";"); + if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; + } + } + + var startLine = start.line, has = hasImport(startLine), prev; + if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) + return null; + for (var end = has.end;;) { + var next = hasImport(end.line + 1); + if (next == null) break; + end = next.end; + } + return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; +}); + +CodeMirror.registerHelper("fold", "include", function(cm, start) { + function hasInclude(line) { + if (line < cm.firstLine() || line > cm.lastLine()) return null; + var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); + if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); + if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; + } + + var startLine = start.line, has = hasInclude(startLine); + if (has == null || hasInclude(startLine - 1) != null) return null; + for (var end = startLine;;) { + var next = hasInclude(end + 1); + if (next == null) break; + ++end; + } + return {from: CodeMirror.Pos(startLine, has + 1), + to: cm.clipPos(CodeMirror.Pos(end))}; +}); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/comment-fold.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/comment-fold.js new file mode 100644 index 0000000..836101d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/comment-fold.js @@ -0,0 +1,59 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerGlobalHelper("fold", "comment", function(mode) { + return mode.blockCommentStart && mode.blockCommentEnd; +}, function(cm, start) { + var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd; + if (!startToken || !endToken) return; + var line = start.line, lineText = cm.getLine(line); + + var startCh; + for (var at = start.ch, pass = 0;;) { + var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1); + if (found == -1) { + if (pass == 1) return; + pass = 1; + at = lineText.length; + continue; + } + if (pass == 1 && found < start.ch) return; + if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) && + (found == 0 || lineText.slice(found - endToken.length, found) == endToken || + !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) { + startCh = found + startToken.length; + break; + } + at = found - 1; + } + + var depth = 1, lastLine = cm.lastLine(), end, endCh; + outer: for (var i = line; i <= lastLine; ++i) { + var text = cm.getLine(i), pos = i == line ? startCh : 0; + for (;;) { + var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); + if (nextOpen < 0) nextOpen = text.length; + if (nextClose < 0) nextClose = text.length; + pos = Math.min(nextOpen, nextClose); + if (pos == text.length) break; + if (pos == nextOpen) ++depth; + else if (!--depth) { end = i; endCh = pos; break outer; } + ++pos; + } + } + if (end == null || line == end && endCh == startCh) return; + return {from: CodeMirror.Pos(line, startCh), + to: CodeMirror.Pos(end, endCh)}; +}); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldcode.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldcode.js new file mode 100644 index 0000000..887df3f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldcode.js @@ -0,0 +1,157 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function doFold(cm, pos, options, force) { + if (options && options.call) { + var finder = options; + options = null; + } else { + var finder = getOption(cm, options, "rangeFinder"); + } + if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); + var minSize = getOption(cm, options, "minFoldSize"); + + function getRange(allowFolded) { + var range = finder(cm, pos); + if (!range || range.to.line - range.from.line < minSize) return null; + var marks = cm.findMarksAt(range.from); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold && force !== "fold") { + if (!allowFolded) return null; + range.cleared = true; + marks[i].clear(); + } + } + return range; + } + + var range = getRange(true); + if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { + pos = CodeMirror.Pos(pos.line - 1, 0); + range = getRange(false); + } + if (!range || range.cleared || force === "unfold") return; + + var myWidget = makeWidget(cm, options, range); + CodeMirror.on(myWidget, "mousedown", function(e) { + myRange.clear(); + CodeMirror.e_preventDefault(e); + }); + var myRange = cm.markText(range.from, range.to, { + replacedWith: myWidget, + clearOnEnter: getOption(cm, options, "clearOnEnter"), + __isFold: true + }); + myRange.on("clear", function(from, to) { + CodeMirror.signal(cm, "unfold", cm, from, to); + }); + CodeMirror.signal(cm, "fold", cm, range.from, range.to); + } + + function makeWidget(cm, options, range) { + var widget = getOption(cm, options, "widget"); + + if (typeof widget == "function") { + widget = widget(range.from, range.to); + } + + if (typeof widget == "string") { + var text = document.createTextNode(widget); + widget = document.createElement("span"); + widget.appendChild(text); + widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) + } + return widget; + } + + // Clumsy backwards-compatible interface + CodeMirror.newFoldFunction = function(rangeFinder, widget) { + return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; + }; + + // New-style interface + CodeMirror.defineExtension("foldCode", function(pos, options, force) { + doFold(this, pos, options, force); + }); + + CodeMirror.defineExtension("isFolded", function(pos) { + var marks = this.findMarksAt(pos); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold) return true; + }); + + CodeMirror.commands.toggleFold = function(cm) { + cm.foldCode(cm.getCursor()); + }; + CodeMirror.commands.fold = function(cm) { + cm.foldCode(cm.getCursor(), null, "fold"); + }; + CodeMirror.commands.unfold = function(cm) { + cm.foldCode(cm.getCursor(), null, "unfold"); + }; + CodeMirror.commands.foldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); + }); + }; + CodeMirror.commands.unfoldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); + }); + }; + + CodeMirror.registerHelper("fold", "combine", function() { + var funcs = Array.prototype.slice.call(arguments, 0); + return function(cm, start) { + for (var i = 0; i < funcs.length; ++i) { + var found = funcs[i](cm, start); + if (found) return found; + } + }; + }); + + CodeMirror.registerHelper("fold", "auto", function(cm, start) { + var helpers = cm.getHelpers(start, "fold"); + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, start); + if (cur) return cur; + } + }); + + var defaultOptions = { + rangeFinder: CodeMirror.fold.auto, + widget: "\u2194", + minFoldSize: 0, + scanUp: false, + clearOnEnter: true + }; + + CodeMirror.defineOption("foldOptions", null); + + function getOption(cm, options, name) { + if (options && options[name] !== undefined) + return options[name]; + var editorOptions = cm.options.foldOptions; + if (editorOptions && editorOptions[name] !== undefined) + return editorOptions[name]; + return defaultOptions[name]; + } + + CodeMirror.defineExtension("foldOption", function(options, name) { + return getOption(this, options, name); + }); +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.css new file mode 100644 index 0000000..f0ae770 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.css @@ -0,0 +1,22 @@ +.CodeMirror-foldmarker { + color: blue; + text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; + font-family: arial; + line-height: .3; + cursor: pointer; + font-size: 0.8rem; +} +.CodeMirror-foldgutter { + width: .85em; +} +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + cursor: pointer; + font-size: 0.8rem; +} +.CodeMirror-foldgutter-open:after { + content: "\25BE"; +} +.CodeMirror-foldgutter-folded:after { + content: "\25B8"; +} diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.js new file mode 100644 index 0000000..7d46a60 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/foldgutter.js @@ -0,0 +1,163 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./foldcode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./foldcode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.clearGutter(cm.state.foldGutter.options.gutter); + cm.state.foldGutter = null; + cm.off("gutterClick", onGutterClick); + cm.off("changes", onChange); + cm.off("viewportChange", onViewportChange); + cm.off("fold", onFold); + cm.off("unfold", onFold); + cm.off("swapDoc", onChange); + } + if (val) { + cm.state.foldGutter = new State(parseOptions(val)); + updateInViewport(cm); + cm.on("gutterClick", onGutterClick); + cm.on("changes", onChange); + cm.on("viewportChange", onViewportChange); + cm.on("fold", onFold); + cm.on("unfold", onFold); + cm.on("swapDoc", onChange); + } + }); + + var Pos = CodeMirror.Pos; + + function State(options) { + this.options = options; + this.from = this.to = 0; + } + + function parseOptions(opts) { + if (opts === true) opts = {}; + if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; + if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; + if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; + return opts; + } + + function isFolded(cm, line) { + var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold) { + var fromPos = marks[i].find(-1); + if (fromPos && fromPos.line === line) + return marks[i]; + } + } + } + + function marker(spec) { + if (typeof spec == "string") { + var elt = document.createElement("div"); + elt.className = spec + " CodeMirror-guttermarker-subtle"; + return elt; + } else { + return spec.cloneNode(true); + } + } + + function updateFoldInfo(cm, from, to) { + var opts = cm.state.foldGutter.options, cur = from - 1; + var minSize = cm.foldOption(opts, "minFoldSize"); + var func = cm.foldOption(opts, "rangeFinder"); + // we can reuse the built-in indicator element if its className matches the new state + var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded); + var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen); + cm.eachLine(from, to, function(line) { + ++cur; + var mark = null; + var old = line.gutterMarkers; + if (old) old = old[opts.gutter]; + if (isFolded(cm, cur)) { + if (clsFolded && old && clsFolded.test(old.className)) return; + mark = marker(opts.indicatorFolded); + } else { + var pos = Pos(cur, 0); + var range = func && func(cm, pos); + if (range && range.to.line - range.from.line >= minSize) { + if (clsOpen && old && clsOpen.test(old.className)) return; + mark = marker(opts.indicatorOpen); + } + } + if (!mark && !old) return; + cm.setGutterMarker(line, opts.gutter, mark); + }); + } + + // copied from CodeMirror/src/util/dom.js + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } + + function updateInViewport(cm) { + var vp = cm.getViewport(), state = cm.state.foldGutter; + if (!state) return; + cm.operation(function() { + updateFoldInfo(cm, vp.from, vp.to); + }); + state.from = vp.from; state.to = vp.to; + } + + function onGutterClick(cm, line, gutter) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + if (gutter != opts.gutter) return; + var folded = isFolded(cm, line); + if (folded) folded.clear(); + else cm.foldCode(Pos(line, 0), opts); + } + + function onChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + state.from = state.to = 0; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); + } + + function onViewportChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { + var vp = cm.getViewport(); + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + updateInViewport(cm); + } else { + cm.operation(function() { + if (vp.from < state.from) { + updateFoldInfo(cm, vp.from, state.from); + state.from = vp.from; + } + if (vp.to > state.to) { + updateFoldInfo(cm, state.to, vp.to); + state.to = vp.to; + } + }); + } + }, opts.updateViewportTimeSpan || 400); + } + + function onFold(cm, from) { + var state = cm.state.foldGutter; + if (!state) return; + var line = from.line; + if (line >= state.from && line < state.to) + updateFoldInfo(cm, line, line + 1); + } +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/indent-fold.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/indent-fold.js new file mode 100644 index 0000000..0cc1126 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/indent-fold.js @@ -0,0 +1,48 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +function lineIndent(cm, lineNo) { + var text = cm.getLine(lineNo) + var spaceTo = text.search(/\S/) + if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1)))) + return -1 + return CodeMirror.countColumn(text, null, cm.getOption("tabSize")) +} + +CodeMirror.registerHelper("fold", "indent", function(cm, start) { + var myIndent = lineIndent(cm, start.line) + if (myIndent < 0) return + var lastLineInFold = null + + // Go through lines until we find a line that definitely doesn't belong in + // the block we're folding, or to the end. + for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) { + var indent = lineIndent(cm, i) + if (indent == -1) { + } else if (indent > myIndent) { + // Lines with a greater indent are considered part of the block. + lastLineInFold = i; + } else { + // If this line has non-space, non-comment content, and is + // indented less or equal to the start line, it is the start of + // another block. + break; + } + } + if (lastLineInFold) return { + from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), + to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length) + }; +}); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/markdown-fold.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/markdown-fold.js new file mode 100644 index 0000000..6a55178 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/markdown-fold.js @@ -0,0 +1,49 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("fold", "markdown", function(cm, start) { + var maxDepth = 100; + + function isHeader(lineNo) { + var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)); + return tokentype && /\bheader\b/.test(tokentype); + } + + function headerLevel(lineNo, line, nextLine) { + var match = line && line.match(/^#+/); + if (match && isHeader(lineNo)) return match[0].length; + match = nextLine && nextLine.match(/^[=\-]+\s*$/); + if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2; + return maxDepth; + } + + var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1); + var level = headerLevel(start.line, firstLine, nextLine); + if (level === maxDepth) return undefined; + + var lastLineNo = cm.lastLine(); + var end = start.line, nextNextLine = cm.getLine(end + 2); + while (end < lastLineNo) { + if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break; + ++end; + nextLine = nextNextLine; + nextNextLine = cm.getLine(end + 2); + } + + return { + from: CodeMirror.Pos(start.line, firstLine.length), + to: CodeMirror.Pos(end, cm.getLine(end).length) + }; +}); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/xml-fold.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/xml-fold.js new file mode 100644 index 0000000..13bc383 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/fold/xml-fold.js @@ -0,0 +1,184 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + function cmp(a, b) { return a.line - b.line || a.ch - b.ch; } + + var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; + var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; + var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g"); + + function Iter(cm, line, ch, range) { + this.line = line; this.ch = ch; + this.cm = cm; this.text = cm.getLine(line); + this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine(); + this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine(); + } + + function tagAt(iter, ch) { + var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch)); + return type && /\btag\b/.test(type); + } + + function nextLine(iter) { + if (iter.line >= iter.max) return; + iter.ch = 0; + iter.text = iter.cm.getLine(++iter.line); + return true; + } + function prevLine(iter) { + if (iter.line <= iter.min) return; + iter.text = iter.cm.getLine(--iter.line); + iter.ch = iter.text.length; + return true; + } + + function toTagEnd(iter) { + for (;;) { + var gt = iter.text.indexOf(">", iter.ch); + if (gt == -1) { if (nextLine(iter)) continue; else return; } + if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; } + var lastSlash = iter.text.lastIndexOf("/", gt); + var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); + iter.ch = gt + 1; + return selfClose ? "selfClose" : "regular"; + } + } + function toTagStart(iter) { + for (;;) { + var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1; + if (lt == -1) { if (prevLine(iter)) continue; else return; } + if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; } + xmlTagStart.lastIndex = lt; + iter.ch = lt; + var match = xmlTagStart.exec(iter.text); + if (match && match.index == lt) return match; + } + } + + function toNextTag(iter) { + for (;;) { + xmlTagStart.lastIndex = iter.ch; + var found = xmlTagStart.exec(iter.text); + if (!found) { if (nextLine(iter)) continue; else return; } + if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; } + iter.ch = found.index + found[0].length; + return found; + } + } + function toPrevTag(iter) { + for (;;) { + var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1; + if (gt == -1) { if (prevLine(iter)) continue; else return; } + if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; } + var lastSlash = iter.text.lastIndexOf("/", gt); + var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); + iter.ch = gt + 1; + return selfClose ? "selfClose" : "regular"; + } + } + + function findMatchingClose(iter, tag) { + var stack = []; + for (;;) { + var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0); + if (!next || !(end = toTagEnd(iter))) return; + if (end == "selfClose") continue; + if (next[1]) { // closing tag + for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) { + stack.length = i; + break; + } + if (i < 0 && (!tag || tag == next[2])) return { + tag: next[2], + from: Pos(startLine, startCh), + to: Pos(iter.line, iter.ch) + }; + } else { // opening tag + stack.push(next[2]); + } + } + } + function findMatchingOpen(iter, tag) { + var stack = []; + for (;;) { + var prev = toPrevTag(iter); + if (!prev) return; + if (prev == "selfClose") { toTagStart(iter); continue; } + var endLine = iter.line, endCh = iter.ch; + var start = toTagStart(iter); + if (!start) return; + if (start[1]) { // closing tag + stack.push(start[2]); + } else { // opening tag + for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) { + stack.length = i; + break; + } + if (i < 0 && (!tag || tag == start[2])) return { + tag: start[2], + from: Pos(iter.line, iter.ch), + to: Pos(endLine, endCh) + }; + } + } + } + + CodeMirror.registerHelper("fold", "xml", function(cm, start) { + var iter = new Iter(cm, start.line, 0); + for (;;) { + var openTag = toNextTag(iter) + if (!openTag || iter.line != start.line) return + var end = toTagEnd(iter) + if (!end) return + if (!openTag[1] && end != "selfClose") { + var startPos = Pos(iter.line, iter.ch); + var endPos = findMatchingClose(iter, openTag[2]); + return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null + } + } + }); + CodeMirror.findMatchingTag = function(cm, pos, range) { + var iter = new Iter(cm, pos.line, pos.ch, range); + if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return; + var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch); + var start = end && toTagStart(iter); + if (!end || !start || cmp(iter, pos) > 0) return; + var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]}; + if (end == "selfClose") return {open: here, close: null, at: "open"}; + + if (start[1]) { // closing tag + return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"}; + } else { // opening tag + iter = new Iter(cm, to.line, to.ch, range); + return {open: here, close: findMatchingClose(iter, start[2]), at: "open"}; + } + }; + + CodeMirror.findEnclosingTag = function(cm, pos, range, tag) { + var iter = new Iter(cm, pos.line, pos.ch, range); + for (;;) { + var open = findMatchingOpen(iter, tag); + if (!open) break; + var forward = new Iter(cm, pos.line, pos.ch, range); + var close = findMatchingClose(forward, open.tag); + if (close) return {open: open, close: close}; + } + }; + + // Used by addon/edit/closetag.js + CodeMirror.scanForClosingTag = function(cm, pos, name, end) { + var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); + return findMatchingClose(iter, name); + }; +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.css new file mode 100644 index 0000000..89e88aa --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.css @@ -0,0 +1,36 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.js new file mode 100644 index 0000000..3d0609e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/hint/show-hint.js @@ -0,0 +1,511 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// declare global: DOMRect + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + // This is the old interface, kept around for now to stay + // backwards-compatible. + CodeMirror.showHint = function(cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = {hint: getHints}; + if (options) for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); + }; + + CodeMirror.defineExtension("showHint", function(options) { + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + if (!completion.options.hint) return; + + CodeMirror.signal(this, "startCompletion", this); + completion.update(true); + }); + + CodeMirror.defineExtension("closeHint", function() { + if (this.state.completionActive) this.state.completionActive.close() + }) + + function Completion(cm, options) { + this.cm = cm; + this.options = options; + this.widget = null; + this.debounce = 0; + this.tick = 0; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; + + if (this.options.updateOnCursorActivity) { + var self = this; + cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); + } + } + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + this.tick = null; + if (this.options.updateOnCursorActivity) { + this.cm.off("cursorActivity", this.activityFunc); + } + + if (this.widget && this.data) CodeMirror.signal(this.data, "close"); + if (this.widget) this.widget.close(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i], self = this; + this.cm.operation(function() { + if (completion.hint) + completion.hint(self.cm, data, completion); + else + self.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + self.cm.scrollIntoView(); + }); + if (this.options.closeOnPick) { + this.close(); + } + }, + + cursorActivity: function() { + if (this.debounce) { + cancelAnimationFrame(this.debounce); + this.debounce = 0; + } + + var identStart = this.startPos; + if(this.data) { + identStart = this.data.from; + } + + var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); + if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || + pos.ch < identStart.ch || this.cm.somethingSelected() || + (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { + this.close(); + } else { + var self = this; + this.debounce = requestAnimationFrame(function() {self.update();}); + if (this.widget) this.widget.disable(); + } + }, + + update: function(first) { + if (this.tick == null) return + var self = this, myTick = ++this.tick + fetchHints(this.options.hint, this.cm, this.options, function(data) { + if (self.tick == myTick) self.finishUpdate(data, first) + }) + }, + + finishUpdate: function(data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + + var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); + if (this.widget) this.widget.close(); + + this.data = data; + + if (data && data.list.length) { + if (picked && data.list.length == 1) { + this.pick(data, 0); + } else { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + } + } + } + }; + + function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; + } + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(completion, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + + var mac = /Mac/.test(navigator.platform); + + if (mac) { + baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);}; + baseMap["Ctrl-N"] = function() {handle.moveFocus(1);}; + } + + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + this.picked = false; + var widget = this, cm = completion.cm; + var ownerDocument = cm.getInputField().ownerDocument; + var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; + + var hints = this.hints = ownerDocument.createElement("ul"); + var theme = completion.cm.options.theme; + hints.className = "CodeMirror-hints " + theme; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var container = completion.options.container || ownerDocument.body; + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + var offsetLeft = 0, offsetTop = 0; + if (container !== ownerDocument.body) { + // We offset the cursor position because left and top are relative to the offsetParent's top left corner. + var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1; + var offsetParent = isContainerPositioned ? container : container.offsetParent; + var offsetParentPosition = offsetParent.getBoundingClientRect(); + var bodyPosition = ownerDocument.body.getBoundingClientRect(); + offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft); + offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop); + } + hints.style.left = (left - offsetLeft) + "px"; + hints.style.top = (top - offsetTop) + "px"; + + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth); + var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight); + container.appendChild(hints); + + var box = completion.options.moveOnOverlap ? hints.getBoundingClientRect() : new DOMRect(); + var scrolls = completion.options.paddingForScrollbar ? hints.scrollHeight > hints.clientHeight + 1 : false; + + // Compute in the timeout to avoid reflow on init + var startScroll; + setTimeout(function() { startScroll = cm.getScrollInfo(); }); + + var overlapY = box.bottom - winH; + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height - offsetTop) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left - offsetLeft) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (scrolls) overlapX += cm.display.nativeBarWidth; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px"; + } + if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) + node.style.paddingRight = cm.display.nativeBarWidth + "px" + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + if (!startScroll) startScroll = cm.getScrollInfo(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + // The first hint doesn't need to be scrolled to on init + var selectedHintRange = this.getSelectedHintRange(); + if (selectedHintRange.from !== 0 || selectedHintRange.to !== 0) { + this.scrollToActive(); + } + + CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + if (this.hints.parentNode) this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + disable: function() { + this.completion.cm.removeKeyMap(this.keyMap); + var widget = this; + this.keyMap = {Enter: function() { widget.picked = true; }}; + this.completion.cm.addKeyMap(this.keyMap); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + this.scrollToActive() + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + scrollToActive: function() { + var selectedHintRange = this.getSelectedHintRange(); + var node1 = this.hints.childNodes[selectedHintRange.from]; + var node2 = this.hints.childNodes[selectedHintRange.to]; + var firstNode = this.hints.firstChild; + if (node1.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop; + else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop; + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + }, + + getSelectedHintRange: function() { + var margin = this.completion.options.scrollMargin || 0; + return { + from: Math.max(0, this.selectedHint - margin), + to: Math.min(this.data.list.length - 1, this.selectedHint + margin), + }; + } + }; + + function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result + } + + function fetchHints(hint, cm, options, callback) { + if (hint.async) { + hint(cm, callback, options) + } else { + var result = hint(cm, options) + if (result && result.then) result.then(callback) + else callback(result) + } + } + + function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), words + if (helpers.length) { + var resolved = function(cm, callback, options) { + var app = applicableHelpers(cm, helpers); + function run(i) { + if (i == app.length) return callback(null) + fetchHints(app[i], cm, options, function(result) { + if (result && result.list.length > 0) callback(result) + else run(i + 1) + }) + } + run(0) + } + resolved.async = true + resolved.supportsSelection = true + return resolved + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } + } else if (CodeMirror.hint.anyword) { + return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } + } else { + return function() {} + } + } + + CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur) + var term, from = CodeMirror.Pos(cur.line, token.start), to = cur + if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { + term = token.string.substr(0, cur.ch - token.start) + } else { + term = "" + from = cur + } + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, term.length) == term) + found.push(word); + } + + if (found.length) return {list: found, from: from, to: to}; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; + + var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnPick: true, + closeOnUnfocus: true, + updateOnCursorActivity: true, + completeOnSingleClick: true, + container: null, + customKeys: null, + extraKeys: null, + paddingForScrollbar: true, + moveOnOverlap: true, + }; + + CodeMirror.defineOption("hintOptions", null); +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.css new file mode 100644 index 0000000..b2a50f2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.css @@ -0,0 +1,180 @@ +.CodeMirror-merge { + position: relative; + border: 1px solid #ddd; + white-space: pre; +} + + .CodeMirror-merge, .CodeMirror-merge .CodeMirror { + height: 100%; + } + +.CodeMirror-merge-2pane .CodeMirror-merge-pane { + width: 47%; +} + +.CodeMirror-merge-2pane .CodeMirror-merge-gap { + width: 6%; +} + +.CodeMirror-merge-3pane .CodeMirror-merge-pane { + width: 31%; +} + +.CodeMirror-merge-3pane .CodeMirror-merge-gap { + width: 3.5%; +} + +.CodeMirror-merge-pane { + display: inline-block; + white-space: normal; + vertical-align: top; +} + +.CodeMirror-merge-pane-rightmost { + position: absolute; + right: 0px; + z-index: 1; +} + +.CodeMirror-merge-gap { + z-index: 2; + display: inline-block; + height: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + position: relative; + background: #f8f8f8; +} + +.CodeMirror-merge-scrolllock-wrap { + position: absolute; + bottom: 0; + left: 50%; +} + +.CodeMirror-merge-scrolllock { + position: relative; + left: -50%; + cursor: pointer; + color: #555; + line-height: 1; +} + + .CodeMirror-merge-scrolllock:after { + content: "\21db\00a0\00a0\21da"; + } + + .CodeMirror-merge-scrolllock.CodeMirror-merge-scrolllock-enabled:after { + content: "\21db\21da"; + } + +.CodeMirror-merge-copybuttons-left, .CodeMirror-merge-copybuttons-right { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + line-height: 1; +} + +.CodeMirror-merge-copy { + position: absolute; + cursor: pointer; + color: #44c; + z-index: 3; +} + +.CodeMirror-merge-copy-reverse { + position: absolute; + cursor: pointer; + color: #44c; +} + +.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy { + left: 2px; +} + +.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy { + right: 2px; +} + +.CodeMirror-merge-r-inserted, .CodeMirror-merge-l-inserted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==); + background-position: bottom left; + background-repeat: repeat-x; +} + +.CodeMirror-merge-r-deleted, .CodeMirror-merge-l-deleted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==); + background-position: bottom left; + background-repeat: repeat-x; +} + +.CodeMirror-merge-r-chunk { + background: #ffffe0; +} + +.CodeMirror-merge-r-chunk-start { + border-top: 1px solid #ee8; +} + +.CodeMirror-merge-r-chunk-end { + border-bottom: 1px solid #ee8; +} + +.CodeMirror-merge-r-connect { + fill: #ffffe0; + stroke: #ee8; + stroke-width: 1px; +} + +.CodeMirror-merge-l-chunk { + background: #eef; +} + +.CodeMirror-merge-l-chunk-start { + border-top: 1px solid #88e; +} + +.CodeMirror-merge-l-chunk-end { + border-bottom: 1px solid #88e; +} + +.CodeMirror-merge-l-connect { + fill: #eef; + stroke: #88e; + stroke-width: 1px; +} + +.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk { + background: #dfd; +} + +.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start { + border-top: 1px solid #4e4; +} + +.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end { + border-bottom: 1px solid #4e4; +} + +.CodeMirror-merge-collapsed-widget:before { + content: "(...)"; +} + +.CodeMirror-merge-collapsed-widget { + cursor: pointer; + color: #88b; + background: #eef; + border: 1px solid #ddf; + font-size: 90%; + padding: 0 3px; + border-radius: 4px; +} + +.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt { + display: none; +} diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.js new file mode 100644 index 0000000..d4dd840 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/merge/merge.js @@ -0,0 +1,1006 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); // Note non-packaged dependency diff_match_patch + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "diff_match_patch"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + var Pos = CodeMirror.Pos; + var svgNS = "http://www.w3.org/2000/svg"; + + function DiffView(mv, type) { + this.mv = mv; + this.type = type; + this.classes = type == "left" + ? {chunk: "CodeMirror-merge-l-chunk", + start: "CodeMirror-merge-l-chunk-start", + end: "CodeMirror-merge-l-chunk-end", + insert: "CodeMirror-merge-l-inserted", + del: "CodeMirror-merge-l-deleted", + connect: "CodeMirror-merge-l-connect"} + : {chunk: "CodeMirror-merge-r-chunk", + start: "CodeMirror-merge-r-chunk-start", + end: "CodeMirror-merge-r-chunk-end", + insert: "CodeMirror-merge-r-inserted", + del: "CodeMirror-merge-r-deleted", + connect: "CodeMirror-merge-r-connect"}; + } + + DiffView.prototype = { + constructor: DiffView, + init: function(pane, orig, options) { + this.edit = this.mv.edit; + ;(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this); + this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options))); + if (this.mv.options.connect == "align") { + if (!this.edit.state.trackAlignable) this.edit.state.trackAlignable = new TrackAlignable(this.edit) + this.orig.state.trackAlignable = new TrackAlignable(this.orig) + } + this.lockButton.title = this.edit.phrase("Toggle locked scrolling"); + + this.orig.state.diffViews = [this]; + var classLocation = options.chunkClassLocation || "background"; + if (Object.prototype.toString.call(classLocation) != "[object Array]") classLocation = [classLocation] + this.classes.classLocation = classLocation + + this.diff = getDiff(asString(orig), asString(options.value), this.mv.options.ignoreWhitespace); + this.chunks = getChunks(this.diff); + this.diffOutOfDate = this.dealigned = false; + this.needsScrollSync = null + + this.showDifferences = options.showDifferences !== false; + }, + registerEvents: function(otherDv) { + this.forceUpdate = registerUpdate(this); + setScrollLock(this, true, false); + registerScroll(this, otherDv); + }, + setShowDifferences: function(val) { + val = val !== false; + if (val != this.showDifferences) { + this.showDifferences = val; + this.forceUpdate("full"); + } + } + }; + + function ensureDiff(dv) { + if (dv.diffOutOfDate) { + dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue(), dv.mv.options.ignoreWhitespace); + dv.chunks = getChunks(dv.diff); + dv.diffOutOfDate = false; + CodeMirror.signal(dv.edit, "updateDiff", dv.diff); + } + } + + var updating = false; + function registerUpdate(dv) { + var edit = {from: 0, to: 0, marked: []}; + var orig = {from: 0, to: 0, marked: []}; + var debounceChange, updatingFast = false; + function update(mode) { + updating = true; + updatingFast = false; + if (mode == "full") { + if (dv.svg) clear(dv.svg); + if (dv.copyButtons) clear(dv.copyButtons); + clearMarks(dv.edit, edit.marked, dv.classes); + clearMarks(dv.orig, orig.marked, dv.classes); + edit.from = edit.to = orig.from = orig.to = 0; + } + ensureDiff(dv); + if (dv.showDifferences) { + updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes); + updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes); + } + + if (dv.mv.options.connect == "align") + alignChunks(dv); + makeConnections(dv); + if (dv.needsScrollSync != null) syncScroll(dv, dv.needsScrollSync) + + updating = false; + } + function setDealign(fast) { + if (updating) return; + dv.dealigned = true; + set(fast); + } + function set(fast) { + if (updating || updatingFast) return; + clearTimeout(debounceChange); + if (fast === true) updatingFast = true; + debounceChange = setTimeout(update, fast === true ? 20 : 250); + } + function change(_cm, change) { + if (!dv.diffOutOfDate) { + dv.diffOutOfDate = true; + edit.from = edit.to = orig.from = orig.to = 0; + } + // Update faster when a line was added/removed + setDealign(change.text.length - 1 != change.to.line - change.from.line); + } + function swapDoc() { + dv.diffOutOfDate = true; + dv.dealigned = true; + update("full"); + } + dv.edit.on("change", change); + dv.orig.on("change", change); + dv.edit.on("swapDoc", swapDoc); + dv.orig.on("swapDoc", swapDoc); + if (dv.mv.options.connect == "align") { + CodeMirror.on(dv.edit.state.trackAlignable, "realign", setDealign) + CodeMirror.on(dv.orig.state.trackAlignable, "realign", setDealign) + } + dv.edit.on("viewportChange", function() { set(false); }); + dv.orig.on("viewportChange", function() { set(false); }); + update(); + return update; + } + + function registerScroll(dv, otherDv) { + dv.edit.on("scroll", function() { + syncScroll(dv, true) && makeConnections(dv); + }); + dv.orig.on("scroll", function() { + syncScroll(dv, false) && makeConnections(dv); + if (otherDv) syncScroll(otherDv, true) && makeConnections(otherDv); + }); + } + + function syncScroll(dv, toOrig) { + // Change handler will do a refresh after a timeout when diff is out of date + if (dv.diffOutOfDate) { + if (dv.lockScroll && dv.needsScrollSync == null) dv.needsScrollSync = toOrig + return false + } + dv.needsScrollSync = null + if (!dv.lockScroll) return true; + var editor, other, now = +new Date; + if (toOrig) { editor = dv.edit; other = dv.orig; } + else { editor = dv.orig; other = dv.edit; } + // Don't take action if the position of this editor was recently set + // (to prevent feedback loops) + if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 250 > now) return false; + + var sInfo = editor.getScrollInfo(); + if (dv.mv.options.connect == "align") { + targetPos = sInfo.top; + } else { + var halfScreen = .5 * sInfo.clientHeight, midY = sInfo.top + halfScreen; + var mid = editor.lineAtHeight(midY, "local"); + var around = chunkBoundariesAround(dv.chunks, mid, toOrig); + var off = getOffsets(editor, toOrig ? around.edit : around.orig); + var offOther = getOffsets(other, toOrig ? around.orig : around.edit); + var ratio = (midY - off.top) / (off.bot - off.top); + var targetPos = (offOther.top - halfScreen) + ratio * (offOther.bot - offOther.top); + + var botDist, mix; + // Some careful tweaking to make sure no space is left out of view + // when scrolling to top or bottom. + if (targetPos > sInfo.top && (mix = sInfo.top / halfScreen) < 1) { + targetPos = targetPos * mix + sInfo.top * (1 - mix); + } else if ((botDist = sInfo.height - sInfo.clientHeight - sInfo.top) < halfScreen) { + var otherInfo = other.getScrollInfo(); + var botDistOther = otherInfo.height - otherInfo.clientHeight - targetPos; + if (botDistOther > botDist && (mix = botDist / halfScreen) < 1) + targetPos = targetPos * mix + (otherInfo.height - otherInfo.clientHeight - botDist) * (1 - mix); + } + } + + other.scrollTo(sInfo.left, targetPos); + other.state.scrollSetAt = now; + other.state.scrollSetBy = dv; + return true; + } + + function getOffsets(editor, around) { + var bot = around.after; + if (bot == null) bot = editor.lastLine() + 1; + return {top: editor.heightAtLine(around.before || 0, "local"), + bot: editor.heightAtLine(bot, "local")}; + } + + function setScrollLock(dv, val, action) { + dv.lockScroll = val; + if (val && action != false) syncScroll(dv, DIFF_INSERT) && makeConnections(dv); + (val ? CodeMirror.addClass : CodeMirror.rmClass)(dv.lockButton, "CodeMirror-merge-scrolllock-enabled"); + } + + // Updating the marks for editor content + + function removeClass(editor, line, classes) { + var locs = classes.classLocation + for (var i = 0; i < locs.length; i++) { + editor.removeLineClass(line, locs[i], classes.chunk); + editor.removeLineClass(line, locs[i], classes.start); + editor.removeLineClass(line, locs[i], classes.end); + } + } + + function clearMarks(editor, arr, classes) { + for (var i = 0; i < arr.length; ++i) { + var mark = arr[i]; + if (mark instanceof CodeMirror.TextMarker) + mark.clear(); + else if (mark.parent) + removeClass(editor, mark, classes); + } + arr.length = 0; + } + + // FIXME maybe add a margin around viewport to prevent too many updates + function updateMarks(editor, diff, state, type, classes) { + var vp = editor.getViewport(); + editor.operation(function() { + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + clearMarks(editor, state.marked, classes); + markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes); + state.from = vp.from; state.to = vp.to; + } else { + if (vp.from < state.from) { + markChanges(editor, diff, type, state.marked, vp.from, state.from, classes); + state.from = vp.from; + } + if (vp.to > state.to) { + markChanges(editor, diff, type, state.marked, state.to, vp.to, classes); + state.to = vp.to; + } + } + }); + } + + function addClass(editor, lineNr, classes, main, start, end) { + var locs = classes.classLocation, line = editor.getLineHandle(lineNr); + for (var i = 0; i < locs.length; i++) { + if (main) editor.addLineClass(line, locs[i], classes.chunk); + if (start) editor.addLineClass(line, locs[i], classes.start); + if (end) editor.addLineClass(line, locs[i], classes.end); + } + return line; + } + + function markChanges(editor, diff, type, marks, from, to, classes) { + var pos = Pos(0, 0); + var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1)); + var cls = type == DIFF_DELETE ? classes.del : classes.insert; + function markChunk(start, end) { + var bfrom = Math.max(from, start), bto = Math.min(to, end); + for (var i = bfrom; i < bto; ++i) + marks.push(addClass(editor, i, classes, true, i == start, i == end - 1)); + // When the chunk is empty, make sure a horizontal line shows up + if (start == end && bfrom == end && bto == end) { + if (bfrom) + marks.push(addClass(editor, bfrom - 1, classes, false, false, true)); + else + marks.push(addClass(editor, bfrom, classes, false, true, false)); + } + } + + var chunkStart = 0, pending = false; + for (var i = 0; i < diff.length; ++i) { + var part = diff[i], tp = part[0], str = part[1]; + if (tp == DIFF_EQUAL) { + var cleanFrom = pos.line + (startOfLineClean(diff, i) ? 0 : 1); + moveOver(pos, str); + var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0); + if (cleanTo > cleanFrom) { + if (pending) { markChunk(chunkStart, cleanFrom); pending = false } + chunkStart = cleanTo; + } + } else { + pending = true + if (tp == type) { + var end = moveOver(pos, str, true); + var a = posMax(top, pos), b = posMin(bot, end); + if (!posEq(a, b)) + marks.push(editor.markText(a, b, {className: cls})); + pos = end; + } + } + } + if (pending) markChunk(chunkStart, pos.line + 1); + } + + // Updating the gap between editor and original + + function makeConnections(dv) { + if (!dv.showDifferences) return; + + if (dv.svg) { + clear(dv.svg); + var w = dv.gap.offsetWidth; + attrs(dv.svg, "width", w, "height", dv.gap.offsetHeight); + } + if (dv.copyButtons) clear(dv.copyButtons); + + var vpEdit = dv.edit.getViewport(), vpOrig = dv.orig.getViewport(); + var outerTop = dv.mv.wrap.getBoundingClientRect().top + var sTopEdit = outerTop - dv.edit.getScrollerElement().getBoundingClientRect().top + dv.edit.getScrollInfo().top + var sTopOrig = outerTop - dv.orig.getScrollerElement().getBoundingClientRect().top + dv.orig.getScrollInfo().top; + for (var i = 0; i < dv.chunks.length; i++) { + var ch = dv.chunks[i]; + if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from && + ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from) + drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w); + } + } + + function getMatchingOrigLine(editLine, chunks) { + var editStart = 0, origStart = 0; + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + if (chunk.editTo > editLine && chunk.editFrom <= editLine) return null; + if (chunk.editFrom > editLine) break; + editStart = chunk.editTo; + origStart = chunk.origTo; + } + return origStart + (editLine - editStart); + } + + // Combines information about chunks and widgets/markers to return + // an array of lines, in a single editor, that probably need to be + // aligned with their counterparts in the editor next to it. + function alignableFor(cm, chunks, isOrig) { + var tracker = cm.state.trackAlignable + var start = cm.firstLine(), trackI = 0 + var result = [] + for (var i = 0;; i++) { + var chunk = chunks[i] + var chunkStart = !chunk ? 1e9 : isOrig ? chunk.origFrom : chunk.editFrom + for (; trackI < tracker.alignable.length; trackI += 2) { + var n = tracker.alignable[trackI] + 1 + if (n <= start) continue + if (n <= chunkStart) result.push(n) + else break + } + if (!chunk) break + result.push(start = isOrig ? chunk.origTo : chunk.editTo) + } + return result + } + + // Given information about alignable lines in two editors, fill in + // the result (an array of three-element arrays) to reflect the + // lines that need to be aligned with each other. + function mergeAlignable(result, origAlignable, chunks, setIndex) { + var rI = 0, origI = 0, chunkI = 0, diff = 0 + outer: for (;; rI++) { + var nextR = result[rI], nextO = origAlignable[origI] + if (!nextR && nextO == null) break + + var rLine = nextR ? nextR[0] : 1e9, oLine = nextO == null ? 1e9 : nextO + while (chunkI < chunks.length) { + var chunk = chunks[chunkI] + if (chunk.origFrom <= oLine && chunk.origTo > oLine) { + origI++ + rI-- + continue outer; + } + if (chunk.editTo > rLine) { + if (chunk.editFrom <= rLine) continue outer; + break + } + diff += (chunk.origTo - chunk.origFrom) - (chunk.editTo - chunk.editFrom) + chunkI++ + } + if (rLine == oLine - diff) { + nextR[setIndex] = oLine + origI++ + } else if (rLine < oLine - diff) { + nextR[setIndex] = rLine + diff + } else { + var record = [oLine - diff, null, null] + record[setIndex] = oLine + result.splice(rI, 0, record) + origI++ + } + } + } + + function findAlignedLines(dv, other) { + var alignable = alignableFor(dv.edit, dv.chunks, false), result = [] + if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) { + var n = other.chunks[i].editTo + while (j < alignable.length && alignable[j] < n) j++ + if (j == alignable.length || alignable[j] != n) alignable.splice(j++, 0, n) + } + for (var i = 0; i < alignable.length; i++) + result.push([alignable[i], null, null]) + + mergeAlignable(result, alignableFor(dv.orig, dv.chunks, true), dv.chunks, 1) + if (other) + mergeAlignable(result, alignableFor(other.orig, other.chunks, true), other.chunks, 2) + + return result + } + + function alignChunks(dv, force) { + if (!dv.dealigned && !force) return; + if (!dv.orig.curOp) return dv.orig.operation(function() { + alignChunks(dv, force); + }); + + dv.dealigned = false; + var other = dv.mv.left == dv ? dv.mv.right : dv.mv.left; + if (other) { + ensureDiff(other); + other.dealigned = false; + } + var linesToAlign = findAlignedLines(dv, other); + + // Clear old aligners + var aligners = dv.mv.aligners; + for (var i = 0; i < aligners.length; i++) + aligners[i].clear(); + aligners.length = 0; + + var cm = [dv.edit, dv.orig], scroll = [], offset = [] + if (other) cm.push(other.orig); + for (var i = 0; i < cm.length; i++) { + scroll.push(cm[i].getScrollInfo().top); + offset.push(-cm[i].getScrollerElement().getBoundingClientRect().top) + } + + if (offset[0] != offset[1] || cm.length == 3 && offset[1] != offset[2]) + alignLines(cm, offset, [0, 0, 0], aligners) + for (var ln = 0; ln < linesToAlign.length; ln++) + alignLines(cm, offset, linesToAlign[ln], aligners); + + for (var i = 0; i < cm.length; i++) + cm[i].scrollTo(null, scroll[i]); + } + + function alignLines(cm, cmOffset, lines, aligners) { + var maxOffset = -1e8, offset = []; + for (var i = 0; i < cm.length; i++) if (lines[i] != null) { + var off = cm[i].heightAtLine(lines[i], "local") - cmOffset[i]; + offset[i] = off; + maxOffset = Math.max(maxOffset, off); + } + for (var i = 0; i < cm.length; i++) if (lines[i] != null) { + var diff = maxOffset - offset[i]; + if (diff > 1) + aligners.push(padAbove(cm[i], lines[i], diff)); + } + } + + function padAbove(cm, line, size) { + var above = true; + if (line > cm.lastLine()) { + line--; + above = false; + } + var elt = document.createElement("div"); + elt.className = "CodeMirror-merge-spacer"; + elt.style.height = size + "px"; elt.style.minWidth = "1px"; + return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true}); + } + + function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) { + var flip = dv.type == "left"; + var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig; + if (dv.svg) { + var topLpx = top; + var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local", true) - sTopEdit; + if (flip) { var tmp = topLpx; topLpx = topRpx; topRpx = tmp; } + var botLpx = dv.orig.heightAtLine(chunk.origTo, "local", true) - sTopOrig; + var botRpx = dv.edit.heightAtLine(chunk.editTo, "local", true) - sTopEdit; + if (flip) { var tmp = botLpx; botLpx = botRpx; botRpx = tmp; } + var curveTop = " C " + w/2 + " " + topRpx + " " + w/2 + " " + topLpx + " " + (w + 2) + " " + topLpx; + var curveBot = " C " + w/2 + " " + botLpx + " " + w/2 + " " + botRpx + " -1 " + botRpx; + attrs(dv.svg.appendChild(document.createElementNS(svgNS, "path")), + "d", "M -1 " + topRpx + curveTop + " L " + (w + 2) + " " + botLpx + curveBot + " z", + "class", dv.classes.connect); + } + if (dv.copyButtons) { + var copy = dv.copyButtons.appendChild(elt("div", dv.type == "left" ? "\u21dd" : "\u21dc", + "CodeMirror-merge-copy")); + var editOriginals = dv.mv.options.allowEditingOriginals; + copy.title = dv.edit.phrase(editOriginals ? "Push to left" : "Revert chunk"); + copy.chunk = chunk; + copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px"; + + if (editOriginals) { + var topReverse = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit; + var copyReverse = dv.copyButtons.appendChild(elt("div", dv.type == "right" ? "\u21dd" : "\u21dc", + "CodeMirror-merge-copy-reverse")); + copyReverse.title = "Push to right"; + copyReverse.chunk = {editFrom: chunk.origFrom, editTo: chunk.origTo, + origFrom: chunk.editFrom, origTo: chunk.editTo}; + copyReverse.style.top = topReverse + "px"; + dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px"; + } + } + } + + function copyChunk(dv, to, from, chunk) { + if (dv.diffOutOfDate) return; + var origStart = chunk.origTo > from.lastLine() ? Pos(chunk.origFrom - 1) : Pos(chunk.origFrom, 0) + var origEnd = Pos(chunk.origTo, 0) + var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0) + var editEnd = Pos(chunk.editTo, 0) + var handler = dv.mv.options.revertChunk + if (handler) + handler(dv.mv, from, origStart, origEnd, to, editStart, editEnd) + else + to.replaceRange(from.getRange(origStart, origEnd), editStart, editEnd) + } + + // Merge view, containing 0, 1, or 2 diff views. + + var MergeView = CodeMirror.MergeView = function(node, options) { + if (!(this instanceof MergeView)) return new MergeView(node, options); + + this.options = options; + var origLeft = options.origLeft, origRight = options.origRight == null ? options.orig : options.origRight; + + var hasLeft = origLeft != null, hasRight = origRight != null; + var panes = 1 + (hasLeft ? 1 : 0) + (hasRight ? 1 : 0); + var wrap = [], left = this.left = null, right = this.right = null; + var self = this; + + if (hasLeft) { + left = this.left = new DiffView(this, "left"); + var leftPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-left"); + wrap.push(leftPane); + wrap.push(buildGap(left)); + } + + var editPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-editor"); + wrap.push(editPane); + + if (hasRight) { + right = this.right = new DiffView(this, "right"); + wrap.push(buildGap(right)); + var rightPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-right"); + wrap.push(rightPane); + } + + (hasRight ? rightPane : editPane).className += " CodeMirror-merge-pane-rightmost"; + + wrap.push(elt("div", null, null, "height: 0; clear: both;")); + + var wrapElt = this.wrap = node.appendChild(elt("div", wrap, "CodeMirror-merge CodeMirror-merge-" + panes + "pane")); + this.edit = CodeMirror(editPane, copyObj(options)); + + if (left) left.init(leftPane, origLeft, options); + if (right) right.init(rightPane, origRight, options); + if (options.collapseIdentical) + this.editor().operation(function() { + collapseIdenticalStretches(self, options.collapseIdentical); + }); + if (options.connect == "align") { + this.aligners = []; + alignChunks(this.left || this.right, true); + } + if (left) left.registerEvents(right) + if (right) right.registerEvents(left) + + + var onResize = function() { + if (left) makeConnections(left); + if (right) makeConnections(right); + }; + CodeMirror.on(window, "resize", onResize); + var resizeInterval = setInterval(function() { + for (var p = wrapElt.parentNode; p && p != document.body; p = p.parentNode) {} + if (!p) { clearInterval(resizeInterval); CodeMirror.off(window, "resize", onResize); } + }, 5000); + }; + + function buildGap(dv) { + var lock = dv.lockButton = elt("div", null, "CodeMirror-merge-scrolllock"); + var lockWrap = elt("div", [lock], "CodeMirror-merge-scrolllock-wrap"); + CodeMirror.on(lock, "click", function() { setScrollLock(dv, !dv.lockScroll); }); + var gapElts = [lockWrap]; + if (dv.mv.options.revertButtons !== false) { + dv.copyButtons = elt("div", null, "CodeMirror-merge-copybuttons-" + dv.type); + CodeMirror.on(dv.copyButtons, "click", function(e) { + var node = e.target || e.srcElement; + if (!node.chunk) return; + if (node.className == "CodeMirror-merge-copy-reverse") { + copyChunk(dv, dv.orig, dv.edit, node.chunk); + return; + } + copyChunk(dv, dv.edit, dv.orig, node.chunk); + }); + gapElts.unshift(dv.copyButtons); + } + if (dv.mv.options.connect != "align") { + var svg = document.createElementNS && document.createElementNS(svgNS, "svg"); + if (svg && !svg.createSVGRect) svg = null; + dv.svg = svg; + if (svg) gapElts.push(svg); + } + + return dv.gap = elt("div", gapElts, "CodeMirror-merge-gap"); + } + + MergeView.prototype = { + constructor: MergeView, + editor: function() { return this.edit; }, + rightOriginal: function() { return this.right && this.right.orig; }, + leftOriginal: function() { return this.left && this.left.orig; }, + setShowDifferences: function(val) { + if (this.right) this.right.setShowDifferences(val); + if (this.left) this.left.setShowDifferences(val); + }, + rightChunks: function() { + if (this.right) { ensureDiff(this.right); return this.right.chunks; } + }, + leftChunks: function() { + if (this.left) { ensureDiff(this.left); return this.left.chunks; } + } + }; + + function asString(obj) { + if (typeof obj == "string") return obj; + else return obj.getValue(); + } + + // Operations on diffs + var dmp; + function getDiff(a, b, ignoreWhitespace) { + if (!dmp) dmp = new diff_match_patch(); + + var diff = dmp.diff_main(a, b); + // The library sometimes leaves in empty parts, which confuse the algorithm + for (var i = 0; i < diff.length; ++i) { + var part = diff[i]; + if (ignoreWhitespace ? !/[^ \t]/.test(part[1]) : !part[1]) { + diff.splice(i--, 1); + } else if (i && diff[i - 1][0] == part[0]) { + diff.splice(i--, 1); + diff[i][1] += part[1]; + } + } + return diff; + } + + function getChunks(diff) { + var chunks = []; + if (!diff.length) return chunks; + var startEdit = 0, startOrig = 0; + var edit = Pos(0, 0), orig = Pos(0, 0); + for (var i = 0; i < diff.length; ++i) { + var part = diff[i], tp = part[0]; + if (tp == DIFF_EQUAL) { + var startOff = !startOfLineClean(diff, i) || edit.line < startEdit || orig.line < startOrig ? 1 : 0; + var cleanFromEdit = edit.line + startOff, cleanFromOrig = orig.line + startOff; + moveOver(edit, part[1], null, orig); + var endOff = endOfLineClean(diff, i) ? 1 : 0; + var cleanToEdit = edit.line + endOff, cleanToOrig = orig.line + endOff; + if (cleanToEdit > cleanFromEdit) { + if (i) chunks.push({origFrom: startOrig, origTo: cleanFromOrig, + editFrom: startEdit, editTo: cleanFromEdit}); + startEdit = cleanToEdit; startOrig = cleanToOrig; + } + } else { + moveOver(tp == DIFF_INSERT ? edit : orig, part[1]); + } + } + if (startEdit <= edit.line || startOrig <= orig.line) + chunks.push({origFrom: startOrig, origTo: orig.line + 1, + editFrom: startEdit, editTo: edit.line + 1}); + return chunks; + } + + function endOfLineClean(diff, i) { + if (i == diff.length - 1) return true; + var next = diff[i + 1][1]; + if ((next.length == 1 && i < diff.length - 2) || next.charCodeAt(0) != 10) return false; + if (i == diff.length - 2) return true; + next = diff[i + 2][1]; + return (next.length > 1 || i == diff.length - 3) && next.charCodeAt(0) == 10; + } + + function startOfLineClean(diff, i) { + if (i == 0) return true; + var last = diff[i - 1][1]; + if (last.charCodeAt(last.length - 1) != 10) return false; + if (i == 1) return true; + last = diff[i - 2][1]; + return last.charCodeAt(last.length - 1) == 10; + } + + function chunkBoundariesAround(chunks, n, nInEdit) { + var beforeE, afterE, beforeO, afterO; + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + var fromLocal = nInEdit ? chunk.editFrom : chunk.origFrom; + var toLocal = nInEdit ? chunk.editTo : chunk.origTo; + if (afterE == null) { + if (fromLocal > n) { afterE = chunk.editFrom; afterO = chunk.origFrom; } + else if (toLocal > n) { afterE = chunk.editTo; afterO = chunk.origTo; } + } + if (toLocal <= n) { beforeE = chunk.editTo; beforeO = chunk.origTo; } + else if (fromLocal <= n) { beforeE = chunk.editFrom; beforeO = chunk.origFrom; } + } + return {edit: {before: beforeE, after: afterE}, orig: {before: beforeO, after: afterO}}; + } + + function collapseSingle(cm, from, to) { + cm.addLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); + var widget = document.createElement("span"); + widget.className = "CodeMirror-merge-collapsed-widget"; + widget.title = cm.phrase("Identical text collapsed. Click to expand."); + var mark = cm.markText(Pos(from, 0), Pos(to - 1), { + inclusiveLeft: true, + inclusiveRight: true, + replacedWith: widget, + clearOnEnter: true + }); + function clear() { + mark.clear(); + cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); + } + if (mark.explicitlyCleared) clear(); + CodeMirror.on(widget, "click", clear); + mark.on("clear", clear); + CodeMirror.on(widget, "click", clear); + return {mark: mark, clear: clear}; + } + + function collapseStretch(size, editors) { + var marks = []; + function clear() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + } + for (var i = 0; i < editors.length; i++) { + var editor = editors[i]; + var mark = collapseSingle(editor.cm, editor.line, editor.line + size); + marks.push(mark); + mark.mark.on("clear", clear); + } + return marks[0].mark; + } + + function unclearNearChunks(dv, margin, off, clear) { + for (var i = 0; i < dv.chunks.length; i++) { + var chunk = dv.chunks[i]; + for (var l = chunk.editFrom - margin; l < chunk.editTo + margin; l++) { + var pos = l + off; + if (pos >= 0 && pos < clear.length) clear[pos] = false; + } + } + } + + function collapseIdenticalStretches(mv, margin) { + if (typeof margin != "number") margin = 2; + var clear = [], edit = mv.editor(), off = edit.firstLine(); + for (var l = off, e = edit.lastLine(); l <= e; l++) clear.push(true); + if (mv.left) unclearNearChunks(mv.left, margin, off, clear); + if (mv.right) unclearNearChunks(mv.right, margin, off, clear); + + for (var i = 0; i < clear.length; i++) { + if (clear[i]) { + var line = i + off; + for (var size = 1; i < clear.length - 1 && clear[i + 1]; i++, size++) {} + if (size > margin) { + var editors = [{line: line, cm: edit}]; + if (mv.left) editors.push({line: getMatchingOrigLine(line, mv.left.chunks), cm: mv.left.orig}); + if (mv.right) editors.push({line: getMatchingOrigLine(line, mv.right.chunks), cm: mv.right.orig}); + var mark = collapseStretch(size, editors); + if (mv.options.onCollapse) mv.options.onCollapse(mv, line, size, mark); + } + } + } + } + + // General utilities + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + function clear(node) { + for (var count = node.childNodes.length; count > 0; --count) + node.removeChild(node.firstChild); + } + + function attrs(elt) { + for (var i = 1; i < arguments.length; i += 2) + elt.setAttribute(arguments[i], arguments[i+1]); + } + + function copyObj(obj, target) { + if (!target) target = {}; + for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; + return target; + } + + function moveOver(pos, str, copy, other) { + var out = copy ? Pos(pos.line, pos.ch) : pos, at = 0; + for (;;) { + var nl = str.indexOf("\n", at); + if (nl == -1) break; + ++out.line; + if (other) ++other.line; + at = nl + 1; + } + out.ch = (at ? 0 : out.ch) + (str.length - at); + if (other) other.ch = (at ? 0 : other.ch) + (str.length - at); + return out; + } + + // Tracks collapsed markers and line widgets, in order to be able to + // accurately align the content of two editors. + + var F_WIDGET = 1, F_WIDGET_BELOW = 2, F_MARKER = 4 + + function TrackAlignable(cm) { + this.cm = cm + this.alignable = [] + this.height = cm.doc.height + var self = this + cm.on("markerAdded", function(_, marker) { + if (!marker.collapsed) return + var found = marker.find(1) + if (found != null) self.set(found.line, F_MARKER) + }) + cm.on("markerCleared", function(_, marker, _min, max) { + if (max != null && marker.collapsed) + self.check(max, F_MARKER, self.hasMarker) + }) + cm.on("markerChanged", this.signal.bind(this)) + cm.on("lineWidgetAdded", function(_, widget, lineNo) { + if (widget.mergeSpacer) return + if (widget.above) self.set(lineNo - 1, F_WIDGET_BELOW) + else self.set(lineNo, F_WIDGET) + }) + cm.on("lineWidgetCleared", function(_, widget, lineNo) { + if (widget.mergeSpacer) return + if (widget.above) self.check(lineNo - 1, F_WIDGET_BELOW, self.hasWidgetBelow) + else self.check(lineNo, F_WIDGET, self.hasWidget) + }) + cm.on("lineWidgetChanged", this.signal.bind(this)) + cm.on("change", function(_, change) { + var start = change.from.line, nBefore = change.to.line - change.from.line + var nAfter = change.text.length - 1, end = start + nAfter + if (nBefore || nAfter) self.map(start, nBefore, nAfter) + self.check(end, F_MARKER, self.hasMarker) + if (nBefore || nAfter) self.check(change.from.line, F_MARKER, self.hasMarker) + }) + cm.on("viewportChange", function() { + if (self.cm.doc.height != self.height) self.signal() + }) + } + + TrackAlignable.prototype = { + signal: function() { + CodeMirror.signal(this, "realign") + this.height = this.cm.doc.height + }, + + set: function(n, flags) { + var pos = -1 + for (; pos < this.alignable.length; pos += 2) { + var diff = this.alignable[pos] - n + if (diff == 0) { + if ((this.alignable[pos + 1] & flags) == flags) return + this.alignable[pos + 1] |= flags + this.signal() + return + } + if (diff > 0) break + } + this.signal() + this.alignable.splice(pos, 0, n, flags) + }, + + find: function(n) { + for (var i = 0; i < this.alignable.length; i += 2) + if (this.alignable[i] == n) return i + return -1 + }, + + check: function(n, flag, pred) { + var found = this.find(n) + if (found == -1 || !(this.alignable[found + 1] & flag)) return + if (!pred.call(this, n)) { + this.signal() + var flags = this.alignable[found + 1] & ~flag + if (flags) this.alignable[found + 1] = flags + else this.alignable.splice(found, 2) + } + }, + + hasMarker: function(n) { + var handle = this.cm.getLineHandle(n) + if (handle.markedSpans) for (var i = 0; i < handle.markedSpans.length; i++) + if (handle.markedSpans[i].marker.collapsed && handle.markedSpans[i].to != null) + return true + return false + }, + + hasWidget: function(n) { + var handle = this.cm.getLineHandle(n) + if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) + if (!handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true + return false + }, + + hasWidgetBelow: function(n) { + if (n == this.cm.lastLine()) return false + var handle = this.cm.getLineHandle(n + 1) + if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) + if (handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true + return false + }, + + map: function(from, nBefore, nAfter) { + var diff = nAfter - nBefore, to = from + nBefore, widgetFrom = -1, widgetTo = -1 + for (var i = 0; i < this.alignable.length; i += 2) { + var n = this.alignable[i] + if (n == from && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetFrom = i + if (n == to && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetTo = i + if (n <= from) continue + else if (n < to) this.alignable.splice(i--, 2) + else this.alignable[i] += diff + } + if (widgetFrom > -1) { + var flags = this.alignable[widgetFrom + 1] + if (flags == F_WIDGET_BELOW) this.alignable.splice(widgetFrom, 2) + else this.alignable[widgetFrom + 1] = flags & ~F_WIDGET_BELOW + } + if (widgetTo > -1 && nAfter) + this.set(from + nAfter, F_WIDGET_BELOW) + } + } + + function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; } + function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; } + function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } + + function findPrevDiff(chunks, start, isOrig) { + for (var i = chunks.length - 1; i >= 0; i--) { + var chunk = chunks[i]; + var to = (isOrig ? chunk.origTo : chunk.editTo) - 1; + if (to < start) return to; + } + } + + function findNextDiff(chunks, start, isOrig) { + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + var from = (isOrig ? chunk.origFrom : chunk.editFrom); + if (from > start) return from; + } + } + + function goNearbyDiff(cm, dir) { + var found = null, views = cm.state.diffViews, line = cm.getCursor().line; + if (views) for (var i = 0; i < views.length; i++) { + var dv = views[i], isOrig = cm == dv.orig; + ensureDiff(dv); + var pos = dir < 0 ? findPrevDiff(dv.chunks, line, isOrig) : findNextDiff(dv.chunks, line, isOrig); + if (pos != null && (found == null || (dir < 0 ? pos > found : pos < found))) + found = pos; + } + if (found != null) + cm.setCursor(found, 0); + else + return CodeMirror.Pass; + } + + CodeMirror.commands.goNextDiff = function(cm) { + return goNearbyDiff(cm, 1); + }; + CodeMirror.commands.goPrevDiff = function(cm) { + return goNearbyDiff(cm, -1); + }; +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/annotatescrollbar.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/annotatescrollbar.js new file mode 100644 index 0000000..c12e44c --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/annotatescrollbar.js @@ -0,0 +1,128 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineExtension("annotateScrollbar", function(options) { + if (typeof options == "string") options = {className: options}; + return new Annotation(this, options); + }); + + CodeMirror.defineOption("scrollButtonHeight", 0); + + function Annotation(cm, options) { + this.cm = cm; + this.options = options; + this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); + this.annotations = []; + this.doRedraw = this.doUpdate = null; + this.div = cm.getWrapperElement().appendChild(document.createElement("div")); + this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; + this.computeScale(); + + function scheduleRedraw(delay) { + clearTimeout(self.doRedraw); + self.doRedraw = setTimeout(function() { self.redraw(); }, delay); + } + + var self = this; + cm.on("refresh", this.resizeHandler = function() { + clearTimeout(self.doUpdate); + self.doUpdate = setTimeout(function() { + if (self.computeScale()) scheduleRedraw(20); + }, 100); + }); + cm.on("markerAdded", this.resizeHandler); + cm.on("markerCleared", this.resizeHandler); + if (options.listenForChanges !== false) + cm.on("changes", this.changeHandler = function() { + scheduleRedraw(250); + }); + } + + Annotation.prototype.computeScale = function() { + var cm = this.cm; + var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / + cm.getScrollerElement().scrollHeight + if (hScale != this.hScale) { + this.hScale = hScale; + return true; + } + }; + + Annotation.prototype.update = function(annotations) { + this.annotations = annotations; + this.redraw(); + }; + + Annotation.prototype.redraw = function(compute) { + if (compute !== false) this.computeScale(); + var cm = this.cm, hScale = this.hScale; + + var frag = document.createDocumentFragment(), anns = this.annotations; + + var wrapping = cm.getOption("lineWrapping"); + var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; + var curLine = null, curLineObj = null; + + function getY(pos, top) { + if (curLine != pos.line) { + curLine = pos.line + curLineObj = cm.getLineHandle(pos.line) + var visual = cm.getLineHandleVisualStart(curLineObj) + if (visual != curLineObj) { + curLine = cm.getLineNumber(visual) + curLineObj = visual + } + } + if ((curLineObj.widgets && curLineObj.widgets.length) || + (wrapping && curLineObj.height > singleLineH)) + return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; + var topY = cm.heightAtLine(curLineObj, "local"); + return topY + (top ? 0 : curLineObj.height); + } + + var lastLine = cm.lastLine() + if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { + var ann = anns[i]; + if (ann.to.line > lastLine) continue; + var top = nextTop || getY(ann.from, true) * hScale; + var bottom = getY(ann.to, false) * hScale; + while (i < anns.length - 1) { + if (anns[i + 1].to.line > lastLine) break; + nextTop = getY(anns[i + 1].from, true) * hScale; + if (nextTop > bottom + .9) break; + ann = anns[++i]; + bottom = getY(ann.to, false) * hScale; + } + if (bottom == top) continue; + var height = Math.max(bottom - top, 3); + + var elt = frag.appendChild(document.createElement("div")); + elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " + + (top + this.buttonHeight) + "px; height: " + height + "px"; + elt.className = this.options.className; + if (ann.id) { + elt.setAttribute("annotation-id", ann.id); + } + } + this.div.textContent = ""; + this.div.appendChild(frag); + }; + + Annotation.prototype.clear = function() { + this.cm.off("refresh", this.resizeHandler); + this.cm.off("markerAdded", this.resizeHandler); + this.cm.off("markerCleared", this.resizeHandler); + if (this.changeHandler) this.cm.off("changes", this.changeHandler); + this.div.parentNode.removeChild(this.div); + }; +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/scrollpastend.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/scrollpastend.js new file mode 100644 index 0000000..2ed9d95 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/scrollpastend.js @@ -0,0 +1,48 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("change", onChange); + cm.off("refresh", updateBottomMargin); + cm.display.lineSpace.parentNode.style.paddingBottom = ""; + cm.state.scrollPastEndPadding = null; + } + if (val) { + cm.on("change", onChange); + cm.on("refresh", updateBottomMargin); + updateBottomMargin(cm); + } + }); + + function onChange(cm, change) { + if (CodeMirror.changeEnd(change).line == cm.lastLine()) + updateBottomMargin(cm); + } + + function updateBottomMargin(cm) { + var padding = ""; + if (cm.lineCount() > 1) { + var totalH = cm.display.scroller.clientHeight - 30, + lastLineH = cm.getLineHandle(cm.lastLine()).height; + padding = (totalH - lastLineH) + "px"; + } + if (cm.state.scrollPastEndPadding != padding) { + cm.state.scrollPastEndPadding = padding; + cm.display.lineSpace.parentNode.style.paddingBottom = padding; + cm.off("refresh", updateBottomMargin); + cm.setSize(); + cm.on("refresh", updateBottomMargin); + } + } +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.css new file mode 100644 index 0000000..b3241e2 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.css @@ -0,0 +1,66 @@ +.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { + position: absolute; + background: #ccc; + -moz-box-sizing: border-box; + box-sizing: border-box; + border: 1px solid #bbb; + border-radius: 2px; +} + +.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { + position: absolute; + z-index: 6; + background: #2d425d; +} + +.CodeMirror-simplescroll-horizontal { + bottom: 0; left: 0; + height: 8px; +} +.CodeMirror-simplescroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-simplescroll-vertical { + right: 0; top: 0; + width: 8px; +} +.CodeMirror-simplescroll-vertical div { + right: 0; + width: 100%; +} + + +.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { + display: none; +} + +.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { + position: absolute; + background: #bcd; + border-radius: 3px; +} + +.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { + position: absolute; + z-index: 6; +} + +.CodeMirror-overlayscroll-horizontal { + bottom: 0; left: 0; + height: 6px; +} +.CodeMirror-overlayscroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-overlayscroll-vertical { + right: 0; top: 0; + width: 6px; +} +.CodeMirror-overlayscroll-vertical div { + right: 0; + width: 100%; +} diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.js new file mode 100644 index 0000000..750a2bd --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/scroll/simplescrollbars.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function Bar(cls, orientation, scroll) { + this.orientation = orientation; + this.scroll = scroll; + this.screen = this.total = this.size = 1; + this.pos = 0; + + this.node = document.createElement("div"); + this.node.className = cls + "-" + orientation; + this.inner = this.node.appendChild(document.createElement("div")); + + var self = this; + CodeMirror.on(this.inner, "mousedown", function(e) { + if (e.which != 1) return; + CodeMirror.e_preventDefault(e); + var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; + var start = e[axis], startpos = self.pos; + function done() { + CodeMirror.off(document, "mousemove", move); + CodeMirror.off(document, "mouseup", done); + } + function move(e) { + if (e.which != 1) return done(); + self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); + } + CodeMirror.on(document, "mousemove", move); + CodeMirror.on(document, "mouseup", done); + }); + + CodeMirror.on(this.node, "click", function(e) { + CodeMirror.e_preventDefault(e); + var innerBox = self.inner.getBoundingClientRect(), where; + if (self.orientation == "horizontal") + where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; + else + where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; + self.moveTo(self.pos + where * self.screen); + }); + + function onWheel(e) { + var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; + var oldPos = self.pos; + self.moveTo(self.pos + moved); + if (self.pos != oldPos) CodeMirror.e_preventDefault(e); + } + CodeMirror.on(this.node, "mousewheel", onWheel); + CodeMirror.on(this.node, "DOMMouseScroll", onWheel); + } + + Bar.prototype.setPos = function(pos, force) { + if (pos < 0) pos = 0; + if (pos > this.total - this.screen) pos = this.total - this.screen; + if (!force && pos == this.pos) return false; + this.pos = pos; + this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = + (pos * (this.size / this.total)) + "px"; + return true + }; + + Bar.prototype.moveTo = function(pos) { + if (this.setPos(pos)) this.scroll(pos, this.orientation); + } + + var minButtonSize = 10; + + Bar.prototype.update = function(scrollSize, clientSize, barSize) { + var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize + if (sizeChanged) { + this.screen = clientSize; + this.total = scrollSize; + this.size = barSize; + } + + var buttonSize = this.screen * (this.size / this.total); + if (buttonSize < minButtonSize) { + this.size -= minButtonSize - buttonSize; + buttonSize = minButtonSize; + } + this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = + buttonSize + "px"; + this.setPos(this.pos, sizeChanged); + }; + + function SimpleScrollbars(cls, place, scroll) { + this.addClass = cls; + this.horiz = new Bar(cls, "horizontal", scroll); + place(this.horiz.node); + this.vert = new Bar(cls, "vertical", scroll); + place(this.vert.node); + this.width = null; + } + + SimpleScrollbars.prototype.update = function(measure) { + if (this.width == null) { + var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; + if (style) this.width = parseInt(style.height); + } + var width = this.width || 0; + + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + this.vert.node.style.display = needsV ? "block" : "none"; + this.horiz.node.style.display = needsH ? "block" : "none"; + + if (needsV) { + this.vert.update(measure.scrollHeight, measure.clientHeight, + measure.viewHeight - (needsH ? width : 0)); + this.vert.node.style.bottom = needsH ? width + "px" : "0"; + } + if (needsH) { + this.horiz.update(measure.scrollWidth, measure.clientWidth, + measure.viewWidth - (needsV ? width : 0) - measure.barLeft); + this.horiz.node.style.right = needsV ? width + "px" : "0"; + this.horiz.node.style.left = measure.barLeft + "px"; + } + + return {right: needsV ? width : 0, bottom: needsH ? width : 0}; + }; + + SimpleScrollbars.prototype.setScrollTop = function(pos) { + this.vert.setPos(pos); + }; + + SimpleScrollbars.prototype.setScrollLeft = function(pos) { + this.horiz.setPos(pos); + }; + + SimpleScrollbars.prototype.clear = function() { + var parent = this.horiz.node.parentNode; + parent.removeChild(this.horiz.node); + parent.removeChild(this.vert.node); + }; + + CodeMirror.scrollbarModel.simple = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); + }; + CodeMirror.scrollbarModel.overlay = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); + }; +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/jump-to-line.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/jump-to-line.js new file mode 100644 index 0000000..a04de38 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/jump-to-line.js @@ -0,0 +1,53 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +// Defines jumpToLine command. Uses dialog.js if present. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + // default search panel location + CodeMirror.defineOption("search", {bottom: false}); + + function dialog(cm, text, shortText, deflt, f) { + if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom}); + else f(prompt(shortText, deflt)); + } + + function getJumpDialog(cm) { + return cm.phrase("Jump to line:") + ' ' + cm.phrase("(Use line:column or scroll% syntax)") + ''; + } + + function interpretLine(cm, string) { + var num = Number(string) + if (/^[-+]/.test(string)) return cm.getCursor().line + num + else return num - 1 + } + + CodeMirror.commands.jumpToLine = function(cm) { + var cur = cm.getCursor(); + dialog(cm, getJumpDialog(cm), cm.phrase("Jump to line:"), (cur.line + 1) + ":" + cur.ch, function(posStr) { + if (!posStr) return; + + var match; + if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { + cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) + } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { + var line = Math.round(cm.lineCount() * Number(match[1]) / 100); + if (/^[-+]/.test(match[1])) line = cur.line + line + 1; + cm.setCursor(line - 1, cur.ch); + } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { + cm.setCursor(interpretLine(cm, match[1]), cur.ch); + } + }); + }; + + CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/search.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/search.js new file mode 100644 index 0000000..360a498 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/search.js @@ -0,0 +1,295 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +// Define search commands. Depends on dialog.js or another +// implementation of the openDialog method. + +// Replace works a little oddly -- it will do the replace on the next +// Ctrl-G (or whatever is bound to findNext) press. You prevent a +// replace by making sure the match is no longer selected when hitting +// Ctrl-G. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + // default search panel location + CodeMirror.defineOption("search", {bottom: false}); + + function searchOverlay(query, caseInsensitive) { + if (typeof query == "string") + query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); + else if (!query.global) + query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); + + return {token: function(stream) { + query.lastIndex = stream.pos; + var match = query.exec(stream.string); + if (match && match.index == stream.pos) { + stream.pos += match[0].length || 1; + return "searching"; + } else if (match) { + stream.pos = match.index; + } else { + stream.skipToEnd(); + } + }}; + } + + function SearchState() { + this.posFrom = this.posTo = this.lastQuery = this.query = null; + this.overlay = null; + } + + function getSearchState(cm) { + return cm.state.search || (cm.state.search = new SearchState()); + } + + function queryCaseInsensitive(query) { + return typeof query == "string" && query == query.toLowerCase(); + } + + function getSearchCursor(cm, query, pos) { + // Heuristic: if the query string is all lowercase, do a case insensitive search. + return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true}); + } + + function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { + cm.openDialog(text, onEnter, { + value: deflt, + selectValueOnOpen: true, + closeOnEnter: false, + onClose: function() { clearSearch(cm); }, + onKeyDown: onKeyDown, + bottom: cm.options.search.bottom + }); + } + + function dialog(cm, text, shortText, deflt, f) { + if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom}); + else f(prompt(shortText, deflt)); + } + + function confirmDialog(cm, text, shortText, fs) { + if (cm.openConfirm) cm.openConfirm(text, fs); + else if (confirm(shortText)) fs[0](); + } + + function parseString(string) { + return string.replace(/\\([nrt\\])/g, function(match, ch) { + if (ch == "n") return "\n" + if (ch == "r") return "\r" + if (ch == "t") return "\t" + if (ch == "\\") return "\\" + return match + }) + } + + function parseQuery(query) { + var isRE = query.match(/^\/(.*)\/([a-z]*)$/); + if (isRE) { + try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } + catch(e) {} // Not a regular expression after all, do a string search + } else { + query = parseString(query) + } + if (typeof query == "string" ? query == "" : query.test("")) + query = /x^/; + return query; + } + + function startSearch(cm, state, query) { + state.queryText = query; + state.query = parseQuery(query); + cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); + state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); + cm.addOverlay(state.overlay); + if (cm.showMatchesOnScrollbar) { + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); + } + } + + function doSearch(cm, rev, persistent, immediate) { + var state = getSearchState(cm); + if (state.query) return findNext(cm, rev); + var q = cm.getSelection() || state.lastQuery; + if (q instanceof RegExp && q.source == "x^") q = null + if (persistent && cm.openDialog) { + var hiding = null + var searchNext = function(query, event) { + CodeMirror.e_stop(event); + if (!query) return; + if (query != state.queryText) { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + } + if (hiding) hiding.style.opacity = 1 + findNext(cm, event.shiftKey, function(_, to) { + var dialog + if (to.line < 3 && document.querySelector && + (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && + dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) + (hiding = dialog).style.opacity = .4 + }) + }; + persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) { + var keyName = CodeMirror.keyName(event) + var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName] + if (cmd == "findNext" || cmd == "findPrev" || + cmd == "findPersistentNext" || cmd == "findPersistentPrev") { + CodeMirror.e_stop(event); + startSearch(cm, getSearchState(cm), query); + cm.execCommand(cmd); + } else if (cmd == "find" || cmd == "findPersistent") { + CodeMirror.e_stop(event); + searchNext(query, event); + } + }); + if (immediate && q) { + startSearch(cm, state, q); + findNext(cm, rev); + } + } else { + dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) { + if (query && !state.query) cm.operation(function() { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + findNext(cm, rev); + }); + }); + } + } + + function findNext(cm, rev, callback) {cm.operation(function() { + var state = getSearchState(cm); + var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); + if (!cursor.find(rev)) { + cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); + if (!cursor.find(rev)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); + state.posFrom = cursor.from(); state.posTo = cursor.to(); + if (callback) callback(cursor.from(), cursor.to()) + });} + + function clearSearch(cm) {cm.operation(function() { + var state = getSearchState(cm); + state.lastQuery = state.query; + if (!state.query) return; + state.query = state.queryText = null; + cm.removeOverlay(state.overlay); + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + });} + + function el(tag, attrs) { + var element = tag ? document.createElement(tag) : document.createDocumentFragment(); + for (var key in attrs) { + element[key] = attrs[key]; + } + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i] + element.appendChild(typeof child == "string" ? document.createTextNode(child) : child); + } + return element; + } + + function getQueryDialog(cm) { + var label = el("label", {className: "CodeMirror-search-label"}, + cm.phrase("Search:"), + el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field", + id: "CodeMirror-search-field"})); + label.setAttribute("for","CodeMirror-search-field"); + return el("", null, label, " ", + el("span", {style: "color: #666", className: "CodeMirror-search-hint"}, + cm.phrase("(Use /re/ syntax for regexp search)"))); + } + function getReplaceQueryDialog(cm) { + return el("", null, " ", + el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field"}), " ", + el("span", {style: "color: #666", className: "CodeMirror-search-hint"}, + cm.phrase("(Use /re/ syntax for regexp search)"))); + } + function getReplacementQueryDialog(cm) { + return el("", null, + el("span", {className: "CodeMirror-search-label"}, cm.phrase("With:")), " ", + el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field"})); + } + function getDoReplaceConfirm(cm) { + return el("", null, + el("span", {className: "CodeMirror-search-label"}, cm.phrase("Replace?")), " ", + el("button", {}, cm.phrase("Yes")), " ", + el("button", {}, cm.phrase("No")), " ", + el("button", {}, cm.phrase("All")), " ", + el("button", {}, cm.phrase("Stop"))); + } + + function replaceAll(cm, query, text) { + cm.operation(function() { + for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { + if (typeof query != "string") { + var match = cm.getRange(cursor.from(), cursor.to()).match(query); + cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + } else cursor.replace(text); + } + }); + } + + function replace(cm, all) { + if (cm.getOption("readOnly")) return; + var query = cm.getSelection() || getSearchState(cm).lastQuery; + var dialogText = all ? cm.phrase("Replace all:") : cm.phrase("Replace:") + var fragment = el("", null, + el("span", {className: "CodeMirror-search-label"}, dialogText), + getReplaceQueryDialog(cm)) + dialog(cm, fragment, dialogText, query, function(query) { + if (!query) return; + query = parseQuery(query); + dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function(text) { + text = parseString(text) + if (all) { + replaceAll(cm, query, text) + } else { + clearSearch(cm); + var cursor = getSearchCursor(cm, query, cm.getCursor("from")); + var advance = function() { + var start = cursor.from(), match; + if (!(match = cursor.findNext())) { + cursor = getSearchCursor(cm, query); + if (!(match = cursor.findNext()) || + (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); + confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"), + [function() {doReplace(match);}, advance, + function() {replaceAll(cm, query, text)}]); + }; + var doReplace = function(match) { + cursor.replace(typeof query == "string" ? text : + text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + advance(); + }; + advance(); + } + }); + }); + } + + CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; + CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; + CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; + CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; + CodeMirror.commands.findNext = doSearch; + CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; + CodeMirror.commands.clearSearch = clearSearch; + CodeMirror.commands.replace = replace; + CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/searchcursor.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/searchcursor.js new file mode 100644 index 0000000..ac94d24 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/addon/search/searchcursor.js @@ -0,0 +1,305 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod) + else // Plain browser env + mod(CodeMirror) +})(function(CodeMirror) { + "use strict" + var Pos = CodeMirror.Pos + + function regexpFlags(regexp) { + var flags = regexp.flags + return flags != null ? flags : (regexp.ignoreCase ? "i" : "") + + (regexp.global ? "g" : "") + + (regexp.multiline ? "m" : "") + } + + function ensureFlags(regexp, flags) { + var current = regexpFlags(regexp), target = current + for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1) + target += flags.charAt(i) + return current == target ? regexp : new RegExp(regexp.source, target) + } + + function maybeMultiline(regexp) { + return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) + } + + function searchRegexpForward(doc, regexp, start) { + regexp = ensureFlags(regexp, "g") + for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { + regexp.lastIndex = ch + var string = doc.getLine(line), match = regexp.exec(string) + if (match) + return {from: Pos(line, match.index), + to: Pos(line, match.index + match[0].length), + match: match} + } + } + + function searchRegexpForwardMultiline(doc, regexp, start) { + if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) + + regexp = ensureFlags(regexp, "gm") + var string, chunk = 1 + for (var line = start.line, last = doc.lastLine(); line <= last;) { + // This grows the search buffer in exponentially-sized chunks + // between matches, so that nearby matches are fast and don't + // require concatenating the whole document (in case we're + // searching for something that has tons of matches), but at the + // same time, the amount of retries is limited. + for (var i = 0; i < chunk; i++) { + if (line > last) break + var curLine = doc.getLine(line++) + string = string == null ? curLine : string + "\n" + curLine + } + chunk = chunk * 2 + regexp.lastIndex = start.ch + var match = regexp.exec(string) + if (match) { + var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") + var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length + return {from: Pos(startLine, startCh), + to: Pos(startLine + inside.length - 1, + inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), + match: match} + } + } + } + + function lastMatchIn(string, regexp, endMargin) { + var match, from = 0 + while (from <= string.length) { + regexp.lastIndex = from + var newMatch = regexp.exec(string) + if (!newMatch) break + var end = newMatch.index + newMatch[0].length + if (end > string.length - endMargin) break + if (!match || end > match.index + match[0].length) + match = newMatch + from = newMatch.index + 1 + } + return match + } + + function searchRegexpBackward(doc, regexp, start) { + regexp = ensureFlags(regexp, "g") + for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { + var string = doc.getLine(line) + var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch) + if (match) + return {from: Pos(line, match.index), + to: Pos(line, match.index + match[0].length), + match: match} + } + } + + function searchRegexpBackwardMultiline(doc, regexp, start) { + if (!maybeMultiline(regexp)) return searchRegexpBackward(doc, regexp, start) + regexp = ensureFlags(regexp, "gm") + var string, chunkSize = 1, endMargin = doc.getLine(start.line).length - start.ch + for (var line = start.line, first = doc.firstLine(); line >= first;) { + for (var i = 0; i < chunkSize && line >= first; i++) { + var curLine = doc.getLine(line--) + string = string == null ? curLine : curLine + "\n" + string + } + chunkSize *= 2 + + var match = lastMatchIn(string, regexp, endMargin) + if (match) { + var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") + var startLine = line + before.length, startCh = before[before.length - 1].length + return {from: Pos(startLine, startCh), + to: Pos(startLine + inside.length - 1, + inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), + match: match} + } + } + } + + var doFold, noFold + if (String.prototype.normalize) { + doFold = function(str) { return str.normalize("NFD").toLowerCase() } + noFold = function(str) { return str.normalize("NFD") } + } else { + doFold = function(str) { return str.toLowerCase() } + noFold = function(str) { return str } + } + + // Maps a position in a case-folded line back to a position in the original line + // (compensating for codepoints increasing in number during folding) + function adjustPos(orig, folded, pos, foldFunc) { + if (orig.length == folded.length) return pos + for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { + if (min == max) return min + var mid = (min + max) >> 1 + var len = foldFunc(orig.slice(0, mid)).length + if (len == pos) return mid + else if (len > pos) max = mid + else min = mid + 1 + } + } + + function searchStringForward(doc, query, start, caseFold) { + // Empty string would match anything and never progress, so we + // define it to match nothing instead. + if (!query.length) return null + var fold = caseFold ? doFold : noFold + var lines = fold(query).split(/\r|\n\r?/) + + search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { + var orig = doc.getLine(line).slice(ch), string = fold(orig) + if (lines.length == 1) { + var found = string.indexOf(lines[0]) + if (found == -1) continue search + var start = adjustPos(orig, string, found, fold) + ch + return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), + to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} + } else { + var cutFrom = string.length - lines[0].length + if (string.slice(cutFrom) != lines[0]) continue search + for (var i = 1; i < lines.length - 1; i++) + if (fold(doc.getLine(line + i)) != lines[i]) continue search + var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] + if (endString.slice(0, lastLine.length) != lastLine) continue search + return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), + to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} + } + } + } + + function searchStringBackward(doc, query, start, caseFold) { + if (!query.length) return null + var fold = caseFold ? doFold : noFold + var lines = fold(query).split(/\r|\n\r?/) + + search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { + var orig = doc.getLine(line) + if (ch > -1) orig = orig.slice(0, ch) + var string = fold(orig) + if (lines.length == 1) { + var found = string.lastIndexOf(lines[0]) + if (found == -1) continue search + return {from: Pos(line, adjustPos(orig, string, found, fold)), + to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} + } else { + var lastLine = lines[lines.length - 1] + if (string.slice(0, lastLine.length) != lastLine) continue search + for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) + if (fold(doc.getLine(start + i)) != lines[i]) continue search + var top = doc.getLine(line + 1 - lines.length), topString = fold(top) + if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search + return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), + to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} + } + } + } + + function SearchCursor(doc, query, pos, options) { + this.atOccurrence = false + this.afterEmptyMatch = false + this.doc = doc + pos = pos ? doc.clipPos(pos) : Pos(0, 0) + this.pos = {from: pos, to: pos} + + var caseFold + if (typeof options == "object") { + caseFold = options.caseFold + } else { // Backwards compat for when caseFold was the 4th argument + caseFold = options + options = null + } + + if (typeof query == "string") { + if (caseFold == null) caseFold = false + this.matches = function(reverse, pos) { + return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) + } + } else { + query = ensureFlags(query, "gm") + if (!options || options.multiline !== false) + this.matches = function(reverse, pos) { + return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) + } + else + this.matches = function(reverse, pos) { + return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) + } + } + } + + SearchCursor.prototype = { + findNext: function() {return this.find(false)}, + findPrevious: function() {return this.find(true)}, + + find: function(reverse) { + var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to); + if (this.afterEmptyMatch && this.atOccurrence) { + // do not return the same 0 width match twice + head = Pos(head.line, head.ch) + if (reverse) { + head.ch--; + if (head.ch < 0) { + head.line--; + head.ch = (this.doc.getLine(head.line) || "").length; + } + } else { + head.ch++; + if (head.ch > (this.doc.getLine(head.line) || "").length) { + head.ch = 0; + head.line++; + } + } + if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) { + return this.atOccurrence = false + } + } + var result = this.matches(reverse, head) + this.afterEmptyMatch = result && CodeMirror.cmpPos(result.from, result.to) == 0 + + if (result) { + this.pos = result + this.atOccurrence = true + return this.pos.match || true + } else { + var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) + this.pos = {from: end, to: end} + return this.atOccurrence = false + } + }, + + from: function() {if (this.atOccurrence) return this.pos.from}, + to: function() {if (this.atOccurrence) return this.pos.to}, + + replace: function(newText, origin) { + if (!this.atOccurrence) return + var lines = CodeMirror.splitLines(newText) + this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) + this.pos.to = Pos(this.pos.from.line + lines.length - 1, + lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) + } + } + + CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this.doc, query, pos, caseFold) + }) + CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this, query, pos, caseFold) + }) + + CodeMirror.defineExtension("selectMatches", function(query, caseFold) { + var ranges = [] + var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) + while (cur.findNext()) { + if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break + ranges.push({anchor: cur.from(), head: cur.to()}) + } + if (ranges.length) + this.setSelections(ranges, 0) + }) +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.css new file mode 100644 index 0000000..36586e3 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.css @@ -0,0 +1,350 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; + direction: ltr; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: transparent; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #ffffff; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: 0; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 50px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -50px; margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; + outline: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -50px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre.CodeMirror-line, +.CodeMirror-wrap pre.CodeMirror-line-like { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.js new file mode 100644 index 0000000..149b6fd --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/lib/codemirror.js @@ -0,0 +1,9800 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// This is CodeMirror (https://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.CodeMirror = factory()); +}(this, (function () { 'use strict'; + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); + var edge = /Edge\/(\d+)/.exec(userAgent); + var ie = ie_upto10 || ie_11up || edge; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); + var webkit = !edge && /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = !edge && /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = safari && (/Mobile\/\w+/.test(userAgent) || navigator.maxTouchPoints > 2); + var android = /Android/.test(userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var chromeOS = /\bCrOS\b/.test(userAgent); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) { presto_version = Number(presto_version[1]); } + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } + + var rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + { e.removeChild(e.firstChild); } + return e + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e) + } + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) { e.className = className; } + if (style) { e.style.cssText = style; } + if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } + else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } + return e + } + // wrapper for elt, which removes the elt from the accessibility tree + function eltP(tag, content, className, style) { + var e = elt(tag, content, className, style); + e.setAttribute("role", "presentation"); + return e + } + + var range; + if (document.createRange) { range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r + }; } + else { range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r + }; } + + function contains(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + { child = child.parentNode; } + if (parent.contains) + { return parent.contains(child) } + do { + if (child.nodeType == 11) { child = child.host; } + if (child == parent) { return true } + } while (child = child.parentNode) + } + + function activeElt() { + // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. + // IE < 10 will throw when accessed while the page is loading or in an iframe. + // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. + var activeElement; + try { + activeElement = document.activeElement; + } catch(e) { + activeElement = document.body || null; + } + while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) + { activeElement = activeElement.shadowRoot.activeElement; } + return activeElement + } + + function addClass(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } + } + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } + return b + } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } + else if (ie) // Suppress mysterious IE10 errors + { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args)} + } + + function copyObj(obj, target, overwrite) { + if (!target) { target = {}; } + for (var prop in obj) + { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + { target[prop] = obj[prop]; } } + return target + } + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) { end = string.length; } + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + { return n + (end - i) } + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + } + + var Delayed = function() { + this.id = null; + this.f = null; + this.time = 0; + this.handler = bind(this.onTimeout, this); + }; + Delayed.prototype.onTimeout = function (self) { + self.id = 0; + if (self.time <= +new Date) { + self.f(); + } else { + setTimeout(self.handler, self.time - +new Date); + } + }; + Delayed.prototype.set = function (ms, f) { + this.f = f; + var time = +new Date + ms; + if (!this.id || time < this.time) { + clearTimeout(this.id); + this.id = setTimeout(this.handler, ms); + this.time = time; + } + }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + { if (array[i] == elt) { return i } } + return -1 + } + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 50; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = {toString: function(){return "CodeMirror.Pass"}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) { nextTab = string.length; } + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + { return pos + Math.min(skipped, goal - col) } + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) { return pos } + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + { spaceStrs.push(lst(spaceStrs) + " "); } + return spaceStrs[n] + } + + function lst(arr) { return arr[arr.length-1] } + + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } + return out + } + + function insertSorted(array, value, score) { + var pos = 0, priority = score(value); + while (pos < array.length && score(array[pos]) <= priority) { pos++; } + array.splice(pos, 0, value); + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) { copyObj(props, inst); } + return inst + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordCharBasic(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) + } + function isWordChar(ch, helper) { + if (!helper) { return isWordCharBasic(ch) } + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } + return helper.test(ch) + } + + function isEmpty(obj) { + for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } + return true + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } + + // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. + function skipExtendingChars(str, pos, dir) { + while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } + return pos + } + + // Returns the value from the range [`from`; `to`] that satisfies + // `pred` and is closest to `from`. Assumes that at least `to` + // satisfies `pred`. Supports `from` being greater than `to`. + function findFirst(pred, from, to) { + // At any point we are certain `to` satisfies `pred`, don't know + // whether `from` does. + var dir = from > to ? -1 : 1; + for (;;) { + if (from == to) { return from } + var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); + if (mid == from) { return pred(mid) ? from : to } + if (pred(mid)) { to = mid; } + else { from = mid + dir; } + } + } + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) { return f(from, to, "ltr", 0) } + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); + found = true; + } + } + if (!found) { f(from, to, "ltr"); } + } + + var bidiOther = null; + function getBidiPartAt(order, ch, sticky) { + var found; + bidiOther = null; + for (var i = 0; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < ch && cur.to > ch) { return i } + if (cur.to == ch) { + if (cur.from != cur.to && sticky == "before") { found = i; } + else { bidiOther = i; } + } + if (cur.from == ch) { + if (cur.from != cur.to && sticky != "before") { found = i; } + else { bidiOther = i; } + } + } + return found != null ? found : bidiOther + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6f9 + var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; + function charType(code) { + if (code <= 0xf7) { return lowTypes.charAt(code) } + else if (0x590 <= code && code <= 0x5f4) { return "R" } + else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } + else if (0x6ee <= code && code <= 0x8ac) { return "r" } + else if (0x2000 <= code && code <= 0x200b) { return "w" } + else if (code == 0x200c) { return "b" } + else { return "L" } + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str, direction) { + var outerType = direction == "ltr" ? "L" : "R"; + + if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } + var len = str.length, types = []; + for (var i = 0; i < len; ++i) + { types.push(charType(str.charCodeAt(i))); } + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { + var type = types[i$1]; + if (type == "m") { types[i$1] = prev; } + else { prev = type; } + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { + var type$1 = types[i$2]; + if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } + else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { + var type$2 = types[i$3]; + if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } + else if (type$2 == "," && prev$1 == types[i$3+1] && + (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } + prev$1 = type$2; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i$4 = 0; i$4 < len; ++i$4) { + var type$3 = types[i$4]; + if (type$3 == ",") { types[i$4] = "N"; } + else if (type$3 == "%") { + var end = (void 0); + for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i$4; j < end; ++j) { types[j] = replace; } + i$4 = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { + var type$4 = types[i$5]; + if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } + else if (isStrong.test(type$4)) { cur$1 = type$4; } + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i$6 = 0; i$6 < len; ++i$6) { + if (isNeutral.test(types[i$6])) { + var end$1 = (void 0); + for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} + var before = (i$6 ? types[i$6-1] : outerType) == "L"; + var after = (end$1 < len ? types[end$1] : outerType) == "L"; + var replace$1 = before == after ? (before ? "L" : "R") : outerType; + for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } + i$6 = end$1 - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i$7 = 0; i$7 < len;) { + if (countsAsLeft.test(types[i$7])) { + var start = i$7; + for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} + order.push(new BidiSpan(0, start, i$7)); + } else { + var pos = i$7, at = order.length, isRTL = direction == "rtl" ? 1 : 0; + for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} + for (var j$2 = pos; j$2 < i$7;) { + if (countsAsNum.test(types[j$2])) { + if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); at += isRTL; } + var nstart = j$2; + for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} + order.splice(at, 0, new BidiSpan(2, nstart, j$2)); + at += isRTL; + pos = j$2; + } else { ++j$2; } + } + if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } + } + } + if (direction == "ltr") { + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + } + + return direction == "rtl" ? order.reverse() : order + } + })(); + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line, direction) { + var order = line.order; + if (order == null) { order = line.order = bidiOrdering(line.text, direction); } + return order + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var noHandlers = []; + + var on = function(emitter, type, f) { + if (emitter.addEventListener) { + emitter.addEventListener(type, f, false); + } else if (emitter.attachEvent) { + emitter.attachEvent("on" + type, f); + } else { + var map = emitter._handlers || (emitter._handlers = {}); + map[type] = (map[type] || noHandlers).concat(f); + } + }; + + function getHandlers(emitter, type) { + return emitter._handlers && emitter._handlers[type] || noHandlers + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) { + emitter.removeEventListener(type, f, false); + } else if (emitter.detachEvent) { + emitter.detachEvent("on" + type, f); + } else { + var map = emitter._handlers, arr = map && map[type]; + if (arr) { + var index = indexOf(arr, f); + if (index > -1) + { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } + } + } + } + + function signal(emitter, type /*, values...*/) { + var handlers = getHandlers(emitter, type); + if (!handlers.length) { return } + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) { return } + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) + { set.push(arr[i]); } } + } + + function hasHandler(emitter, type) { + return getHandlers(emitter, type).length > 0 + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + function e_preventDefault(e) { + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } + } + function e_stopPropagation(e) { + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + + function e_target(e) {return e.target || e.srcElement} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) { b = 1; } + else if (e.button & 2) { b = 3; } + else if (e.button & 4) { b = 2; } + } + if (mac && e.ctrlKey && b == 1) { b = 3; } + return b + } + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) { return false } + var div = elt('div'); + return "draggable" in div || "dragDrop" in div + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) { return badBidiRects } + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + var r1 = range(txt, 1, 2).getBoundingClientRect(); + removeChildren(measure); + if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) + return badBidiRects = (r1.right - r0.right < 3) + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) { nl = string.length; } + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result + } : function (string) { return string.split(/\r\n?|\n/); }; + + var hasSelection = window.getSelection ? function (te) { + try { return te.selectionStart != te.selectionEnd } + catch(e) { return false } + } : function (te) { + var range; + try {range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) { return false } + return range.compareEndPoints("StartToEnd", range) != 0 + }; + + var hasCopyEvent = (function () { + var e = elt("div"); + if ("oncopy" in e) { return true } + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function" + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) { return badZoomedRects } + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 + } + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + function defineMode(name, mode) { + if (arguments.length > 2) + { mode.dependencies = Array.prototype.slice.call(arguments, 2); } + modes[name] = mode; + } + + function defineMIME(mime, spec) { + mimeModes[mime] = spec; + } + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + function resolveMode(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") { found = {name: found}; } + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return resolveMode("application/xml") + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { + return resolveMode("application/json") + } + if (typeof spec == "string") { return {name: spec} } + else { return spec || {name: "null"} } + } + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + function getMode(options, spec) { + spec = resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) { return getMode(options, "text/plain") } + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) { continue } + if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) { modeObj.helperType = spec.helperType; } + if (spec.modeProps) { for (var prop$1 in spec.modeProps) + { modeObj[prop$1] = spec.modeProps[prop$1]; } } + + return modeObj + } + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = {}; + function extendMode(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + } + + function copyState(mode, state) { + if (state === true) { return state } + if (mode.copyState) { return mode.copyState(state) } + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) { val = val.concat([]); } + nstate[n] = val; + } + return nstate + } + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + function innerMode(mode, state) { + var info; + while (mode.innerMode) { + info = mode.innerMode(state); + if (!info || info.mode == mode) { break } + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state} + } + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true + } + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = function(string, tabSize, lineOracle) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + this.lineOracle = lineOracle; + }; + + StringStream.prototype.eol = function () {return this.pos >= this.string.length}; + StringStream.prototype.sol = function () {return this.pos == this.lineStart}; + StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; + StringStream.prototype.next = function () { + if (this.pos < this.string.length) + { return this.string.charAt(this.pos++) } + }; + StringStream.prototype.eat = function (match) { + var ch = this.string.charAt(this.pos); + var ok; + if (typeof match == "string") { ok = ch == match; } + else { ok = ch && (match.test ? match.test(ch) : match(ch)); } + if (ok) {++this.pos; return ch} + }; + StringStream.prototype.eatWhile = function (match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start + }; + StringStream.prototype.eatSpace = function () { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this.pos; } + return this.pos > start + }; + StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; + StringStream.prototype.skipTo = function (ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true} + }; + StringStream.prototype.backUp = function (n) {this.pos -= n;}; + StringStream.prototype.column = function () { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.indentation = function () { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.match = function (pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) { this.pos += pattern.length; } + return true + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) { return null } + if (match && consume !== false) { this.pos += match[0].length; } + return match + } + }; + StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; + StringStream.prototype.hideFirstChars = function (n, inner) { + this.lineStart += n; + try { return inner() } + finally { this.lineStart -= n; } + }; + StringStream.prototype.lookAhead = function (n) { + var oracle = this.lineOracle; + return oracle && oracle.lookAhead(n) + }; + StringStream.prototype.baseToken = function () { + var oracle = this.lineOracle; + return oracle && oracle.baseToken(this.pos) + }; + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } + var chunk = doc; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break } + n -= sz; + } + } + return chunk.lines[n] + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function (line) { + var text = line.text; + if (n == end.line) { text = text.slice(0, end.ch); } + if (n == start.line) { text = text.slice(start.ch); } + out.push(text); + ++n; + }); + return out + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value + return out + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) { return null } + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) { break } + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { + var child = chunk.children[i$1], ch = child.height; + if (h < ch) { chunk = child; continue outer } + h -= ch; + n += child.chunkSize(); + } + return n + } while (!chunk.lines) + var i = 0; + for (; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) { break } + h -= lh; + } + return n + i + } + + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)) + } + + // A Pos instance represents a position within the text. + function Pos(line, ch, sticky) { + if ( sticky === void 0 ) sticky = null; + + if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } + this.line = line; + this.ch = ch; + this.sticky = sticky; + } + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + function cmp(a, b) { return a.line - b.line || a.ch - b.ch } + + function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } + + function copyPos(x) {return Pos(x.line, x.ch)} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} + function clipPos(doc, pos) { + if (pos.line < doc.first) { return Pos(doc.first, 0) } + var last = doc.first + doc.size - 1; + if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } + return clipToLen(pos, getLine(doc, pos.line).text.length) + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } + else if (ch < 0) { return Pos(pos.line, 0) } + else { return pos } + } + function clipPosArray(doc, array) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } + return out + } + + var SavedContext = function(state, lookAhead) { + this.state = state; + this.lookAhead = lookAhead; + }; + + var Context = function(doc, state, line, lookAhead) { + this.state = state; + this.doc = doc; + this.line = line; + this.maxLookAhead = lookAhead || 0; + this.baseTokens = null; + this.baseTokenPos = 1; + }; + + Context.prototype.lookAhead = function (n) { + var line = this.doc.getLine(this.line + n); + if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } + return line + }; + + Context.prototype.baseToken = function (n) { + if (!this.baseTokens) { return null } + while (this.baseTokens[this.baseTokenPos] <= n) + { this.baseTokenPos += 2; } + var type = this.baseTokens[this.baseTokenPos + 1]; + return {type: type && type.replace(/( |^)overlay .*/, ""), + size: this.baseTokens[this.baseTokenPos] - n} + }; + + Context.prototype.nextLine = function () { + this.line++; + if (this.maxLookAhead > 0) { this.maxLookAhead--; } + }; + + Context.fromSaved = function (doc, saved, line) { + if (saved instanceof SavedContext) + { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } + else + { return new Context(doc, copyState(doc.mode, saved), line) } + }; + + Context.prototype.save = function (copy) { + var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; + return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state + }; + + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, context, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, + lineClasses, forceToEnd); + var state = context.state; + + // Run overlays, adjust style array. + var loop = function ( o ) { + context.baseTokens = st; + var overlay = cm.state.overlays[o], i = 1, at = 0; + context.state = true; + runMode(cm, line.text, overlay.mode, context, function (end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + { st.splice(i, 1, end, st[i+1], i_end); } + i += 2; + at = Math.min(end, i_end); + } + if (!style) { return } + if (overlay.opaque) { + st.splice(start, i - start, end, "overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "overlay " + style; + } + } + }, lineClasses); + context.state = state; + context.baseTokens = null; + context.baseTokenPos = 1; + }; + + for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var context = getContextBefore(cm, lineNo(line)); + var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); + var result = highlightLine(cm, line, context); + if (resetState) { context.state = resetState; } + line.stateAfter = context.save(!resetState); + line.styles = result.styles; + if (result.classes) { line.styleClasses = result.classes; } + else if (line.styleClasses) { line.styleClasses = null; } + if (updateFrontier === cm.doc.highlightFrontier) + { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } + } + return line.styles + } + + function getContextBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) { return new Context(doc, true, n) } + var start = findStartLine(cm, n, precise); + var saved = start > doc.first && getLine(doc, start - 1).stateAfter; + var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); + + doc.iter(start, n, function (line) { + processLine(cm, line.text, context); + var pos = context.line; + line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; + context.nextLine(); + }); + if (precise) { doc.modeFrontier = context.line; } + return context + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, context, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize, context); + stream.start = stream.pos = startAt || 0; + if (text == "") { callBlankLine(mode, context.state); } + while (!stream.eol()) { + readToken(mode, stream, context.state); + stream.start = stream.pos; + } + } + + function callBlankLine(mode, state) { + if (mode.blankLine) { return mode.blankLine(state) } + if (!mode.innerMode) { return } + var inner = innerMode(mode, state); + if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) { inner[0] = innerMode(mode, state).mode; } + var style = mode.token(stream, state); + if (stream.pos > stream.start) { return style } + } + throw new Error("Mode " + mode.name + " failed to advance stream.") + } + + var Token = function(stream, type, state) { + this.start = stream.start; this.end = stream.pos; + this.string = stream.current(); + this.type = type || null; + this.state = state; + }; + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; + if (asArray) { tokens = []; } + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, context.state); + if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } + } + return asArray ? tokens : new Token(stream, style, context.state) + } + + function extractLineClasses(type, output) { + if (type) { for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) { break } + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + { output[prop] = lineClass[2]; } + else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop])) + { output[prop] += " " + lineClass[2]; } + } } + return type + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize, context), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) { processLine(cm, text, context, stream.pos); } + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) { style = "m-" + (style ? mName + " " + style : mName); } + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 5000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 + // characters, and returns inaccurate measurements in nodes + // starting around 5000 chars. + var pos = Math.min(stream.pos, curStart + 5000); + f(pos, curStyle); + curStart = pos; + } + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) { return doc.first } + var line = getLine(doc, search - 1), after = line.stateAfter; + if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) + { return search } + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline + } + + function retreatFrontier(doc, n) { + doc.modeFrontier = Math.min(doc.modeFrontier, n); + if (doc.highlightFrontier < n - 10) { return } + var start = doc.first; + for (var line = n - 1; line > start; line--) { + var saved = getLine(doc, line).stateAfter; + // change is on 3 + // state on line 1 looked ahead 2 -- so saw 3 + // test 1 + 2 < 3 should cover this + if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { + start = line + 1; + break + } + } + doc.highlightFrontier = Math.min(doc.highlightFrontier, start); + } + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + function seeReadOnlySpans() { + sawReadOnlySpans = true; + } + + function seeCollapsedSpans() { + sawCollapsedSpans = true; + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) { return span } + } } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + var r; + for (var i = 0; i < spans.length; ++i) + { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } + return r + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } } + return nw + } + function markedSpansAfter(old, endCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } } + return nw + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) { return null } + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) { return null } + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) { span.to = startCh; } + else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i$1 = 0; i$1 < last.length; ++i$1) { + var span$1 = last[i$1]; + if (span$1.to != null) { span$1.to += offset; } + if (span$1.from == null) { + var found$1 = getMarkedSpanFor(first, span$1.marker); + if (!found$1) { + span$1.from = offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } else { + span$1.from += offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } + } + // Make sure we didn't create any zero-length spans + if (first) { first = clearEmptySpans(first); } + if (last && last != first) { last = clearEmptySpans(last); } + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + { for (var i$2 = 0; i$2 < first.length; ++i$2) + { if (first[i$2].to == null) + { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } + for (var i$3 = 0; i$3 < gap; ++i$3) + { newMarkers.push(gapMarkers); } + newMarkers.push(last); + } + return newMarkers + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + { spans.splice(i--, 1); } + } + if (!spans.length) { return null } + return spans + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function (line) { + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + { (markers || (markers = [])).push(mark); } + } } + }); + if (!markers) { return null } + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + { newParts.push({from: p.from, to: m.from}); } + if (dto > 0 || !mk.inclusiveRight && !dto) + { newParts.push({from: m.to, to: p.to}); } + parts.splice.apply(parts, newParts); + j += newParts.length - 3; + } + } + return parts + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.detachLine(line); } + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.attachLine(line); } + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) { return lenDiff } + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) { return -fromCmp } + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) { return toCmp } + return b.id - a.id + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + { found = sp.marker; } + } } + return found + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } + + function collapsedSpanAround(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } + } } + return found + } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) { continue } + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } + if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || + fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) + { return true } + } } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + { line = merged.find(-1, true).line; } + return line + } + + function visualLineEnd(line) { + var merged; + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return line + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line + ;(lines || (lines = [])).push(line); + } + return lines + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) { return lineN } + return lineNo(vis) + } + + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) { return lineN } + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) { return lineN } + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return lineNo(line) + 1 + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) { continue } + if (sp.from == null) { return true } + if (sp.marker.widgetNode) { continue } + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + { return true } + } } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) + } + if (span.marker.inclusiveRight && span.to == line.text.length) + { return true } + for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) { return true } + } + } + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) { break } + else { h += line.height; } + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i$1 = 0; i$1 < p.children.length; ++i$1) { + var cur = p.children[i$1]; + if (cur == chunk) { break } + else { h += cur.height; } + } + } + return h + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) { return 0 } + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found$1 = merged.find(0, true); + len -= cur.text.length - found$1.from.ch; + cur = found$1.to.line; + len += cur.text.length - found$1.to.ch; + } + return len + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function (line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + + Line.prototype.lineNo = function () { return lineNo(this) }; + eventMixin(Line); + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + if (line.order != null) { line.order = null; } + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) { return null } + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")) + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, + col: 0, pos: 0, cm: cm, + trailingSpace: false, + splitSpaces: cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) + { builder.addToken = buildTokenBadBidi(builder.addToken, order); } + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } + if (line.styleClasses.textClass) + { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) + ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit) { + var last = builder.content.lastChild; + if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) + { builder.content.className = "cm-tab-wrap-hack"; } + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } + + return builder + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, css, attributes) { + if (!text) { return } + var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + var content; + if (!special.test(text)) { + builder.col += text.length; + content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) { mustWrap = true; } + builder.pos += text.length; + } else { + content = document.createDocumentFragment(); + var pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } + else { content.appendChild(txt); } + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) { break } + pos += skipped + 1; + var txt$1 = (void 0); + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt$1.setAttribute("role", "presentation"); + txt$1.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt$1.setAttribute("cm-text", m[0]); + builder.col += 1; + } else { + txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); + txt$1.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } + else { content.appendChild(txt$1); } + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt$1); + builder.pos++; + } + } + builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; + if (style || startStyle || endStyle || mustWrap || css || attributes) { + var fullStyle = style || ""; + if (startStyle) { fullStyle += startStyle; } + if (endStyle) { fullStyle += endStyle; } + var token = elt("span", [content], fullStyle, css); + if (attributes) { + for (var attr in attributes) { if (attributes.hasOwnProperty(attr) && attr != "style" && attr != "class") + { token.setAttribute(attr, attributes[attr]); } } + } + return builder.content.appendChild(token) + } + builder.content.appendChild(content); + } + + // Change some spaces to NBSP to prevent the browser from collapsing + // trailing spaces at the end of a line when rendering text (issue #1362). + function splitSpaces(text, trailingBefore) { + if (text.length > 1 && !/ /.test(text)) { return text } + var spaceBefore = trailingBefore, result = ""; + for (var i = 0; i < text.length; i++) { + var ch = text.charAt(i); + if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) + { ch = "\u00a0"; } + result += ch; + spaceBefore = ch == " "; + } + return result + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function (builder, text, style, startStyle, endStyle, css, attributes) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + var part = (void 0); + for (var i = 0; i < order.length; i++) { + part = order[i]; + if (part.to > start && part.from <= start) { break } + } + if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, css, attributes) } + inner(builder, text.slice(0, part.to - start), style, startStyle, null, css, attributes); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + } + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + { widget = builder.content.appendChild(document.createElement("span")); } + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + builder.trailingSpace = false; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i$1 = 1; i$1 < styles.length; i$1+=2) + { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } + return + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed, attributes; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = css = ""; + attributes = null; + collapsed = null; nextChange = Infinity; + var foundBookmarks = [], endStyles = (void 0); + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) { spanStyle += " " + m.className; } + if (m.css) { css = (css ? css + ";" : "") + m.css; } + if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } + if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } + // support for the old title property + // https://github.com/codemirror/CodeMirror/pull/5673 + if (m.title) { (attributes || (attributes = {})).title = m.title; } + if (m.attributes) { + for (var attr in m.attributes) + { (attributes || (attributes = {}))[attr] = m.attributes[attr]; } + } + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + { collapsed = sp; } + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) + { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } + + if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) + { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) { return } + if (collapsed.to == pos) { collapsed = false; } + } + } + if (pos >= len) { break } + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", css, attributes); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array + } + + var operationGroup = null; + + function pushOperation(op) { + if (operationGroup) { + operationGroup.ops.push(op); + } else { + op.ownsGroup = operationGroup = { + ops: [op], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + { callbacks[i].call(null); } + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } + } + } while (i < callbacks.length) + } + + function finishOperation(op, endCb) { + var group = op.ownsGroup; + if (!group) { return } + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + endCb(group); + } + } + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = getHandlers(emitter, type); + if (!arr.length) { return } + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + var loop = function ( i ) { + list.push(function () { return arr[i].apply(null, args); }); + }; + + for (var i = 0; i < arr.length; ++i) + loop( i ); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) { delayed[i](); } + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") { updateLineText(cm, lineView); } + else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } + else if (type == "class") { updateLineClasses(cm, lineView); } + else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } + } + return lineView.node + } + + function updateLineBackground(cm, lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) { cls += " CodeMirror-linebackground"; } + if (lineView.background) { + if (cls) { lineView.background.className = cls; } + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + cm.display.input.setUneditable(lineView.background); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built + } + return buildLineContent(cm, lineView) + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) { lineView.node = built.pre; } + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(cm, lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(cm, lineView) { + updateLineBackground(cm, lineView); + if (lineView.line.wrapClass) + { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } + else if (lineView.node != lineView.text) + { lineView.node.className = ""; } + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(lineView.gutterBackground); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap$1 = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(gutterWrap); + wrap$1.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + { gutterWrap.className += " " + lineView.line.gutterClass; } + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + { lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } + if (markers) { for (var k = 0; k < cm.display.gutterSpecs.length; ++k) { + var id = cm.display.gutterSpecs[k].className, found = markers.hasOwnProperty(id) && markers[id]; + if (found) + { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } + } } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) { lineView.alignable = null; } + var isWidget = classTest("CodeMirror-linewidget"); + for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { + next = node.nextSibling; + if (isWidget.test(node.className)) { lineView.node.removeChild(node); } + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) { lineView.bgClass = built.bgClass; } + if (built.textClass) { lineView.textClass = built.textClass; } + + updateLineClasses(cm, lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) { return } + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget" + (widget.className ? " " + widget.className : "")); + if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + { wrap.insertBefore(node, lineView.gutter || lineView.text); } + else + { wrap.appendChild(node); } + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } + } + } + + function widgetHeight(widget) { + if (widget.height != null) { return widget.height } + var cm = widget.doc.cm; + if (!cm) { return 0 } + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } + if (widget.noHScroll) + { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.parentNode.offsetHeight + } + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + { return true } + } + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} + function paddingH(display) { + if (display.cachedPaddingH) { return display.cachedPaddingH } + var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } + return data + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + { heights.push((cur.bottom + next.top) / 2 - rect.top); } + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + { return {map: lineView.measure.map, cache: lineView.measure.cache} } + for (var i = 0; i < lineView.rest.length; i++) + { if (lineView.rest[i] == line) + { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } + for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) + { if (lineNo(lineView.rest[i$1]) > lineN) + { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + { return cm.display.view[findViewIndex(cm, lineN)] } + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + { return ext } + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) { + view = null; + } else if (view && view.changes) { + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } + if (!view) + { view = updateExternalMeasurement(cm, line); } + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + } + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) { ch = -1; } + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + { prepared.rect = prepared.view.text.getBoundingClientRect(); } + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) { prepared.cache[key] = found; } + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom} + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse, mStart, mEnd; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + mStart = map[i]; + mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) { collapse = "right"; } + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + { collapse = bias; } + if (bias == "left" && start == 0) + { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } } + if (bias == "right" && start == mEnd - mStart) + { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } } + break + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} + } + + function getUsefulRect(rects, bias) { + var rect = nullRect; + if (bias == "left") { for (var i = 0; i < rects.length; i++) { + if ((rect = rects[i]).left != rect.right) { break } + } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { + if ((rect = rects[i$1]).left != rect.right) { break } + } } + return rect + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) + { rect = node.parentNode.getBoundingClientRect(); } + else + { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } + if (rect.left || rect.right || start == 0) { break } + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) { collapse = bias = "right"; } + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + { rect = rects[bias == "right" ? rects.length - 1 : 0]; } + else + { rect = node.getBoundingClientRect(); } + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } + else + { rect = nullRect; } + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + var i = 0; + for (; i < heights.length - 1; i++) + { if (mid < heights[i]) { break } } + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) { result.bogus = true; } + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + { return rect } + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY} + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { lineView.measure.caches[i] = {}; } } + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + { clearLineMeasurementCacheFor(cm.display.view[i]); } + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } + cm.display.lineNumChars = null; + } + + function pageScrollX() { + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 + // which causes page_Offset and bounding client rects to use + // different reference viewports and invalidate our calculations. + if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } + return window.pageXOffset || (document.documentElement || document.body).scrollLeft + } + function pageScrollY() { + if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } + return window.pageYOffset || (document.documentElement || document.body).scrollTop + } + + function widgetTopHeight(lineObj) { + var height = 0; + if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) + { height += widgetHeight(lineObj.widgets[i]); } } } + return height + } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"./null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { + if (!includeWidgets) { + var height = widgetTopHeight(lineObj); + rect.top += height; rect.bottom += height; + } + if (context == "line") { return rect } + if (!context) { context = "local"; } + var yOff = heightAtLine(lineObj); + if (context == "local") { yOff += paddingTop(cm.display); } + else { yOff -= cm.display.viewOffset; } + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"./null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") { return coords } + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` + // and after `char - 1` in writing order of `char - 1` + // A cursor Pos(line, char, "after") is on the same visual line as `char` + // and before `char` in writing order of `char` + // Examples (upper-case letters are RTL, lower-case are LTR): + // Pos(0, 1, ...) + // before after + // ab a|b a|b + // aB a|B aB| + // Ab |Ab A|b + // AB B|A B|A + // Every position after the last character on a line is considered to stick + // to the last character on the line. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) { m.left = m.right; } else { m.right = m.left; } + return intoCoordSystem(cm, lineObj, m, context) + } + var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; + if (ch >= lineObj.text.length) { + ch = lineObj.text.length; + sticky = "before"; + } else if (ch <= 0) { + ch = 0; + sticky = "after"; + } + if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } + + function getBidi(ch, partPos, invert) { + var part = order[partPos], right = part.level == 1; + return get(invert ? ch - 1 : ch, right != invert) + } + var partPos = getBidiPartAt(order, ch, sticky); + var other = bidiOther; + var val = getBidi(ch, partPos, sticky == "before"); + if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } + return val + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0; + pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height} + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, sticky, outside, xRel) { + var pos = Pos(line, ch, sticky); + pos.xRel = xRel; + if (outside) { pos.outside = outside; } + return pos + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } + if (x < 0) { x = 0; } + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); + if (!collapsed) { return found } + var rangeEnd = collapsed.find(1); + if (rangeEnd.line == lineN) { return rangeEnd } + lineObj = getLine(doc, lineN = rangeEnd.line); + } + } + + function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { + y -= widgetTopHeight(lineObj); + var end = lineObj.text.length; + var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); + end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); + return {begin: begin, end: end} + } + + function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; + return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) + } + + // Returns true if the given side of a box is after the given + // coordinates, in top-to-bottom, left-to-right order. + function boxIsAfter(box, x, y, left) { + return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + // Move y into line-local coordinate space + y -= heightAtLine(lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + // When directly calling `measureCharPrepared`, we have to adjust + // for the widgets at this line. + var widgetHeight = widgetTopHeight(lineObj); + var begin = 0, end = lineObj.text.length, ltr = true; + + var order = getOrder(lineObj, cm.doc.direction); + // If the line isn't plain left-to-right text, first figure out + // which bidi section the coordinates fall into. + if (order) { + var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) + (cm, lineObj, lineNo, preparedMeasure, order, x, y); + ltr = part.level != 1; + // The awkward -1 offsets are needed because findFirst (called + // on these below) will treat its first bound as inclusive, + // second as exclusive, but we want to actually address the + // characters in the part's range + begin = ltr ? part.from : part.to - 1; + end = ltr ? part.to : part.from - 1; + } + + // A binary search to find the first character whose bounding box + // starts after the coordinates. If we run across any whose box wrap + // the coordinates, store that. + var chAround = null, boxAround = null; + var ch = findFirst(function (ch) { + var box = measureCharPrepared(cm, preparedMeasure, ch); + box.top += widgetHeight; box.bottom += widgetHeight; + if (!boxIsAfter(box, x, y, false)) { return false } + if (box.top <= y && box.left <= x) { + chAround = ch; + boxAround = box; + } + return true + }, begin, end); + + var baseX, sticky, outside = false; + // If a box around the coordinates was found, use that + if (boxAround) { + // Distinguish coordinates nearer to the left or right side of the box + var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; + ch = chAround + (atStart ? 0 : 1); + sticky = atStart ? "after" : "before"; + baseX = atLeft ? boxAround.left : boxAround.right; + } else { + // (Adjust for extended bound, if necessary.) + if (!ltr && (ch == end || ch == begin)) { ch++; } + // To determine which side to associate with, get the box to the + // left of the character and compare it's vertical position to the + // coordinates + sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : + (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? + "after" : "before"; + // Now get accurate coordinates for this place, in order to get a + // base X position + var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure); + baseX = coords.left; + outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; + } + + ch = skipExtendingChars(lineObj.text, ch, 1); + return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) + } + + function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { + // Bidi parts are sorted left-to-right, and in a non-line-wrapping + // situation, we can take this ordering to correspond to the visual + // ordering. This finds the first part whose end is after the given + // coordinates. + var index = findFirst(function (i) { + var part = order[i], ltr = part.level != 1; + return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), + "line", lineObj, preparedMeasure), x, y, true) + }, 0, order.length - 1); + var part = order[index]; + // If this isn't the first part, the part's start is also after + // the coordinates, and the coordinates aren't on the same line as + // that start, move one part back. + if (index > 0) { + var ltr = part.level != 1; + var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), + "line", lineObj, preparedMeasure); + if (boxIsAfter(start, x, y, true) && start.top > y) + { part = order[index - 1]; } + } + return part + } + + function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { + // In a wrapped line, rtl text on wrapping boundaries can do things + // that don't correspond to the ordering in our `order` array at + // all, so a binary search doesn't work, and we want to return a + // part that only spans one line so that the binary search in + // coordsCharInner is safe. As such, we first find the extent of the + // wrapped line, and then do a flat search in which we discard any + // spans that aren't on the line. + var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); + var begin = ref.begin; + var end = ref.end; + if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } + var part = null, closestDist = null; + for (var i = 0; i < order.length; i++) { + var p = order[i]; + if (p.from >= end || p.to <= begin) { continue } + var ltr = p.level != 1; + var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; + // Weigh against spans ending before this, so that they are only + // picked if nothing ends after + var dist = endX < x ? x - endX + 1e9 : endX - x; + if (!part || closestDist > dist) { + part = p; + closestDist = dist; + } + } + if (!part) { part = order[order.length - 1]; } + // Clip the part to the wrapped line. + if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } + if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } + return part + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) { return display.cachedTextHeight } + if (measureText == null) { + measureText = elt("pre", null, "CodeMirror-line-like"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) { display.cachedTextHeight = height; } + removeChildren(display.measure); + return height || 1 + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) { return display.cachedCharWidth } + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor], "CodeMirror-line-like"); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) { display.cachedCharWidth = width; } + return width || 10 + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + var id = cm.display.gutterSpecs[i].className; + left[id] = n.offsetLeft + n.clientLeft + gutterLeft; + width[id] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth} + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function (line) { + if (lineIsHidden(cm.doc, line)) { return 0 } + + var widgetsHeight = 0; + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } + } } + + if (wrapping) + { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } + else + { return widgetsHeight + th } + } + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function (line) { + var estHeight = est(line); + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + }); + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e$1) { return null } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel > 0 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) { return null } + n -= cm.display.viewFrom; + if (n < 0) { return null } + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) { return i } + } + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) { from = cm.doc.first; } + if (to == null) { to = cm.doc.first + cm.doc.size; } + if (!lendiff) { lendiff = 0; } + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + { display.updateLineNumbers = from; } + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + { resetView(cm); } + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut$1 = viewCuttingPoint(cm, from, from, -1); + if (cut$1) { + display.view = display.view.slice(0, cut$1.index); + display.viewTo = cut$1.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + { ext.lineN += lendiff; } + else if (from < ext.lineN + ext.size) + { display.externalMeasured = null; } + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + { display.externalMeasured = null; } + + if (line < display.viewFrom || line >= display.viewTo) { return } + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) { return } + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) { arr.push(type); } + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + { return {index: index, lineN: newN} } + var n = cm.display.viewFrom; + for (var i = 0; i < index; i++) + { n += view[i].size; } + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) { return null } + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) { return null } + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN} + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } + else if (display.viewFrom < from) + { display.view = display.view.slice(findViewIndex(cm, from)); } + display.viewFrom = from; + if (display.viewTo < to) + { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } + else if (display.viewTo > to) + { display.view = display.view.slice(0, findViewIndex(cm, to)); } + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } + } + return dirty + } + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + if ( primary === void 0 ) primary = true; + + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (!primary && i == doc.sel.primIndex) { continue } + var range = doc.sel.ranges[i]; + if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + { drawSelectionCursor(cm, range.head, curFragment); } + if (!collapsed) + { drawSelectionRange(cm, range, selFragment); } + } + return result + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + var docLTR = doc.direction == "ltr"; + + function add(left, top, width, bottom) { + if (top < 0) { top = 0; } + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias) + } + + function wrapX(pos, dir, side) { + var extent = wrappedLineExtentChar(cm, lineObj, null, pos); + var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; + var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); + return coords(ch, prop)[prop] + } + + var order = getOrder(lineObj, doc.direction); + iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { + var ltr = dir == "ltr"; + var fromPos = coords(from, ltr ? "left" : "right"); + var toPos = coords(to - 1, ltr ? "right" : "left"); + + var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; + var first = i == 0, last = !order || i == order.length - 1; + if (toPos.top - fromPos.top <= 3) { // Single line + var openLeft = (docLTR ? openStart : openEnd) && first; + var openRight = (docLTR ? openEnd : openStart) && last; + var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; + var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; + add(left, fromPos.top, right - left, fromPos.bottom); + } else { // Multiple lines + var topLeft, topRight, botLeft, botRight; + if (ltr) { + topLeft = docLTR && openStart && first ? leftSide : fromPos.left; + topRight = docLTR ? rightSide : wrapX(from, dir, "before"); + botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); + botRight = docLTR && openEnd && last ? rightSide : toPos.right; + } else { + topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); + topRight = !docLTR && openStart && first ? rightSide : fromPos.right; + botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; + botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); + } + add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); + if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } + add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); + } + + if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } + if (cmpCoords(toPos, start) < 0) { start = toPos; } + if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } + if (cmpCoords(toPos, end) < 0) { end = toPos; } + }); + return {start: start, end: end} + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + { add(leftSide, leftEnd.bottom, null, rightStart.top); } + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) { return } + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + { display.blinker = setInterval(function () { + if (!cm.hasFocus()) { onBlur(cm); } + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); } + else if (cm.options.cursorBlinkRate < 0) + { display.cursorDiv.style.visibility = "hidden"; } + } + + function ensureFocus(cm) { + if (!cm.hasFocus()) { + cm.display.input.focus(); + if (!cm.state.focused) { onFocus(cm); } + } + } + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function () { if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + if (cm.state.focused) { onBlur(cm); } + } }, 100); + } + + function onFocus(cm, e) { + if (cm.state.delayingBlurEvent && !cm.state.draggingText) { cm.state.delayingBlurEvent = false; } + + if (cm.options.readOnly == "nocursor") { return } + if (!cm.state.focused) { + signal(cm, "focus", cm, e); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm, e) { + if (cm.state.delayingBlurEvent) { return } + + if (cm.state.focused) { + signal(cm, "blur", cm, e); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], wrapping = cm.options.lineWrapping; + var height = (void 0), width = 0; + if (cur.hidden) { continue } + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + // Check that lines don't extend past the right of the current + // editor width + if (!wrapping && cur.text.firstChild) + { width = cur.text.firstChild.getBoundingClientRect().right - box.left - 1; } + } + var diff = cur.line.height - height; + if (diff > .005 || diff < -.005) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) + { updateWidgetHeight(cur.rest[j]); } } + } + if (width > cm.display.sizerWidth) { + var chWidth = Math.ceil(width / charWidth(cm.display)); + if (chWidth > cm.display.maxLineLength) { + cm.display.maxLineLength = chWidth; + cm.display.maxLine = cur.line; + cm.display.maxLineChanged = true; + } + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i], parent = w.node.parentNode; + if (parent) { w.height = parent.offsetHeight; } + } } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)} + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, rect) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (rect.top + box.top < 0) { doScroll = true; } + else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) { margin = 0; } + var rect; + if (!cm.options.lineWrapping && pos == end) { + // Set pos and end to the cursor positions around the character pos sticks to + // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch + // If pos == Pos(_, 0, "before"), pos and end are unchanged + pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; + end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; + } + for (var limit = 0; limit < 5; limit++) { + var changed = false; + var coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + rect = {left: Math.min(coords.left, endCoords.left), + top: Math.min(coords.top, endCoords.top) - margin, + right: Math.max(coords.left, endCoords.left), + bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; + var scrollPos = calculateScrollPos(cm, rect); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + updateScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } + } + if (!changed) { break } + } + return rect + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, rect) { + var scrollPos = calculateScrollPos(cm, rect); + if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } + if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, rect) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (rect.top < 0) { rect.top = 0; } + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } + var docBottom = cm.doc.height + paddingVert(display); + var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; + if (rect.top < screentop) { + result.scrollTop = atTop ? 0 : rect.top; + } else if (rect.bottom > screentop + screen) { + var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); + if (newTop != screentop) { result.scrollTop = newTop; } + } + + var gutterSpace = cm.options.fixedGutter ? 0 : display.gutters.offsetWidth; + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft - gutterSpace; + var screenw = displayWidth(cm) - display.gutters.offsetWidth; + var tooWide = rect.right - rect.left > screenw; + if (tooWide) { rect.right = rect.left + screenw; } + if (rect.left < 10) + { result.scrollLeft = 0; } + else if (rect.left < screenleft) + { result.scrollLeft = Math.max(0, rect.left + gutterSpace - (tooWide ? 0 : 10)); } + else if (rect.right > screenw + screenleft - 3) + { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } + return result + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollTop(cm, top) { + if (top == null) { return } + resolveScrollToPos(cm); + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(); + cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; + } + + function scrollToCoords(cm, x, y) { + if (x != null || y != null) { resolveScrollToPos(cm); } + if (x != null) { cm.curOp.scrollLeft = x; } + if (y != null) { cm.curOp.scrollTop = y; } + } + + function scrollToRange(cm, range) { + resolveScrollToPos(cm); + cm.curOp.scrollToPos = range; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + scrollToCoordsRange(cm, from, to, range.margin); + } + } + + function scrollToCoordsRange(cm, from, to, margin) { + var sPos = calculateScrollPos(cm, { + left: Math.min(from.left, to.left), + top: Math.min(from.top, to.top) - margin, + right: Math.max(from.right, to.right), + bottom: Math.max(from.bottom, to.bottom) + margin + }); + scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); + } + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function updateScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) { return } + if (!gecko) { updateDisplaySimple(cm, {top: val}); } + setScrollTop(cm, val, true); + if (gecko) { updateDisplaySimple(cm); } + startWorker(cm, 100); + } + + function setScrollTop(cm, val, forceScroll) { + val = Math.max(0, Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)); + if (cm.display.scroller.scrollTop == val && !forceScroll) { return } + cm.doc.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } + } + + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller, forceScroll) { + val = Math.max(0, Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)); + if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } + cm.display.scrollbars.setScrollLeft(val); + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + } + } + + var NativeScrollbars = function(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + vert.tabIndex = horiz.tabIndex = -1; + place(vert); place(horiz); + + on(vert, "scroll", function () { + if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } + }); + on(horiz, "scroll", function () { + if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } + }); + + this.checkedZeroWidth = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } + }; + + NativeScrollbars.prototype.update = function (measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) { this.zeroWidthHack(); } + this.checkedZeroWidth = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} + }; + + NativeScrollbars.prototype.setScrollLeft = function (pos) { + if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } + if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } + }; + + NativeScrollbars.prototype.setScrollTop = function (pos) { + if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } + if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } + }; + + NativeScrollbars.prototype.zeroWidthHack = function () { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }; + + NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // right corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) + : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); + if (elt != bar) { bar.style.pointerEvents = "none"; } + else { delay.set(1000, maybeDisable); } + } + delay.set(1000, maybeDisable); + }; + + NativeScrollbars.prototype.clear = function () { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + }; + + var NullScrollbars = function () {}; + + NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; + NullScrollbars.prototype.setScrollLeft = function () {}; + NullScrollbars.prototype.setScrollTop = function () {}; + NullScrollbars.prototype.clear = function () {}; + + function updateScrollbars(cm, measure) { + if (!measure) { measure = measureForScrollbars(cm); } + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + { updateHeightsInViewport(cm); } + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else { d.scrollbarFiller.style.display = ""; } + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else { d.gutterFiller.style.display = ""; } + } + + var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function () { + if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } + }); + node.setAttribute("cm-not-content", "true"); + }, function (pos, axis) { + if (axis == "horizontal") { setScrollLeft(cm, pos); } + else { updateScrollTop(cm, pos); } + }, cm); + if (cm.display.scrollbars.addClass) + { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: 0, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + pushOperation(cm.curOp); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp; + if (op) { finishOperation(op, function (group) { + for (var i = 0; i < group.ops.length; i++) + { group.ops[i].cm.curOp = null; } + endOperations(group); + }); } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + { endOperation_R1(ops[i]); } + for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) + { endOperation_W1(ops[i$1]); } + for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM + { endOperation_R2(ops[i$2]); } + for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) + { endOperation_W2(ops[i$3]); } + for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM + { endOperation_finish(ops[i$4]); } + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) { findMaxLine(cm); } + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) { updateHeightsInViewport(cm); } + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + { op.preparedSelection = display.input.prepareSelection(); } + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } + cm.display.maxLineChanged = false; + } + + var takeFocus = op.focus && op.focus == activeElt(); + if (op.preparedSelection) + { cm.display.input.showSelection(op.preparedSelection, takeFocus); } + if (op.updatedDisplay || op.startHeight != cm.doc.height) + { updateScrollbars(cm, op.barMeasure); } + if (op.updatedDisplay) + { setDocumentHeight(cm, op.barMeasure); } + + if (op.selectionChanged) { restartBlink(cm); } + + if (cm.state.focused && op.updateInput) + { cm.display.input.reset(op.typing); } + if (takeFocus) { ensureFocus(op.cm); } + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + { display.wheelStartX = display.wheelStartY = null; } + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } + + if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + maybeScrollWindow(cm, rect); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) { for (var i = 0; i < hidden.length; ++i) + { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } + if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) + { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } + + if (display.wrapper.offsetHeight) + { doc.scrollTop = cm.display.scroller.scrollTop; } + + // Fire change events, and delayed event handlers + if (op.changeObjs) + { signal(cm, "changes", cm, op.changeObjs); } + if (op.update) + { op.update.finish(); } + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) { return f() } + startOperation(cm); + try { return f() } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) { return f.apply(cm, arguments) } + startOperation(cm); + try { return f.apply(cm, arguments) } + finally { endOperation(cm); } + } + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) { return f.apply(this, arguments) } + startOperation(this); + try { return f.apply(this, arguments) } + finally { endOperation(this); } + } + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) { return f.apply(this, arguments) } + startOperation(cm); + try { return f.apply(this, arguments) } + finally { endOperation(cm); } + } + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.highlightFrontier < cm.display.viewTo) + { cm.state.highlight.set(time, bind(highlightWorker, cm)); } + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.highlightFrontier >= cm.display.viewTo) { return } + var end = +new Date + cm.options.workTime; + var context = getContextBefore(cm, doc.highlightFrontier); + var changedLines = []; + + doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { + if (context.line >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; + var highlighted = highlightLine(cm, line, context, true); + if (resetState) { context.state = resetState; } + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) { line.styleClasses = newCls; } + else if (oldCls) { line.styleClasses = null; } + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } + if (ischange) { changedLines.push(context.line); } + line.stateAfter = context.save(); + context.nextLine(); + } else { + if (line.text.length <= cm.options.maxHighlightLength) + { processLine(cm, line.text, context); } + line.stateAfter = context.line % 5 == 0 ? context.save() : null; + context.nextLine(); + } + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true + } + }); + doc.highlightFrontier = context.line; + doc.modeFrontier = Math.max(doc.modeFrontier, context.line); + if (changedLines.length) { runInOp(cm, function () { + for (var i = 0; i < changedLines.length; i++) + { regLineChange(cm, changedLines[i], "text"); } + }); } + } + + // DISPLAY DRAWING + + var DisplayUpdate = function(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + }; + + DisplayUpdate.prototype.signal = function (emitter, type) { + if (hasHandler(emitter, type)) + { this.events.push(arguments); } + }; + DisplayUpdate.prototype.finish = function () { + for (var i = 0; i < this.events.length; i++) + { signal.apply(null, this.events[i]); } + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + function selectionSnapshot(cm) { + if (cm.hasFocus()) { return null } + var active = activeElt(); + if (!active || !contains(cm.display.lineDiv, active)) { return null } + var result = {activeElt: active}; + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { + result.anchorNode = sel.anchorNode; + result.anchorOffset = sel.anchorOffset; + result.focusNode = sel.focusNode; + result.focusOffset = sel.focusOffset; + } + } + return result + } + + function restoreSelection(snapshot) { + if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } + snapshot.activeElt.focus(); + if (!/^(INPUT|TEXTAREA)$/.test(snapshot.activeElt.nodeName) && + snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { + var sel = window.getSelection(), range = document.createRange(); + range.setEnd(snapshot.anchorNode, snapshot.anchorOffset); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + sel.extend(snapshot.focusNode, snapshot.focusOffset); + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + { return false } + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } + if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + { return false } + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var selSnapshot = selectionSnapshot(cm); + if (toUpdate > 4) { display.lineDiv.style.display = "none"; } + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) { display.lineDiv.style.display = ""; } + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + restoreSelection(selSnapshot); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = display.sizer.style.minHeight = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + { break } + } else if (first) { + update.visible = visibleLines(cm.display, cm.doc, viewport); + } + if (!updateDisplayIfNeeded(cm, update)) { break } + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.force = false; + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.finish(); + } + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + { node.style.display = "none"; } + else + { node.parentNode.removeChild(node); } + return next + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) { cur = rm(cur); } + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) { cur = rm(cur); } + } + + function updateGutterSpace(display) { + var width = display.gutters.offsetWidth; + display.sizer.style.marginLeft = width + "px"; + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; + } + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { + if (cm.options.fixedGutter) { + if (view[i].gutter) + { view[i].gutter.style.left = left; } + if (view[i].gutterBackground) + { view[i].gutterBackground.style.left = left; } + } + var align = view[i].alignable; + if (align) { for (var j = 0; j < align.length; j++) + { align[j].style.left = left; } } + } } + if (cm.options.fixedGutter) + { display.gutters.style.left = (comp + gutterW) + "px"; } + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) { return false } + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm.display); + return true + } + return false + } + + function getGutters(gutters, lineNumbers) { + var result = [], sawLineNumbers = false; + for (var i = 0; i < gutters.length; i++) { + var name = gutters[i], style = null; + if (typeof name != "string") { style = name.style; name = name.className; } + if (name == "CodeMirror-linenumbers") { + if (!lineNumbers) { continue } + else { sawLineNumbers = true; } + } + result.push({className: name, style: style}); + } + if (lineNumbers && !sawLineNumbers) { result.push({className: "CodeMirror-linenumbers", style: null}); } + return result + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function renderGutters(display) { + var gutters = display.gutters, specs = display.gutterSpecs; + removeChildren(gutters); + display.lineGutter = null; + for (var i = 0; i < specs.length; ++i) { + var ref = specs[i]; + var className = ref.className; + var style = ref.style; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + className)); + if (style) { gElt.style.cssText = style; } + if (className == "CodeMirror-linenumbers") { + display.lineGutter = gElt; + gElt.style.width = (display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = specs.length ? "" : "none"; + updateGutterSpace(display); + } + + function updateGutters(cm) { + renderGutters(cm.display); + regChange(cm); + alignHorizontally(cm); + } + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input, options) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = eltP("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [lines], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } + + if (place) { + if (place.appendChild) { place.appendChild(d.wrapper); } + else { place(d.wrapper); } + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + d.gutterSpecs = getGutters(options.gutters, options.lineNumbers); + renderGutters(d); + + input.init(d); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) { wheelPixelsPerUnit = -.53; } + else if (gecko) { wheelPixelsPerUnit = 15; } + else if (chrome) { wheelPixelsPerUnit = -.7; } + else if (safari) { wheelPixelsPerUnit = -1/3; } + + function wheelEventDelta(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } + else if (dy == null) { dy = e.wheelDelta; } + return {x: dx, y: dy} + } + function wheelEventPixels(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta + } + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) { return } + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy && canScrollY) + { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)); } + setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + { e_preventDefault(e); } + display.wheelStartX = null; // Abort measurement, if in progress + return + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) { top = Math.max(0, top + pixels - 50); } + else { bot = Math.min(cm.doc.height, bot + pixels + 50); } + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function () { + if (display.wheelStartX == null) { return } + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) { return } + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + var Selection = function(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + }; + + Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; + + Selection.prototype.equals = function (other) { + if (other == this) { return true } + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } + } + return true + }; + + Selection.prototype.deepCopy = function () { + var out = []; + for (var i = 0; i < this.ranges.length; i++) + { out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); } + return new Selection(out, this.primIndex) + }; + + Selection.prototype.somethingSelected = function () { + for (var i = 0; i < this.ranges.length; i++) + { if (!this.ranges[i].empty()) { return true } } + return false + }; + + Selection.prototype.contains = function (pos, end) { + if (!end) { end = pos; } + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + { return i } + } + return -1 + }; + + var Range = function(anchor, head) { + this.anchor = anchor; this.head = head; + }; + + Range.prototype.from = function () { return minPos(this.anchor, this.head) }; + Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; + Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(cm, ranges, primIndex) { + var mayTouch = cm && cm.options.selectionsMayTouch; + var prim = ranges[primIndex]; + ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + var diff = cmp(prev.to(), cur.from()); + if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) { --primIndex; } + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex) + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0) + } + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + function changeEnd(change) { + if (!change.text) { return change.to } + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) + } + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) { return pos } + if (cmp(pos, change.to) <= 0) { return changeEnd(change) } + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } + return Pos(line, ch) + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(doc.cm, out, doc.sel.primIndex) + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + { return Pos(nw.line, pos.ch - old.ch + nw.ch) } + else + { return Pos(nw.line + (pos.line - old.line), pos.ch) } + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex) + } + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function (line) { + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + }); + cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) { regChange(cm); } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore) + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + var result = []; + for (var i = start; i < end; ++i) + { result.push(new Line(text[i], spansFor(i), estimateHeight)); } + return result + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) { doc.remove(from.line, nlines); } + if (added.length) { doc.insert(from.line, added); } + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added$1 = linesFor(1, text.length - 1); + added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added$1); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added$2 = linesFor(1, text.length - 1); + if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } + doc.insert(from.line + 1, added$2); + } + + signalLater(doc, "change", doc, change); + } + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) { continue } + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) { continue } + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) { throw new Error("This document is already in use.") } + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + setDirectionClass(cm); + if (!cm.options.lineWrapping) { findMaxLine(cm); } + cm.options.mode = doc.modeOption; + regChange(cm); + } + + function setDirectionClass(cm) { + (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); + } + + function directionChanged(cm) { + runInOp(cm, function () { + setDirectionClass(cm); + regChange(cm); + }); + } + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); + return histChange + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) { array.pop(); } + else { break } + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done) + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done) + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done) + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, or are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + var last; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + { pushSelectionToHistory(doc.sel, hist.done); } + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) { hist.done.shift(); } + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) { signal(doc, "historyAdded"); } + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + { hist.done[hist.done.length - 1] = sel; } + else + { pushSelectionToHistory(sel, hist.done); } + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + { clearSelectionEvents(hist.undone); } + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + { dest.push(sel); } + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { + if (line.markedSpans) + { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) { return null } + var out; + for (var i = 0; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } + else if (out) { out.push(spans[i]); } + } + return !out ? spans : out.length ? out : null + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) { return null } + var nw = []; + for (var i = 0; i < change.text.length; ++i) + { nw.push(removeClearedSpans(found[i])); } + return nw + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) { return stretched } + if (!stretched) { return old } + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + { if (oldCur[k].marker == span.marker) { continue spans } } + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + var copy = []; + for (var i = 0; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m = (void 0); + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } } } + } + } + return copy + } + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(range, head, other, extend) { + if (extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head) + } else { + return new Range(other || head, head) + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options, extend) { + if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } + setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + var out = []; + var extend = doc.cm && (doc.cm.display.shift || doc.extend); + for (var i = 0; i < doc.sel.ranges.length; i++) + { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } + var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel, options) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + { this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); } + }, + origin: options && options.origin + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } + if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } + else { return sel } + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + { sel = filterSelectionChange(doc, sel, options); } + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + { ensureCursorVisible(doc.cm); } + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) { return } + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = 1; + doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) { out = sel.ranges.slice(0, i); } + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel + } + + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + + // Determine if we should prevent the cursor being placed to the left/right of an atomic marker + // Historically this was determined using the inclusiveLeft/Right option, but the new way to control it + // is with selectLeft/Right + var preventCursorLeft = ("selectLeft" in m) ? !m.selectLeft : m.inclusiveLeft; + var preventCursorRight = ("selectRight" in m) ? !m.selectRight : m.inclusiveRight; + + if ((sp.from == null || (preventCursorLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (preventCursorRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) { break } + else {--i; continue} + } + } + if (!m.atomic) { continue } + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); + if (dir < 0 ? preventCursorRight : preventCursorLeft) + { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + { return skipAtomicInner(doc, near, pos, dir, mayClear) } + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? preventCursorLeft : preventCursorRight) + { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null + } + } } + return pos + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0) + } + return found + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } + else { return null } + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } + else { return null } + } else { + return new Pos(pos.line, pos.ch + dir) + } + } + + function selectAll(cm) { + cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); + } + + // UPDATING + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function () { return obj.canceled = true; } + }; + if (update) { obj.update = function (from, to, text, origin) { + if (from) { obj.from = clipPos(doc, from); } + if (to) { obj.to = clipPos(doc, to); } + if (text) { obj.text = text; } + if (origin !== undefined) { obj.origin = origin; } + }; } + signal(doc, "beforeChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } + + if (obj.canceled) { + if (doc.cm) { doc.cm.curOp.updateInput = 2; } + return null + } + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } + if (doc.cm.state.suppressEdits) { return } + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) { return } + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + var suppress = doc.cm && doc.cm.state.suppressEdits; + if (suppress && !allowSelectionOnly) { return } + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + var i = 0; + for (; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + { break } + } + if (i == source.length) { return } + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return + } + selAfter = event; + } else if (suppress) { + source.push(event); + return + } else { break } + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + var loop = function ( i ) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return {} + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + }; + + for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { + var returned = loop( i$1 ); + + if ( returned ) return returned.v; + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) { return } + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( + Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch) + ); }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + { regLineChange(doc.cm, l, "gutter"); } + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return + } + if (change.from.line > doc.lastLine()) { return } + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } + if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } + else { updateDoc(doc, change, spans); } + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + + if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) + { doc.cantEdit = false; } + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function (line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + { signalCursorActivity(cm); } + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function (line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } + } + + retreatFrontier(doc, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + { regChange(cm); } + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + { regLineChange(cm, from.line, "text"); } + else + { regChange(cm, from.line, to.line + 1, lendiff); } + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) { signalLater(cm, "change", cm, obj); } + if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + var assign; + + if (!to) { to = from; } + if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } + if (typeof code == "string") { code = doc.splitLines(code); } + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue + } + for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { + var cur = sub.changes[j$1]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } + else { no = lineNo(handle); } + if (no == null) { return null } + if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } + return line + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + var height = 0; + for (var i = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length }, + + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) { lines[i].parent = this; } + }, + + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + { if (op(this.lines[at])) { return true } } + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size }, + + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) { break } + at = 0; + } else { at -= sz; } + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) { this.children[i].collapse(lines); } + }, + + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. + // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. + var remaining = child.lines.length % 25 + 25; + for (var pos = remaining; pos < child.lines.length;) { + var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); + child.height -= leaf.height; + this.children.splice(++i, 0, leaf); + leaf.parent = this; + } + child.lines = child.lines.slice(0, remaining); + this.maybeSpill(); + } + break + } + at -= sz; + } + }, + + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) { return } + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10) + me.parent.maybeSpill(); + }, + + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) { return true } + if ((n -= used) == 0) { break } + at = 0; + } else { at -= sz; } + } + } + }; + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = function(doc, node, options) { + if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) + { this[opt] = options[opt]; } } } + this.doc = doc; + this.node = node; + }; + + LineWidget.prototype.clear = function () { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) { return } + for (var i = 0; i < ws.length; ++i) { if (ws[i] == this) { ws.splice(i--, 1); } } + if (!ws.length) { line.widgets = null; } + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) { + runInOp(cm, function () { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + signalLater(cm, "lineWidgetCleared", cm, this, no); + } + }; + + LineWidget.prototype.changed = function () { + var this$1 = this; + + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) { return } + if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } + if (cm) { + runInOp(cm, function () { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); + }); + } + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + { addToScrollTop(cm, diff); } + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } + changeLine(doc, handle, "widget", function (line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) { widgets.push(widget); } + else { widgets.splice(Math.min(widgets.length, Math.max(0, widget.insertAt)), 0, widget); } + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) { addToScrollTop(cm, widget.height); } + cm.curOp.forceUpdate = true; + } + return true + }); + if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } + return widget + } + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + var TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + + // Clear the marker. + TextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) { startOperation(cm); } + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) { signalLater(this, "clear", found.from, found.to); } + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) { regLineChange(cm, lineNo(line), "text"); } + else if (cm) { + if (span.to != null) { max = lineNo(line); } + if (span.from != null) { min = lineNo(line); } + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + { updateLineHeight(line, textHeight(cm.display)); } + } + if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { + var visual = visualLine(this.lines[i$1]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } } + + if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) { reCheckSelection(cm.doc); } + } + if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } + if (withOp) { endOperation(cm); } + if (this.parent) { this.parent.clear(); } + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function (side, lineObj) { + if (side == null && this.type == "bookmark") { side = 1; } + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) { return from } + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) { return to } + } + } + return from && {from: from, to: to} + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function () { + var this$1 = this; + + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) { return } + runInOp(cm, function () { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + { updateLineHeight(line, line.height + dHeight); } + } + signalLater(cm, "markerChanged", cm, this$1); + }); + }; + + TextMarker.prototype.attachLine = function (line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } + } + this.lines.push(line); + }; + + TextMarker.prototype.detachLine = function (line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp + ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + eventMixin(TextMarker); + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) { return markTextShared(doc, from, to, options, type) } + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) { copyObj(options, marker, false); } + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + { return marker } + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } + if (options.insertLeft) { marker.widgetNode.insertLeft = true; } + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + { throw new Error("Inserting collapsed marker partially overlapping an existing one") } + seeCollapsedSpans(); + } + + if (marker.addToHistory) + { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function (line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + { updateMaxLine = true; } + if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { + if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } + }); } + + if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } + + if (marker.readOnly) { + seeReadOnlySpans(); + if (doc.history.done.length || doc.history.undone.length) + { doc.clearHistory(); } + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) { cm.curOp.updateMaxLine = true; } + if (marker.collapsed) + { regChange(cm, from.line, to.line + 1); } + else if (marker.className || marker.startStyle || marker.endStyle || marker.css || + marker.attributes || marker.title) + { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } + if (marker.atomic) { reCheckSelection(cm.doc); } + signalLater(cm, "markerAdded", cm, marker); + } + return marker + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + { markers[i].parent = this; } + }; + + SharedTextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + { this.markers[i].clear(); } + signalLater(this, "clear"); + }; + + SharedTextMarker.prototype.find = function (side, lineObj) { + return this.primary.find(side, lineObj) + }; + eventMixin(SharedTextMarker); + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function (doc) { + if (widget) { options.widgetNode = widget.cloneNode(true); } + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + { if (doc.linked[i].isParent) { return } } + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary) + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + var loop = function ( i ) { + var marker = markers[i], linked = [marker.primary.doc]; + linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + }; + + for (var i = 0; i < markers.length; i++) loop( i ); + } + + var nextDocId = 0; + var Doc = function(text, mode, firstLine, lineSep, direction) { + if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } + if (firstLine == null) { firstLine = 0; } + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.modeFrontier = this.highlightFrontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + this.lineSep = lineSep; + this.direction = (direction == "rtl") ? "rtl" : "ltr"; + this.extend = false; + + if (typeof text == "string") { text = this.splitLines(text); } + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) { this.iterN(from - this.first, to - from, op); } + else { this.iterN(this.first, this.first + this.size, from); } + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: this.splitLines(code), origin: "setValue", full: true}, true); + if (this.cm) { scrollToCoords(this.cm, 0, 0); } + setSelection(this, simpleSelection(top), sel_dontScroll); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, + + getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, + getLineNumber: function(line) {return lineNo(line)}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") { line = getLine(this, line); } + return visualLine(line) + }, + + lineCount: function() {return this.size}, + firstLine: function() {return this.first}, + lastLine: function() {return this.first + this.size - 1}, + + clipPos: function(pos) {return clipPos(this, pos)}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") { pos = range.head; } + else if (start == "anchor") { pos = range.anchor; } + else if (start == "end" || start == "to" || start === false) { pos = range.to(); } + else { pos = range.from(); } + return pos + }, + listSelections: function() { return this.sel.ranges }, + somethingSelected: function() {return this.sel.somethingSelected()}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads), options); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) { return } + var out = []; + for (var i = 0; i < ranges.length; i++) + { out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); } + if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } + setSelection(this, normalizeSelection(this.cm, out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) { return lines } + else { return lines.join(lineSep || this.lineSeparator()) } + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) { sel = sel.join(lineSep || this.lineSeparator()); } + parts[i] = sel; + } + return parts + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + { dup[i] = code; } + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) + { makeChange(this, changes[i$1]); } + if (newSel) { setSelectionReplaceHistory(this, newSel); } + else if (this.cm) { ensureCursorVisible(this.cm); } + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } + for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } + return {undo: done, redo: undone} + }, + clearHistory: function() { + var this$1 = this; + + this.history = new History(this.history.maxGeneration); + linkedDocs(this, function (doc) { return doc.history = this$1.history; }, true); + }, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } + return this.history.generation + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration) + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)} + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + setGutterMarker: docMethodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", function (line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) { line.gutterMarkers = null; } + return true + }) + }), + + clearGutter: docMethodOp(function(gutterID) { + var this$1 = this; + + this.iter(function (line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + changeLine(this$1, line, "gutter", function () { + line.gutterMarkers[gutterID] = null; + if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } + return true + }); + } + }); + }), + + lineInfo: function(line) { + var n; + if (typeof line == "number") { + if (!isLine(this, line)) { return null } + n = line; + line = getLine(this, line); + if (!line) { return null } + } else { + n = lineNo(line); + if (n == null) { return null } + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets} + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) { line[prop] = cls; } + else if (classTest(cls).test(line[prop])) { return false } + else { line[prop] += " " + cls; } + return true + }) + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) { return false } + else if (cls == null) { line[prop] = null; } + else { + var found = cur.match(classTest(cls)); + if (!found) { return false } + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true + }) + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options) + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark") + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + { markers.push(span.marker.parent || span.marker); } + } } + return markers + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function (line) { + var spans = line.markedSpans; + if (spans) { for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(span.to != null && lineNo == from.line && from.ch >= span.to || + span.from == null && lineNo != from.line || + span.from != null && lineNo == to.line && span.from >= to.ch) && + (!filter || filter(span.marker))) + { found.push(span.marker.parent || span.marker); } + } } + ++lineNo; + }); + return found + }, + getAllMarks: function() { + var markers = []; + this.iter(function (line) { + var sps = line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) + { if (sps[i].from != null) { markers.push(sps[i].marker); } } } + }); + return markers + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first, sepSize = this.lineSeparator().length; + this.iter(function (line) { + var sz = line.text.length + sepSize; + if (sz > off) { ch = off; return true } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)) + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) { return 0 } + var sepSize = this.lineSeparator().length; + this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value + index += line.text.length + sepSize; + }); + return index + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep, this.direction); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc + }, + + linkedDoc: function(options) { + if (!options) { options = {}; } + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) { from = options.from; } + if (options.to != null && options.to < to) { to = options.to; } + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); + if (options.sharedHist) { copy.history = this.history + ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) { other = other.doc; } + if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) { continue } + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break + } } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode}, + getEditor: function() {return this.cm}, + + splitLines: function(str) { + if (this.lineSep) { return str.split(this.lineSep) } + return splitLinesAuto(str) + }, + lineSeparator: function() { return this.lineSep || "\n" }, + + setDirection: docMethodOp(function (dir) { + if (dir != "rtl") { dir = "ltr"; } + if (dir == this.direction) { return } + this.direction = dir; + this.iter(function (line) { return line.order = null; }); + if (this.cm) { directionChanged(this.cm); } + }) + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + clearDragCursor(cm); + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + { return } + e_preventDefault(e); + if (ie) { lastDrop = +new Date; } + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || cm.isReadOnly()) { return } + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var markAsReadAndPasteIfAllFilesAreRead = function () { + if (++read == n) { + operation(cm, function () { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, + text: cm.doc.splitLines( + text.filter(function (t) { return t != null; }).join(cm.doc.lineSeparator())), + origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(clipPos(cm.doc, pos), clipPos(cm.doc, changeEnd(change)))); + })(); + } + }; + var readTextFromFile = function (file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + var reader = new FileReader; + reader.onerror = function () { return markAsReadAndPasteIfAllFilesAreRead(); }; + reader.onload = function () { + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + text[i] = content; + markAsReadAndPasteIfAllFilesAreRead(); + }; + reader.readAsText(file); + }; + for (var i = 0; i < files.length; i++) { readTextFromFile(files[i], i); } + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function () { return cm.display.input.focus(); }, 20); + return + } + try { + var text$1 = e.dataTransfer.getData("Text"); + if (text$1) { + var selected; + if (cm.state.draggingText && !cm.state.draggingText.copy) + { selected = cm.listSelections(); } + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) + { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } + cm.replaceSelection(text$1, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e$1){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } + + e.dataTransfer.setData("Text", cm.getSelection()); + e.dataTransfer.effectAllowed = "copyMove"; + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) { img.parentNode.removeChild(img); } + } + } + + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) { return } + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.getElementsByClassName) { return } + var byClass = document.getElementsByClassName("CodeMirror"), editors = []; + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) { editors.push(cm); } + } + if (editors.length) { editors[0].operation(function () { + for (var i = 0; i < editors.length; i++) { f(editors[i]); } + }); } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) { return } + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function () { + if (resizeTimer == null) { resizeTimer = setTimeout(function () { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); } + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function () { return forEachCodeMirror(onBlur); }); + } + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + var keyNames = { + 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 224: "Mod", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; + + // Number keys + for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } + // Alphabetic keys + for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } + // Function keys + for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } + + var keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + "fallthrough": "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", + "Ctrl-O": "openLine" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + "fallthrough": ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/); + name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } + else if (/^a(lt)?$/i.test(mod)) { alt = true; } + else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } + else if (/^s(hift)?$/i.test(mod)) { shift = true; } + else { throw new Error("Unrecognized modifier name: " + mod) } + } + if (alt) { name = "Alt-" + name; } + if (ctrl) { name = "Ctrl-" + name; } + if (cmd) { name = "Cmd-" + name; } + if (shift) { name = "Shift-" + name; } + return name + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + function normalizeKeyMap(keymap) { + var copy = {}; + for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } + if (value == "...") { delete keymap[keyname]; continue } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val = (void 0), name = (void 0); + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) { copy[name] = val; } + else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } + } + delete keymap[keyname]; + } } + for (var prop in copy) { keymap[prop] = copy[prop]; } + return keymap + } + + function lookupKey(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) { return "nothing" } + if (found === "...") { return "multi" } + if (found != null && handle(found)) { return "handled" } + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + { return lookupKey(key, map.fallthrough, handle, context) } + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) { return result } + } + } + } + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + function isModifierKey(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" + } + + function addModifierNames(name, event, noShift) { + var base = name; + if (event.altKey && base != "Alt") { name = "Alt-" + name; } + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Mod") { name = "Cmd-" + name; } + if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } + return name + } + + // Look up the name of a key as indicated by an event object. + function keyName(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) { return false } + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) { return false } + // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, + // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) + if (event.keyCode == 3 && event.code) { name = event.code; } + return addModifierNames(name, event, noShift) + } + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function () { + for (var i = kill.length - 1; i >= 0; i--) + { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } + ensureCursorVisible(cm); + }); + } + + function moveCharLogically(line, ch, dir) { + var target = skipExtendingChars(line.text, ch + dir, dir); + return target < 0 || target > line.text.length ? null : target + } + + function moveLogically(line, start, dir) { + var ch = moveCharLogically(line, start.ch, dir); + return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") + } + + function endOfLine(visually, cm, lineObj, lineNo, dir) { + if (visually) { + if (cm.doc.direction == "rtl") { dir = -dir; } + var order = getOrder(lineObj, cm.doc.direction); + if (order) { + var part = dir < 0 ? lst(order) : order[0]; + var moveInStorageOrder = (dir < 0) == (part.level == 1); + var sticky = moveInStorageOrder ? "after" : "before"; + var ch; + // With a wrapped rtl chunk (possibly spanning multiple bidi parts), + // it could be that the last bidi part is not on the last visual line, + // since visual lines contain content order-consecutive chunks. + // Thus, in rtl, we are looking for the first (content-order) character + // in the rtl chunk that is on the last line (that is, the same line + // as the last (content-order) character). + if (part.level > 0 || cm.doc.direction == "rtl") { + var prep = prepareMeasureForLine(cm, lineObj); + ch = dir < 0 ? lineObj.text.length - 1 : 0; + var targetTop = measureCharPrepared(cm, prep, ch).top; + ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); + if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } + } else { ch = dir < 0 ? part.to : part.from; } + return new Pos(lineNo, ch, sticky) + } + } + return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") + } + + function moveVisually(cm, line, start, dir) { + var bidi = getOrder(line, cm.doc.direction); + if (!bidi) { return moveLogically(line, start, dir) } + if (start.ch >= line.text.length) { + start.ch = line.text.length; + start.sticky = "before"; + } else if (start.ch <= 0) { + start.ch = 0; + start.sticky = "after"; + } + var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; + if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { + // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, + // nothing interesting happens. + return moveLogically(line, start, dir) + } + + var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; + var prep; + var getWrappedLineExtent = function (ch) { + if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } + prep = prep || prepareMeasureForLine(cm, line); + return wrappedLineExtentChar(cm, line, prep, ch) + }; + var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); + + if (cm.doc.direction == "rtl" || part.level == 1) { + var moveInStorageOrder = (part.level == 1) == (dir < 0); + var ch = mv(start, moveInStorageOrder ? 1 : -1); + if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { + // Case 2: We move within an rtl part or in an rtl editor on the same visual line + var sticky = moveInStorageOrder ? "before" : "after"; + return new Pos(start.line, ch, sticky) + } + } + + // Case 3: Could not move within this bidi part in this visual line, so leave + // the current bidi part + + var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { + var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder + ? new Pos(start.line, mv(ch, 1), "before") + : new Pos(start.line, ch, "after"); }; + + for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { + var part = bidi[partPos]; + var moveInStorageOrder = (dir > 0) == (part.level != 1); + var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); + if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } + ch = moveInStorageOrder ? part.from : mv(part.to, -1); + if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } + } + }; + + // Case 3a: Look for other bidi parts on the same visual line + var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); + if (res) { return res } + + // Case 3b: Look for other bidi parts on the next visual line + var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); + if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { + res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); + if (res) { return res } + } + + // Case 4: Nowhere to move + return null + } + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = { + selectAll: selectAll, + singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, + killLine: function (cm) { return deleteNearSelection(cm, function (range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + { return {from: range.head, to: Pos(range.head.line + 1, 0)} } + else + { return {from: range.head, to: Pos(range.head.line, len)} } + } else { + return {from: range.from(), to: range.to()} + } + }); }, + deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) + }); }); }, + delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), to: range.from() + }); }); }, + delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()} + }); }, + delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos } + }); }, + undo: function (cm) { return cm.undo(); }, + redo: function (cm) { return cm.redo(); }, + undoSelection: function (cm) { return cm.undoSelection(); }, + redoSelection: function (cm) { return cm.redoSelection(); }, + goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, + goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, + goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1} + ); }, + goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, + {origin: "+move", bias: 1} + ); }, + goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1} + ); }, + goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") + }, sel_move); }, + goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div") + }, sel_move); }, + goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } + return pos + }, sel_move); }, + goLineUp: function (cm) { return cm.moveV(-1, "line"); }, + goLineDown: function (cm) { return cm.moveV(1, "line"); }, + goPageUp: function (cm) { return cm.moveV(-1, "page"); }, + goPageDown: function (cm) { return cm.moveV(1, "page"); }, + goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, + goCharRight: function (cm) { return cm.moveH(1, "char"); }, + goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, + goColumnRight: function (cm) { return cm.moveH(1, "column"); }, + goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, + goGroupRight: function (cm) { return cm.moveH(1, "group"); }, + goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, + goWordRight: function (cm) { return cm.moveH(1, "word"); }, + delCharBefore: function (cm) { return cm.deleteH(-1, "codepoint"); }, + delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, + delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, + delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, + delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, + delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, + indentAuto: function (cm) { return cm.indentSelection("smart"); }, + indentMore: function (cm) { return cm.indentSelection("add"); }, + indentLess: function (cm) { return cm.indentSelection("subtract"); }, + insertTab: function (cm) { return cm.replaceSelection("\t"); }, + insertSoftTab: function (cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(spaceStr(tabSize - col % tabSize)); + } + cm.replaceSelections(spaces); + }, + defaultTab: function (cm) { + if (cm.somethingSelected()) { cm.indentSelection("add"); } + else { cm.execCommand("insertTab"); } + }, + // Swap the two chars left and right of each selection's head. + // Move cursor behind the two swapped characters afterwards. + // + // Doesn't consider line feeds a character. + // Doesn't scan more than one line above to find a character. + // Doesn't do anything on an empty line. + // Doesn't do anything with non-empty selections. + transposeChars: function (cm) { return runInOp(cm, function () { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) { continue } + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) { + cur = new Pos(cur.line, 1); + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); + } + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); }, + newlineAndIndent: function (cm) { return runInOp(cm, function () { + var sels = cm.listSelections(); + for (var i = sels.length - 1; i >= 0; i--) + { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } + sels = cm.listSelections(); + for (var i$1 = 0; i$1 < sels.length; i$1++) + { cm.indentLine(sels[i$1].from().line, null, true); } + ensureCursorVisible(cm); + }); }, + openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, + toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } + }; + + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, visual, lineN, 1) + } + function lineEnd(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLineEnd(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, line, lineN, -1) + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line, cm.doc.direction); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(start.ch, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) + } + return start + } + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) { return false } + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + if (dropShift) { cm.display.shift = false; } + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) { return result } + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm) + } + + // Note that, despite the name, this function is also used to check + // for bound mouse clicks. + + var stopSeq = new Delayed; + + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) { return "handled" } + if (/\'$/.test(name)) + { cm.state.keySeq = null; } + else + { stopSeq.set(50, function () { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); } + if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } + } + return dispatchKeyInner(cm, name, e, handle) + } + + function dispatchKeyInner(cm, name, e, handle) { + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + { cm.state.keySeq = name; } + if (result == "handled") + { signalLater(cm, "keyHandled", cm, name, e); } + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + return !!result + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) { return false } + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) + || dispatchKey(cm, name, e, function (b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + { return doHandleBinding(cm, b) } + }) + } else { + return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) { return } + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + { cm.replaceSelection("", null, "cut"); } + } + if (gecko && !mac && !handled && code == 46 && e.shiftKey && !e.ctrlKey && document.execCommand) + { document.execCommand("cut"); } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + { showCrossHair(cm); } + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) { this.doc.sel.shift = false; } + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + // Some browsers fire keypress events for backspace + if (ch == "\x08") { return } + if (handleCharBinding(cm, e, ch)) { return } + cm.display.input.onKeyPress(e); + } + + var DOUBLECLICK_DELAY = 400; + + var PastClick = function(time, pos, button) { + this.time = time; + this.pos = pos; + this.button = button; + }; + + PastClick.prototype.compare = function (time, pos, button) { + return this.time + DOUBLECLICK_DELAY > time && + cmp(pos, this.pos) == 0 && button == this.button + }; + + var lastClick, lastDoubleClick; + function clickRepeat(pos, button) { + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { + lastClick = lastDoubleClick = null; + return "triple" + } else if (lastClick && lastClick.compare(now, pos, button)) { + lastDoubleClick = new PastClick(now, pos, button); + lastClick = null; + return "double" + } else { + lastClick = new PastClick(now, pos, button); + lastDoubleClick = null; + return "single" + } + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } + display.input.ensurePolled(); + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function () { return display.scroller.draggable = true; }, 100); + } + return + } + if (clickInGutter(cm, e)) { return } + var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; + window.focus(); + + // #3261: make sure, that we're not starting a second selection + if (button == 1 && cm.state.selectingText) + { cm.state.selectingText(e); } + + if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } + + if (button == 1) { + if (pos) { leftButtonDown(cm, pos, repeat, e); } + else if (e_target(e) == display.scroller) { e_preventDefault(e); } + } else if (button == 2) { + if (pos) { extendSelection(cm.doc, pos); } + setTimeout(function () { return display.input.focus(); }, 20); + } else if (button == 3) { + if (captureRightClick) { cm.display.input.onContextMenu(e); } + else { delayBlurEvent(cm); } + } + } + + function handleMappedButton(cm, button, pos, repeat, event) { + var name = "Click"; + if (repeat == "double") { name = "Double" + name; } + else if (repeat == "triple") { name = "Triple" + name; } + name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; + + return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { + if (typeof bound == "string") { bound = commands[bound]; } + if (!bound) { return false } + var done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + done = bound(cm, pos) != Pass; + } finally { + cm.state.suppressEdits = false; + } + return done + }) + } + + function configureMouse(cm, repeat, event) { + var option = cm.getOption("configureMouse"); + var value = option ? option(cm, repeat, event) : {}; + if (value.unit == null) { + var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; + value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; + } + if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } + if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } + if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } + return value + } + + function leftButtonDown(cm, pos, repeat, event) { + if (ie) { setTimeout(bind(ensureFocus, cm), 0); } + else { cm.curOp.focus = activeElt(); } + + var behavior = configureMouse(cm, repeat, event); + + var sel = cm.doc.sel, contained; + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && + repeat == "single" && (contained = sel.contains(pos)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && + (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) + { leftButtonStartDrag(cm, event, pos, behavior); } + else + { leftButtonSelect(cm, event, pos, behavior); } + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, event, pos, behavior) { + var display = cm.display, moved = false; + var dragEnd = operation(cm, function (e) { + if (webkit) { display.scroller.draggable = false; } + cm.state.draggingText = false; + if (cm.state.delayingBlurEvent) { + if (cm.hasFocus()) { cm.state.delayingBlurEvent = false; } + else { delayBlurEvent(cm); } + } + off(display.wrapper.ownerDocument, "mouseup", dragEnd); + off(display.wrapper.ownerDocument, "mousemove", mouseMove); + off(display.scroller, "dragstart", dragStart); + off(display.scroller, "drop", dragEnd); + if (!moved) { + e_preventDefault(e); + if (!behavior.addNew) + { extendSelection(cm.doc, pos, null, null, behavior.extend); } + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if ((webkit && !safari) || ie && ie_version == 9) + { setTimeout(function () {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus();}, 20); } + else + { display.input.focus(); } + } + }); + var mouseMove = function(e2) { + moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; + }; + var dragStart = function () { return moved = true; }; + // Let the drag handler handle this. + if (webkit) { display.scroller.draggable = true; } + cm.state.draggingText = dragEnd; + dragEnd.copy = !behavior.moveOnDrag; + on(display.wrapper.ownerDocument, "mouseup", dragEnd); + on(display.wrapper.ownerDocument, "mousemove", mouseMove); + on(display.scroller, "dragstart", dragStart); + on(display.scroller, "drop", dragEnd); + + cm.state.delayingBlurEvent = true; + setTimeout(function () { return display.input.focus(); }, 20); + // IE's approach to draggable + if (display.scroller.dragDrop) { display.scroller.dragDrop(); } + } + + function rangeForUnit(cm, pos, unit) { + if (unit == "char") { return new Range(pos, pos) } + if (unit == "word") { return cm.findWordAt(pos) } + if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } + var result = unit(cm, pos); + return new Range(result.from, result.to) + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, event, start, behavior) { + if (ie) { delayBlurEvent(cm); } + var display = cm.display, doc = cm.doc; + e_preventDefault(event); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (behavior.addNew && !behavior.extend) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + { ourRange = ranges[ourIndex]; } + else + { ourRange = new Range(start, start); } + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (behavior.unit == "rectangle") { + if (!behavior.addNew) { ourRange = new Range(start, start); } + start = posFromMouse(cm, event, true, true); + ourIndex = -1; + } else { + var range = rangeForUnit(cm, start, behavior.unit); + if (behavior.extend) + { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend); } + else + { ourRange = range; } + } + + if (!behavior.addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { + setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) { return } + lastPos = pos; + + if (behavior.unit == "rectangle") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } + else if (text.length > leftPos) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } + } + if (!ranges.length) { ranges.push(new Range(start, start)); } + setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var range = rangeForUnit(cm, pos, behavior.unit); + var anchor = oldRange.anchor, head; + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + var ranges$1 = startSel.ranges.slice(0); + ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); + setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); + if (!cur) { return } + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) { setTimeout(operation(cm, function () { + if (counter != curCount) { return } + display.scroller.scrollTop += outside; + extend(e); + }), 50); } + } + } + + function done(e) { + cm.state.selectingText = false; + counter = Infinity; + // If e is null or undefined we interpret this as someone trying + // to explicitly cancel the selection rather than the user + // letting go of the mouse button. + if (e) { + e_preventDefault(e); + display.input.focus(); + } + off(display.wrapper.ownerDocument, "mousemove", move); + off(display.wrapper.ownerDocument, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function (e) { + if (e.buttons === 0 || !e_button(e)) { done(e); } + else { extend(e); } + }); + var up = operation(cm, done); + cm.state.selectingText = up; + on(display.wrapper.ownerDocument, "mousemove", move); + on(display.wrapper.ownerDocument, "mouseup", up); + } + + // Used when mouse-selecting to adjust the anchor to the proper side + // of a bidi jump depending on the visual position of the head. + function bidiSimplify(cm, range) { + var anchor = range.anchor; + var head = range.head; + var anchorLine = getLine(cm.doc, anchor.line); + if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } + var order = getOrder(anchorLine); + if (!order) { return range } + var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; + if (part.from != anchor.ch && part.to != anchor.ch) { return range } + var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); + if (boundary == 0 || boundary == order.length) { return range } + + // Compute the relative visual position of the head compared to the + // anchor (<0 is to the left, >0 to the right) + var leftSide; + if (head.line != anchor.line) { + leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; + } else { + var headIndex = getBidiPartAt(order, head.ch, head.sticky); + var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); + if (headIndex == boundary - 1 || headIndex == boundary) + { leftSide = dir < 0; } + else + { leftSide = dir > 0; } + } + + var usePart = order[boundary + (leftSide ? -1 : 0)]; + var from = leftSide == (usePart.level == 1); + var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; + return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) + } + + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent) { + var mX, mY; + if (e.touches) { + mX = e.touches[0].clientX; + mY = e.touches[0].clientY; + } else { + try { mX = e.clientX; mY = e.clientY; } + catch(e$1) { return false } + } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } + if (prevent) { e_preventDefault(e); } + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.display.gutterSpecs.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.display.gutterSpecs[i]; + signal(cm, type, cm, line, gutter.className, e); + return e_defaultPrevented(e) + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true) + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } + if (signalDOMEvent(cm, e, "contextmenu")) { return } + if (!captureRightClick) { cm.display.input.onContextMenu(e); } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) { return false } + return gutterEvent(cm, e, "gutterContextMenu", false) + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + var Init = {toString: function(){return "CodeMirror.Init"}}; + + var defaults = {}; + var optionHandlers = {}; + + function defineOptions(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) { optionHandlers[name] = + notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } + } + + CodeMirror.defineOption = option; + + // Passed to option handlers when there is no old value. + CodeMirror.Init = Init; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function (cm, val) { return cm.setValue(val); }, true); + option("mode", null, function (cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function (cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + + option("lineSeparator", null, function (cm, val) { + cm.doc.lineSep = val; + if (!val) { return } + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function (line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) { break } + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } + }); + option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200c\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != Init) { cm.refresh(); } + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function () { + throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME + }, true); + option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); + option("autocorrect", false, function (cm, val) { return cm.getInputField().autocorrect = val; }, true); + option("autocapitalize", false, function (cm, val) { return cm.getInputField().autocapitalize = val; }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function (cm) { + themeChanged(cm); + updateGutters(cm); + }, true); + option("keyMap", "default", function (cm, val, old) { + var next = getKeyMap(val); + var prev = old != Init && getKeyMap(old); + if (prev && prev.detach) { prev.detach(cm, next); } + if (next.attach) { next.attach(cm, prev || null); } + }); + option("extraKeys", null); + option("configureMouse", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function (cm, val) { + cm.display.gutterSpecs = getGutters(val, cm.options.lineNumbers); + updateGutters(cm); + }, true); + option("fixedGutter", true, function (cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); + option("scrollbarStyle", "native", function (cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function (cm, val) { + cm.display.gutterSpecs = getGutters(cm.options.gutters, val); + updateGutters(cm); + }, true); + option("firstLineNumber", 1, updateGutters, true); + option("lineNumberFormatter", function (integer) { return integer; }, updateGutters, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + option("pasteLinesPerSelection", true); + option("selectionsMayTouch", false); + + option("readOnly", false, function (cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + } + cm.display.input.readOnlyChanged(val); + }); + + option("screenReaderLabel", null, function (cm, val) { + val = (val === '') ? null : val; + cm.display.input.screenReaderLabelChanged(val); + }); + + option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); + option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function (cm, val) { + if (!val) { cm.display.input.resetPosition(); } + }); + + option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); + option("autofocus", null); + option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); + option("phrases", null); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function () { return updateScrollbars(cm); }, 100); + } + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + var this$1 = this; + + if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + + var doc = options.value; + if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } + else if (options.mode) { doc.modeOption = options.mode; } + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input, options); + display.wrapper.CodeMirror = this; + themeChanged(this); + if (options.lineWrapping) + { this.display.wrapper.className += " CodeMirror-wrap"; } + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll + selectingText: false, + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + if (options.autofocus && !mobile) { display.input.focus(); } + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || this.hasFocus()) + { setTimeout(function () { + if (this$1.hasFocus() && !this$1.state.focused) { onFocus(this$1); } + }, 20); } + else + { onBlur(this); } + + for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) + { optionHandlers[opt](this, options[opt], Init); } } + maybeUpdateLineNumberWidth(this); + if (options.finishInit) { options.finishInit(this); } + for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this); } + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + { display.lineDiv.style.textRendering = "auto"; } + } + + // The default configuration options. + CodeMirror.defaults = defaults; + // Functions to run when options are changed. + CodeMirror.optionHandlers = optionHandlers; + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + { on(d.scroller, "dblclick", operation(cm, function (e) { + if (signalDOMEvent(cm, e)) { return } + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); } + else + { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); + on(d.input.getField(), "contextmenu", function (e) { + if (!d.scroller.contains(e.target)) { onContextMenu(cm, e); } + }); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + } + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) { return false } + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1 + } + function farAway(touch, other) { + if (other.left == null) { return true } + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20 + } + on(d.scroller, "touchstart", function (e) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { + d.input.ensurePolled(); + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function () { + if (d.activeTouch) { d.activeTouch.moved = true; } + }); + on(d.scroller, "touchend", function (e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + { range = new Range(pos, pos); } + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + { range = cm.findWordAt(pos); } + else // Triple tap + { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function () { + if (d.scroller.clientHeight) { + updateScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); + on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, + over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, + start: function (e) { return onDragStart(cm, e); }, + drop: operation(cm, onDrop), + leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} + }; + + var inp = d.input.getField(); + on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", function (e) { return onFocus(cm, e); }); + on(inp, "blur", function (e) { return onBlur(cm, e); }); + } + + var initHooks = []; + CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) { how = "add"; } + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) { how = "prev"; } + else { state = getContextBefore(cm, n).state; } + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) { line.stateAfter = null; } + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) { return } + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } + else { indentation = 0; } + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } + if (pos < indentation) { indentString += spaceStr(indentation - pos); } + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { + var range = doc.sel.ranges[i$1]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos$1 = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); + break + } + } + } + } + + // This will be set to a {lineWise: bool, text: [string]} object, so + // that, when pasting, we know what kind of selections the copied + // text was made out of. + var lastCopied = null; + + function setLastCopied(newLastCopied) { + lastCopied = newLastCopied; + } + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) { sel = doc.sel; } + + var recent = +new Date - 200; + var paste = origin == "paste" || cm.state.pasteIncoming > recent; + var textLines = splitLinesAuto(inserted), multiPaste = null; + // When pasting N lines into N selections, insert one line per selection + if (paste && sel.ranges.length > 1) { + if (lastCopied && lastCopied.text.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.text.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.text.length; i++) + { multiPaste.push(doc.splitLines(lastCopied.text[i])); } + } + } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { + multiPaste = map(textLines, function (l) { return [l]; }); + } + } + + var updateInput = cm.curOp.updateInput; + // Normal behavior is to insert the new text into every selection + for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { + var range = sel.ranges[i$1]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + { from = Pos(from.line, from.ch - deleted); } + else if (cm.state.overwrite && !paste) // Handle overwrite + { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } + else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n")) + { from = to = Pos(from.line, 0); } + } + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, + origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !paste) + { triggerElectric(cm, inserted); } + + ensureCursorVisible(cm); + if (cm.curOp.updateInput < 2) { cm.curOp.updateInput = updateInput; } + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = -1; + } + + function handlePaste(e, cm) { + var pasted = e.clipboardData && e.clipboardData.getData("Text"); + if (pasted) { + e.preventDefault(); + if (!cm.isReadOnly() && !cm.options.disableInput) + { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } + return true + } + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) { return } + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } + var mode = cm.getModeAt(range.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range.head.line, "smart"); + break + } } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + { indented = indentLine(cm, range.head.line, "smart"); } + } + if (indented) { signalLater(cm, "electricInput", cm, range.head.line); } + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges} + } + + function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { + field.setAttribute("autocorrect", autocorrect ? "" : "off"); + field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); + field.setAttribute("spellcheck", !!spellcheck); + } + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) { te.style.width = "1000px"; } + else { te.setAttribute("wrap", "off"); } + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) { te.style.border = "1px solid black"; } + disableBrowserMagic(te); + return div + } + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + function addEditorMethods(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + var helpers = CodeMirror.helpers = {}; + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") { return } + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + { operation(this, optionHandlers[option])(this, value, old); } + signal(this, "optionChange", this, option); + }, + + getOption: function(option) {return this.options[option]}, + getDoc: function() {return this.doc}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + { if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true + } } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) { throw new Error("Overlays may not be stateful.") } + insertSorted(this.state.overlays, + {mode: mode, modeSpec: spec, opaque: options && options.opaque, + priority: (options && options.priority) || 0}, + function (overlay) { return overlay.priority; }); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } + else { dir = dir ? "add" : "subtract"; } + } + if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + { indentLine(this, j, how); } + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + { replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) { ensureCursorVisible(this); } + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise) + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true) + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) { type = styles[2]; } + else { for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } + else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } + else { type = styles[mid * 2 + 2]; break } + } } + var cut = type ? type.indexOf("overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) { return mode } + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0] + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) { return found } + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) { found.push(help[mode[type]]); } + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) { found.push(val); } + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i$1 = 0; i$1 < help._global.length; i$1++) { + var cur = help._global[i$1]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + { found.push(cur.val); } + } + return found + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getContextBefore(this, line + 1, precise).state + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) { pos = range.head; } + else if (typeof start == "object") { pos = clipPos(this.doc, start); } + else { pos = start ? range.from() : range.to(); } + return cursorCoords(this, pos, mode || "page") + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page") + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top) + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset) + }, + heightAtLine: function(line, mode, includeWidgets) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) { line = this.doc.first; } + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + + (end ? this.doc.height - heightAtLine(lineObj) : 0) + }, + + defaultTextHeight: function() { return textHeight(this.display) }, + defaultCharWidth: function() { return charWidth(this.display) }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + { top = pos.top - node.offsetHeight; } + else if (pos.bottom + node.offsetHeight <= vspace) + { top = pos.bottom; } + if (left + node.offsetWidth > hspace) + { left = hspace - node.offsetWidth; } + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") { left = 0; } + else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } + node.style.left = left + "px"; + } + if (scroll) + { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + triggerOnMouseDown: methodOp(onMouseDown), + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + { return commands[cmd].call(null, this) } + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) { break } + } + return cur + }, + + moveH: methodOp(function(dir, unit) { + var this$1 = this; + + this.extendSelectionsBy(function (range) { + if (this$1.display.shift || this$1.doc.extend || range.empty()) + { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } + else + { return dir < 0 ? range.from() : range.to() } + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + { doc.replaceSelection("", null, "+delete"); } + else + { deleteNearSelection(this, function (range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} + }); } + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) { x = coords.left; } + else { coords.left = x; } + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) { break } + } + return cur + }, + + moveV: methodOp(function(dir, unit) { + var this$1 = this; + + var doc = this.doc, goals = []; + var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function (range) { + if (collapse) + { return dir < 0 ? range.from() : range.to() } + var headPos = cursorCoords(this$1, range.head, "div"); + if (range.goalColumn != null) { headPos.left = range.goalColumn; } + goals.push(headPos.left); + var pos = findPosV(this$1, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } + return pos + }, sel_move); + if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) + { doc.sel.ranges[i].goalColumn = goals[i]; } } + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function (ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } + : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; + while (start > 0 && check(line.charAt(start - 1))) { --start; } + while (end < line.length && check(line.charAt(end))) { ++end; } + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)) + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) { return } + if (this.state.overwrite = !this.state.overwrite) + { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + else + { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt() }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, + + scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)} + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) { margin = this.options.cursorScrollMargin; } + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) { range.to = range.from; } + range.margin = margin || 0; + + if (range.from.line != null) { + scrollToRange(this, range); + } else { + scrollToCoordsRange(this, range.from, range.to, range.margin); + } + }), + + setSize: methodOp(function(width, height) { + var this$1 = this; + + var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; + if (width != null) { this.display.wrapper.style.width = interpret(width); } + if (height != null) { this.display.wrapper.style.height = interpret(height); } + if (this.options.lineWrapping) { clearLineMeasurementCache(this); } + var lineNo = this.display.viewFrom; + this.doc.iter(lineNo, this.display.viewTo, function (line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) + { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } + ++lineNo; + }); + this.curOp.forceUpdate = true; + signal(this, "refresh", this); + }), + + operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this.display); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) + { estimateLineHeights(this); } + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + // Cancel the current text selection if any (#5821) + if (this.state.selectingText) { this.state.selectingText(); } + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + scrollToCoords(this, doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old + }), + + phrase: function(phraseText) { + var phrases = this.options.phrases; + return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText + }, + + getInputField: function(){return this.display.input.getField()}, + getWrapperElement: function(){return this.display.wrapper}, + getScrollerElement: function(){return this.display.scroller}, + getGutterElement: function(){return this.display.gutters} + }; + eventMixin(CodeMirror); + + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "codepoint", "char", "column" (like char, but + // doesn't cross line boundaries), "word" (across next word), or + // "group" (to the start of next group of word or + // non-word-non-whitespace chars). The visually param controls + // whether, in right-to-left text, direction 1 means to move towards + // the next index in the string, or towards the character to the right + // of the current position. The resulting position will have a + // hitSide=true property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var oldPos = pos; + var origDir = dir; + var lineObj = getLine(doc, pos.line); + var lineDir = visually && doc.direction == "rtl" ? -dir : dir; + function findNextLine() { + var l = pos.line + lineDir; + if (l < doc.first || l >= doc.first + doc.size) { return false } + pos = new Pos(l, pos.ch, pos.sticky); + return lineObj = getLine(doc, l) + } + function moveOnce(boundToLine) { + var next; + if (unit == "codepoint") { + var ch = lineObj.text.charCodeAt(pos.ch + (unit > 0 ? 0 : -1)); + if (isNaN(ch)) { + next = null; + } else { + var astral = dir > 0 ? ch >= 0xD800 && ch < 0xDC00 : ch >= 0xDC00 && ch < 0xDFFF; + next = new Pos(pos.line, Math.max(0, Math.min(lineObj.text.length, pos.ch + dir * (astral ? 2 : 1))), -dir); + } + } else if (visually) { + next = moveVisually(doc.cm, lineObj, pos, dir); + } else { + next = moveLogically(lineObj, pos, dir); + } + if (next == null) { + if (!boundToLine && findNextLine()) + { pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir); } + else + { return false } + } else { + pos = next; + } + return true + } + + if (unit == "char" || unit == "codepoint") { + moveOnce(); + } else if (unit == "column") { + moveOnce(true); + } else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) { break } + var cur = lineObj.text.charAt(pos.ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) { type = "s"; } + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} + break + } + + if (type) { sawType = type; } + if (dir > 0 && !moveOnce(!first)) { break } + } + } + var result = skipAtomic(doc, pos, oldPos, origDir, true); + if (equalCursorPos(oldPos, result)) { result.hitSide = true; } + return result + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); + y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; + + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + var target; + for (;;) { + target = coordsChar(cm, x, y); + if (!target.outside) { break } + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } + y += dir * 5; + } + return target + } + + // CONTENTEDITABLE INPUT STYLE + + var ContentEditableInput = function(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.composing = null; + this.gracePeriod = false; + this.readDOMTimeout = null; + }; + + ContentEditableInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize); + + function belongsToInput(e) { + for (var t = e.target; t; t = t.parentNode) { + if (t == div) { return true } + if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) { break } + } + return false + } + + on(div, "paste", function (e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + // IE doesn't fire input events, so we schedule a read for the pasted content in this way + if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } + }); + + on(div, "compositionstart", function (e) { + this$1.composing = {data: e.data, done: false}; + }); + on(div, "compositionupdate", function (e) { + if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } + }); + on(div, "compositionend", function (e) { + if (this$1.composing) { + if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } + this$1.composing.done = true; + } + }); + + on(div, "touchstart", function () { return input.forceCompositionEnd(); }); + + on(div, "input", function () { + if (!this$1.composing) { this$1.readFromDOMSoon(); } + }); + + function onCopyCut(e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.operation(function () { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + if (e.clipboardData) { + e.clipboardData.clearData(); + var content = lastCopied.text.join("\n"); + // iOS exposes the clipboard API, but seems to discard content inserted into it + e.clipboardData.setData("Text", content); + if (e.clipboardData.getData("Text") == content) { + e.preventDefault(); + return + } + } + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.text.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function () { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + if (hadFocus == div) { input.showPrimarySelection(); } + }, 50); + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }; + + ContentEditableInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.div.setAttribute('aria-label', label); + } else { + this.div.removeAttribute('aria-label'); + } + }; + + ContentEditableInput.prototype.prepareSelection = function () { + var result = prepareSelection(this.cm, false); + result.focus = document.activeElement == this.div; + return result + }; + + ContentEditableInput.prototype.showSelection = function (info, takeFocus) { + if (!info || !this.cm.display.view.length) { return } + if (info.focus || takeFocus) { this.showPrimarySelection(); } + this.showMultipleSelections(info); + }; + + ContentEditableInput.prototype.getSelection = function () { + return this.cm.display.wrapper.ownerDocument.getSelection() + }; + + ContentEditableInput.prototype.showPrimarySelection = function () { + var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); + var from = prim.from(), to = prim.to(); + + if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { + sel.removeAllRanges(); + return + } + + var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), from) == 0 && + cmp(maxPos(curAnchor, curFocus), to) == 0) + { return } + + var view = cm.display.view; + var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || + {node: view[0].measure.map[2], offset: 0}; + var end = to.line < cm.display.viewTo && posToDOM(cm, to); + if (!end) { + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + if (!start || !end) { + sel.removeAllRanges(); + return + } + + var old = sel.rangeCount && sel.getRangeAt(0), rng; + try { rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + if (!gecko && cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) { + sel.removeAllRanges(); + sel.addRange(rng); + } + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } + if (old && sel.anchorNode == null) { sel.addRange(old); } + else if (gecko) { this.startGracePeriod(); } + } + this.rememberSelection(); + }; + + ContentEditableInput.prototype.startGracePeriod = function () { + var this$1 = this; + + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function () { + this$1.gracePeriod = false; + if (this$1.selectionChanged()) + { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } + }, 20); + }; + + ContentEditableInput.prototype.showMultipleSelections = function (info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }; + + ContentEditableInput.prototype.rememberSelection = function () { + var sel = this.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }; + + ContentEditableInput.prototype.selectionInEditor = function () { + var sel = this.getSelection(); + if (!sel.rangeCount) { return false } + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node) + }; + + ContentEditableInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor") { + if (!this.selectionInEditor() || document.activeElement != this.div) + { this.showSelection(this.prepareSelection(), true); } + this.div.focus(); + } + }; + ContentEditableInput.prototype.blur = function () { this.div.blur(); }; + ContentEditableInput.prototype.getField = function () { return this.div }; + + ContentEditableInput.prototype.supportsTouch = function () { return true }; + + ContentEditableInput.prototype.receivedFocus = function () { + var input = this; + if (this.selectionInEditor()) + { this.pollSelection(); } + else + { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }; + + ContentEditableInput.prototype.selectionChanged = function () { + var sel = this.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset + }; + + ContentEditableInput.prototype.pollSelection = function () { + if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } + var sel = this.getSelection(), cm = this.cm; + // On Android Chrome (version 56, at least), backspacing into an + // uneditable block element will put the cursor in that element, + // and then, because it's not editable, hide the virtual keyboard. + // Because Android doesn't allow us to actually detect backspace + // presses in a sane way, this code checks for when that happens + // and simulates a backspace press in this case. + if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { + this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); + this.blur(); + this.focus(); + return + } + if (this.composing) { return } + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) { runInOp(cm, function () { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } + }); } + }; + + ContentEditableInput.prototype.pollContent = function () { + if (this.readDOMTimeout != null) { + clearTimeout(this.readDOMTimeout); + this.readDOMTimeout = null; + } + + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.ch == 0 && from.line > cm.firstLine()) + { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } + if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) + { to = Pos(to.line + 1, 0); } + if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } + + var fromIndex, fromLine, fromNode; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + fromLine = lineNo(display.view[0].line); + fromNode = display.view[0].node; + } else { + fromLine = lineNo(display.view[fromIndex].line); + fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + var toLine, toNode; + if (toIndex == display.view.length - 1) { + toLine = display.viewTo - 1; + toNode = display.lineDiv.lastChild; + } else { + toLine = lineNo(display.view[toIndex + 1].line) - 1; + toNode = display.view[toIndex + 1].node.previousSibling; + } + + if (!fromNode) { return false } + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else { break } + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + { ++cutFront; } + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + { ++cutEnd; } + // Try to move start of change to start of selection if ambiguous + if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { + while (cutFront && cutFront > from.ch && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { + cutFront--; + cutEnd++; + } + } + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); + newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true + } + }; + + ContentEditableInput.prototype.ensurePolled = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.reset = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.forceCompositionEnd = function () { + if (!this.composing) { return } + clearTimeout(this.readDOMTimeout); + this.composing = null; + this.updateFromDOM(); + this.div.blur(); + this.div.focus(); + }; + ContentEditableInput.prototype.readFromDOMSoon = function () { + var this$1 = this; + + if (this.readDOMTimeout != null) { return } + this.readDOMTimeout = setTimeout(function () { + this$1.readDOMTimeout = null; + if (this$1.composing) { + if (this$1.composing.done) { this$1.composing = null; } + else { return } + } + this$1.updateFromDOM(); + }, 80); + }; + + ContentEditableInput.prototype.updateFromDOM = function () { + var this$1 = this; + + if (this.cm.isReadOnly() || !this.pollContent()) + { runInOp(this.cm, function () { return regChange(this$1.cm); }); } + }; + + ContentEditableInput.prototype.setUneditable = function (node) { + node.contentEditable = "false"; + }; + + ContentEditableInput.prototype.onKeyPress = function (e) { + if (e.charCode == 0 || this.composing) { return } + e.preventDefault(); + if (!this.cm.isReadOnly()) + { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } + }; + + ContentEditableInput.prototype.readOnlyChanged = function (val) { + this.div.contentEditable = String(val != "nocursor"); + }; + + ContentEditableInput.prototype.onContextMenu = function () {}; + ContentEditableInput.prototype.resetPosition = function () {}; + + ContentEditableInput.prototype.needsContentAttribute = true; + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) { return null } + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line, cm.doc.direction), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result + } + + function isInGutter(node) { + for (var scan = node; scan; scan = scan.parentNode) + { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } + return false + } + + function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; + function recognizeMarker(id) { return function (marker) { return marker.id == id; } } + function close() { + if (closing) { + text += lineSep; + if (extraLinebreak) { text += lineSep; } + closing = extraLinebreak = false; + } + } + function addText(str) { + if (str) { + close(); + text += str; + } + } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText) { + addText(cmText); + return + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find(0))) + { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)); } + return + } + if (node.getAttribute("contenteditable") == "false") { return } + var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); + if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } + + if (isBlock) { close(); } + for (var i = 0; i < node.childNodes.length; i++) + { walk(node.childNodes[i]); } + + if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } + if (isBlock) { closing = true; } + } else if (node.nodeType == 3) { + addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); + } + } + for (;;) { + walk(from); + if (from == to) { break } + from = from.nextSibling; + extraLinebreak = false; + } + return text + } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) { return null } + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + { return locateNodeInLineView(lineView, node, offset) } + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad) + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) { offset = textNode.nodeValue.length; } + } + while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)]; } + return Pos(line, ch) + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) { return badPos(found, bad) } + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + { return badPos(Pos(found.line, found.ch - dist), bad) } + else + { dist += after.textContent.length; } + } + for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + { return badPos(Pos(found.line, found.ch + dist$1), bad) } + else + { dist$1 += before.textContent.length; } + } + } + + // TEXTAREA INPUT STYLE + + var TextareaInput = function(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + TextareaInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = this.cm; + this.createField(display); + var te = this.textarea; + + display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) { te.style.width = "0px"; } + + on(te, "input", function () { + if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } + input.poll(); + }); + + on(te, "paste", function (e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + + cm.state.pasteIncoming = +new Date; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") { cm.state.cutIncoming = +new Date; } + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function (e) { + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } + if (!te.dispatchEvent) { + cm.state.pasteIncoming = +new Date; + input.focus(); + return + } + + // Pass the `paste` event to the textarea so it's handled by its event listener. + var event = new Event("paste"); + event.clipboardData = e.clipboardData; + te.dispatchEvent(event); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function (e) { + if (!eventInWidget(display, e)) { e_preventDefault(e); } + }); + + on(te, "compositionstart", function () { + var start = cm.getCursor("from"); + if (input.composing) { input.composing.range.clear(); } + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function () { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }; + + TextareaInput.prototype.createField = function (_display) { + // Wraps and hides input textarea + this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + this.textarea = this.wrapper.firstChild; + }; + + TextareaInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.textarea.setAttribute('aria-label', label); + } else { + this.textarea.removeAttribute('aria-label'); + } + }; + + TextareaInput.prototype.prepareSelection = function () { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result + }; + + TextareaInput.prototype.showSelection = function (drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }; + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + TextareaInput.prototype.reset = function (typing) { + if (this.contextMenuPending || this.composing) { return } + var cm = this.cm; + if (cm.somethingSelected()) { + this.prevInput = ""; + var content = cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) { selectInput(this.textarea); } + if (ie && ie_version >= 9) { this.hasSelection = content; } + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) { this.hasSelection = null; } + } + }; + + TextareaInput.prototype.getField = function () { return this.textarea }; + + TextareaInput.prototype.supportsTouch = function () { return false }; + + TextareaInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }; + + TextareaInput.prototype.blur = function () { this.textarea.blur(); }; + + TextareaInput.prototype.resetPosition = function () { + this.wrapper.style.top = this.wrapper.style.left = 0; + }; + + TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + TextareaInput.prototype.slowPoll = function () { + var this$1 = this; + + if (this.pollingFast) { return } + this.polling.set(this.cm.options.pollInterval, function () { + this$1.poll(); + if (this$1.cm.state.focused) { this$1.slowPoll(); } + }); + }; + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + TextareaInput.prototype.fastPoll = function () { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + TextareaInput.prototype.poll = function () { + var this$1 = this; + + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (this.contextMenuPending || !cm.state.focused || + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) + { return false } + + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) { return false } + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } + + runInOp(cm, function () { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, this$1.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } + else { this$1.prevInput = text; } + + if (this$1.composing) { + this$1.composing.range.clear(); + this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true + }; + + TextareaInput.prototype.ensurePolled = function () { + if (this.pollingFast && this.poll()) { this.pollingFast = false; } + }; + + TextareaInput.prototype.onKeyPress = function () { + if (ie && ie_version >= 9) { this.hasSelection = null; } + this.fastPoll(); + }; + + TextareaInput.prototype.onContextMenu = function (e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + if (input.contextMenuPending) { input.contextMenuPending(); } + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) { return } // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } + + var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; + var wrapperBox = input.wrapper.offsetParent.getBoundingClientRect(); + input.wrapper.style.cssText = "position: static"; + te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + var oldScrollY; + if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) { window.scrollTo(null, oldScrollY); } + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } + input.contextMenuPending = rehide; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + if (input.contextMenuPending != rehide) { return } + input.contextMenuPending = false; + input.wrapper.style.cssText = oldWrapperCSS; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } + var i = 0, poll = function () { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") { + operation(cm, selectAll)(cm); + } else if (i++ < 10) { + display.detectingSelectAll = setTimeout(poll, 500); + } else { + display.selForContextMenu = null; + display.input.reset(); + } + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) { prepareSelectAllHack(); } + if (captureRightClick) { + e_stop(e); + var mouseup = function () { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }; + + TextareaInput.prototype.readOnlyChanged = function (val) { + if (!val) { this.reset(); } + this.textarea.disabled = val == "nocursor"; + this.textarea.readOnly = !!val; + }; + + TextareaInput.prototype.setUneditable = function () {}; + + TextareaInput.prototype.needsContentAttribute = false; + + function fromTextArea(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + { options.tabindex = textarea.tabIndex; } + if (!options.placeholder && textarea.placeholder) + { options.placeholder = textarea.placeholder; } + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + + var realSubmit; + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form; + realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function () { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function (cm) { + cm.save = save; + cm.getTextArea = function () { return textarea; }; + cm.toTextArea = function () { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (!options.leaveSubmitMethodAlone && typeof textarea.form.submit == "function") + { textarea.form.submit = realSubmit; } + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, + options); + return cm + } + + function addLegacyProps(CodeMirror) { + CodeMirror.off = off; + CodeMirror.on = on; + CodeMirror.wheelEventPixels = wheelEventPixels; + CodeMirror.Doc = Doc; + CodeMirror.splitLines = splitLinesAuto; + CodeMirror.countColumn = countColumn; + CodeMirror.findColumn = findColumn; + CodeMirror.isWordChar = isWordCharBasic; + CodeMirror.Pass = Pass; + CodeMirror.signal = signal; + CodeMirror.Line = Line; + CodeMirror.changeEnd = changeEnd; + CodeMirror.scrollbarModel = scrollbarModel; + CodeMirror.Pos = Pos; + CodeMirror.cmpPos = cmp; + CodeMirror.modes = modes; + CodeMirror.mimeModes = mimeModes; + CodeMirror.resolveMode = resolveMode; + CodeMirror.getMode = getMode; + CodeMirror.modeExtensions = modeExtensions; + CodeMirror.extendMode = extendMode; + CodeMirror.copyState = copyState; + CodeMirror.startState = startState; + CodeMirror.innerMode = innerMode; + CodeMirror.commands = commands; + CodeMirror.keyMap = keyMap; + CodeMirror.keyName = keyName; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.lookupKey = lookupKey; + CodeMirror.normalizeKeyMap = normalizeKeyMap; + CodeMirror.StringStream = StringStream; + CodeMirror.SharedTextMarker = SharedTextMarker; + CodeMirror.TextMarker = TextMarker; + CodeMirror.LineWidget = LineWidget; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + CodeMirror.e_stop = e_stop; + CodeMirror.addClass = addClass; + CodeMirror.contains = contains; + CodeMirror.rmClass = rmClass; + CodeMirror.keyNames = keyNames; + } + + // EDITOR CONSTRUCTOR + + defineOptions(CodeMirror); + + addEditorMethods(CodeMirror); + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); + for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + { CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments)} + })(Doc.prototype[prop]); } } + + eventMixin(Doc); + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name/*, mode, …*/) { + if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } + defineMode.apply(this, arguments); + }; + + CodeMirror.defineMIME = defineMIME; + + // Minimal default mode. + CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); + CodeMirror.defineMIME("text/plain", "null"); + + // EXTENSIONS + + CodeMirror.defineExtension = function (name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function (name, func) { + Doc.prototype[name] = func; + }; + + CodeMirror.fromTextArea = fromTextArea; + + addLegacyProps(CodeMirror); + + CodeMirror.version = "5.59.1"; + + return CodeMirror; + +}))); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/clike/clike.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/clike/clike.js new file mode 100644 index 0000000..2154f1d --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/clike/clike.js @@ -0,0 +1,935 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +function Context(indented, column, type, info, align, prev) { + this.indented = indented; + this.column = column; + this.type = type; + this.info = info; + this.align = align; + this.prev = prev; +} +function pushContext(state, col, type, info) { + var indent = state.indented; + if (state.context && state.context.type == "statement" && type != "statement") + indent = state.context.indented; + return state.context = new Context(indent, col, type, info, null, state.context); +} +function popContext(state) { + var t = state.context.type; + if (t == ")" || t == "]" || t == "}") + state.indented = state.context.indented; + return state.context = state.context.prev; +} + +function typeBefore(stream, state, pos) { + if (state.prevToken == "variable" || state.prevToken == "type") return true; + if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true; + if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true; +} + +function isTopScope(context) { + for (;;) { + if (!context || context.type == "top") return true; + if (context.type == "}" && context.prev.info != "namespace") return false; + context = context.prev; + } +} + +CodeMirror.defineMode("clike", function(config, parserConfig) { + var indentUnit = config.indentUnit, + statementIndentUnit = parserConfig.statementIndentUnit || indentUnit, + dontAlignCalls = parserConfig.dontAlignCalls, + keywords = parserConfig.keywords || {}, + types = parserConfig.types || {}, + builtin = parserConfig.builtin || {}, + blockKeywords = parserConfig.blockKeywords || {}, + defKeywords = parserConfig.defKeywords || {}, + atoms = parserConfig.atoms || {}, + hooks = parserConfig.hooks || {}, + multiLineStrings = parserConfig.multiLineStrings, + indentStatements = parserConfig.indentStatements !== false, + indentSwitch = parserConfig.indentSwitch !== false, + namespaceSeparator = parserConfig.namespaceSeparator, + isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/, + numberStart = parserConfig.numberStart || /[\d\.]/, + number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i, + isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/, + isIdentifierChar = parserConfig.isIdentifierChar || /[\w\$_\xa1-\uffff]/, + // An optional function that takes a {string} token and returns true if it + // should be treated as a builtin. + isReservedIdentifier = parserConfig.isReservedIdentifier || false; + + var curPunc, isDefKeyword; + + function tokenBase(stream, state) { + var ch = stream.next(); + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + if (numberStart.test(ch)) { + stream.backUp(1) + if (stream.match(number)) return "number" + stream.next() + } + if (isPunctuationChar.test(ch)) { + curPunc = ch; + return null; + } + if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } + if (stream.eat("/")) { + stream.skipToEnd(); + return "comment"; + } + } + if (isOperatorChar.test(ch)) { + while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {} + return "operator"; + } + stream.eatWhile(isIdentifierChar); + if (namespaceSeparator) while (stream.match(namespaceSeparator)) + stream.eatWhile(isIdentifierChar); + + var cur = stream.current(); + if (contains(keywords, cur)) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; + if (contains(defKeywords, cur)) isDefKeyword = true; + return "keyword"; + } + if (contains(types, cur)) return "type"; + if (contains(builtin, cur) + || (isReservedIdentifier && isReservedIdentifier(cur))) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; + return "builtin"; + } + if (contains(atoms, cur)) return "atom"; + return "variable"; + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next, end = false; + while ((next = stream.next()) != null) { + if (next == quote && !escaped) {end = true; break;} + escaped = !escaped && next == "\\"; + } + if (end || !(escaped || multiLineStrings)) + state.tokenize = null; + return "string"; + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return "comment"; + } + + function maybeEOL(stream, state) { + if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context)) + state.typeAtEndOfLine = typeBefore(stream, state, stream.pos) + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: null, + context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false), + indented: 0, + startOfLine: true, + prevToken: null + }; + }, + + token: function(stream, state) { + var ctx = state.context; + if (stream.sol()) { + if (ctx.align == null) ctx.align = false; + state.indented = stream.indentation(); + state.startOfLine = true; + } + if (stream.eatSpace()) { maybeEOL(stream, state); return null; } + curPunc = isDefKeyword = null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style == "comment" || style == "meta") return style; + if (ctx.align == null) ctx.align = true; + + if (curPunc == ";" || curPunc == ":" || (curPunc == "," && stream.match(/^\s*(?:\/\/.*)?$/, false))) + while (state.context.type == "statement") popContext(state); + else if (curPunc == "{") pushContext(state, stream.column(), "}"); + else if (curPunc == "[") pushContext(state, stream.column(), "]"); + else if (curPunc == "(") pushContext(state, stream.column(), ")"); + else if (curPunc == "}") { + while (ctx.type == "statement") ctx = popContext(state); + if (ctx.type == "}") ctx = popContext(state); + while (ctx.type == "statement") ctx = popContext(state); + } + else if (curPunc == ctx.type) popContext(state); + else if (indentStatements && + (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") || + (ctx.type == "statement" && curPunc == "newstatement"))) { + pushContext(state, stream.column(), "statement", stream.current()); + } + + if (style == "variable" && + ((state.prevToken == "def" || + (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) && + isTopScope(state.context) && stream.match(/^\s*\(/, false))))) + style = "def"; + + if (hooks.token) { + var result = hooks.token(stream, state, style); + if (result !== undefined) style = result; + } + + if (style == "def" && parserConfig.styleDefs === false) style = "variable"; + + state.startOfLine = false; + state.prevToken = isDefKeyword ? "def" : style || curPunc; + maybeEOL(stream, state); + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass; + var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); + var closing = firstChar == ctx.type; + if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; + if (parserConfig.dontIndentStatements) + while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info)) + ctx = ctx.prev + if (hooks.indent) { + var hook = hooks.indent(state, ctx, textAfter, indentUnit); + if (typeof hook == "number") return hook + } + var switchBlock = ctx.prev && ctx.prev.info == "switch"; + if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) { + while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev + return ctx.indented + } + if (ctx.type == "statement") + return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit); + if (ctx.align && (!dontAlignCalls || ctx.type != ")")) + return ctx.column + (closing ? 0 : 1); + if (ctx.type == ")" && !closing) + return ctx.indented + statementIndentUnit; + + return ctx.indented + (closing ? 0 : indentUnit) + + (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0); + }, + + electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/, + blockCommentStart: "/*", + blockCommentEnd: "*/", + blockCommentContinue: " * ", + lineComment: "//", + fold: "brace" + }; +}); + + function words(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + function contains(words, word) { + if (typeof words === "function") { + return words(word); + } else { + return words.propertyIsEnumerable(word); + } + } + var cKeywords = "auto if break case register continue return default do sizeof " + + "static else struct switch extern typedef union for goto while enum const " + + "volatile inline restrict asm fortran"; + + // Keywords from https://en.cppreference.com/w/cpp/keyword includes C++20. + var cppKeywords = "alignas alignof and and_eq audit axiom bitand bitor catch " + + "class compl concept constexpr const_cast decltype delete dynamic_cast " + + "explicit export final friend import module mutable namespace new noexcept " + + "not not_eq operator or or_eq override private protected public " + + "reinterpret_cast requires static_assert static_cast template this " + + "thread_local throw try typeid typename using virtual xor xor_eq"; + + var objCKeywords = "bycopy byref in inout oneway out self super atomic nonatomic retain copy " + + "readwrite readonly strong weak assign typeof nullable nonnull null_resettable _cmd " + + "@interface @implementation @end @protocol @encode @property @synthesize @dynamic @class " + + "@public @package @private @protected @required @optional @try @catch @finally @import " + + "@selector @encode @defs @synchronized @autoreleasepool @compatibility_alias @available"; + + var objCBuiltins = "FOUNDATION_EXPORT FOUNDATION_EXTERN NS_INLINE NS_FORMAT_FUNCTION " + + " NS_RETURNS_RETAINEDNS_ERROR_ENUM NS_RETURNS_NOT_RETAINED NS_RETURNS_INNER_POINTER " + + "NS_DESIGNATED_INITIALIZER NS_ENUM NS_OPTIONS NS_REQUIRES_NIL_TERMINATION " + + "NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_SWIFT_NAME NS_REFINED_FOR_SWIFT" + + // Do not use this. Use the cTypes function below. This is global just to avoid + // excessive calls when cTypes is being called multiple times during a parse. + var basicCTypes = words("int long char short double float unsigned signed " + + "void bool"); + + // Do not use this. Use the objCTypes function below. This is global just to avoid + // excessive calls when objCTypes is being called multiple times during a parse. + var basicObjCTypes = words("SEL instancetype id Class Protocol BOOL"); + + // Returns true if identifier is a "C" type. + // C type is defined as those that are reserved by the compiler (basicTypes), + // and those that end in _t (Reserved by POSIX for types) + // http://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html + function cTypes(identifier) { + return contains(basicCTypes, identifier) || /.+_t$/.test(identifier); + } + + // Returns true if identifier is a "Objective C" type. + function objCTypes(identifier) { + return cTypes(identifier) || contains(basicObjCTypes, identifier); + } + + var cBlockKeywords = "case do else for if switch while struct enum union"; + var cDefKeywords = "struct enum union"; + + function cppHook(stream, state) { + if (!state.startOfLine) return false + for (var ch, next = null; ch = stream.peek();) { + if (ch == "\\" && stream.match(/^.$/)) { + next = cppHook + break + } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) { + break + } + stream.next() + } + state.tokenize = next + return "meta" + } + + function pointerHook(_stream, state) { + if (state.prevToken == "type") return "type"; + return false; + } + + // For C and C++ (and ObjC): identifiers starting with __ + // or _ followed by a capital letter are reserved for the compiler. + function cIsReservedIdentifier(token) { + if (!token || token.length < 2) return false; + if (token[0] != '_') return false; + return (token[1] == '_') || (token[1] !== token[1].toLowerCase()); + } + + function cpp14Literal(stream) { + stream.eatWhile(/[\w\.']/); + return "number"; + } + + function cpp11StringHook(stream, state) { + stream.backUp(1); + // Raw strings. + if (stream.match(/(R|u8R|uR|UR|LR)/)) { + var match = stream.match(/"([^\s\\()]{0,16})\(/); + if (!match) { + return false; + } + state.cpp11RawStringDelim = match[1]; + state.tokenize = tokenRawString; + return tokenRawString(stream, state); + } + // Unicode strings/chars. + if (stream.match(/(u8|u|U|L)/)) { + if (stream.match(/["']/, /* eat */ false)) { + return "string"; + } + return false; + } + // Ignore this hook. + stream.next(); + return false; + } + + function cppLooksLikeConstructor(word) { + var lastTwo = /(\w+)::~?(\w+)$/.exec(word); + return lastTwo && lastTwo[1] == lastTwo[2]; + } + + // C#-style strings where "" escapes a quote. + function tokenAtString(stream, state) { + var next; + while ((next = stream.next()) != null) { + if (next == '"' && !stream.eat('"')) { + state.tokenize = null; + break; + } + } + return "string"; + } + + // C++11 raw string literal is "( anything )", where + // can be a string up to 16 characters long. + function tokenRawString(stream, state) { + // Escape characters that have special regex meanings. + var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&'); + var match = stream.match(new RegExp(".*?\\)" + delim + '"')); + if (match) + state.tokenize = null; + else + stream.skipToEnd(); + return "string"; + } + + function def(mimes, mode) { + if (typeof mimes == "string") mimes = [mimes]; + var words = []; + function add(obj) { + if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop)) + words.push(prop); + } + add(mode.keywords); + add(mode.types); + add(mode.builtin); + add(mode.atoms); + if (words.length) { + mode.helperType = mimes[0]; + CodeMirror.registerHelper("hintWords", mimes[0], words); + } + + for (var i = 0; i < mimes.length; ++i) + CodeMirror.defineMIME(mimes[i], mode); + } + + def(["text/x-csrc", "text/x-c", "text/x-chdr"], { + name: "clike", + keywords: words(cKeywords), + types: cTypes, + blockKeywords: words(cBlockKeywords), + defKeywords: words(cDefKeywords), + typeFirstDefinitions: true, + atoms: words("NULL true false"), + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + }, + modeProps: {fold: ["brace", "include"]} + }); + + def(["text/x-c++src", "text/x-c++hdr"], { + name: "clike", + keywords: words(cKeywords + " " + cppKeywords), + types: cTypes, + blockKeywords: words(cBlockKeywords + " class try catch"), + defKeywords: words(cDefKeywords + " class namespace"), + typeFirstDefinitions: true, + atoms: words("true false NULL nullptr"), + dontIndentStatements: /^template$/, + isIdentifierChar: /[\w\$_~\xa1-\uffff]/, + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + "u": cpp11StringHook, + "U": cpp11StringHook, + "L": cpp11StringHook, + "R": cpp11StringHook, + "0": cpp14Literal, + "1": cpp14Literal, + "2": cpp14Literal, + "3": cpp14Literal, + "4": cpp14Literal, + "5": cpp14Literal, + "6": cpp14Literal, + "7": cpp14Literal, + "8": cpp14Literal, + "9": cpp14Literal, + token: function(stream, state, style) { + if (style == "variable" && stream.peek() == "(" && + (state.prevToken == ";" || state.prevToken == null || + state.prevToken == "}") && + cppLooksLikeConstructor(stream.current())) + return "def"; + } + }, + namespaceSeparator: "::", + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-java", { + name: "clike", + keywords: words("abstract assert break case catch class const continue default " + + "do else enum extends final finally for goto if implements import " + + "instanceof interface native new package private protected public " + + "return static strictfp super switch synchronized this throw throws transient " + + "try volatile while @interface"), + types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " + + "Integer Long Number Object Short String StringBuffer StringBuilder Void"), + blockKeywords: words("catch class do else finally for if switch try while"), + defKeywords: words("class interface enum @interface"), + typeFirstDefinitions: true, + atoms: words("true false null"), + number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i, + hooks: { + "@": function(stream) { + // Don't match the @interface keyword. + if (stream.match('interface', false)) return false; + + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + }, + modeProps: {fold: ["brace", "import"]} + }); + + def("text/x-csharp", { + name: "clike", + keywords: words("abstract as async await base break case catch checked class const continue" + + " default delegate do else enum event explicit extern finally fixed for" + + " foreach goto if implicit in interface internal is lock namespace new" + + " operator out override params private protected public readonly ref return sealed" + + " sizeof stackalloc static struct switch this throw try typeof unchecked" + + " unsafe using virtual void volatile while add alias ascending descending dynamic from get" + + " global group into join let orderby partial remove select set value var yield"), + types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" + + " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" + + " UInt64 bool byte char decimal double short int long object" + + " sbyte float string ushort uint ulong"), + blockKeywords: words("catch class do else finally for foreach if struct switch try while"), + defKeywords: words("class interface namespace struct var"), + typeFirstDefinitions: true, + atoms: words("true false null"), + hooks: { + "@": function(stream, state) { + if (stream.eat('"')) { + state.tokenize = tokenAtString; + return tokenAtString(stream, state); + } + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + } + }); + + function tokenTripleString(stream, state) { + var escaped = false; + while (!stream.eol()) { + if (!escaped && stream.match('"""')) { + state.tokenize = null; + break; + } + escaped = stream.next() == "\\" && !escaped; + } + return "string"; + } + + function tokenNestedComment(depth) { + return function (stream, state) { + var ch + while (ch = stream.next()) { + if (ch == "*" && stream.eat("/")) { + if (depth == 1) { + state.tokenize = null + break + } else { + state.tokenize = tokenNestedComment(depth - 1) + return state.tokenize(stream, state) + } + } else if (ch == "/" && stream.eat("*")) { + state.tokenize = tokenNestedComment(depth + 1) + return state.tokenize(stream, state) + } + } + return "comment" + } + } + + def("text/x-scala", { + name: "clike", + keywords: words( + /* scala */ + "abstract case catch class def do else extends final finally for forSome if " + + "implicit import lazy match new null object override package private protected return " + + "sealed super this throw trait try type val var while with yield _ " + + + /* package scala */ + "assert assume require print println printf readLine readBoolean readByte readShort " + + "readChar readInt readLong readFloat readDouble" + ), + types: words( + "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " + + "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " + + "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " + + "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " + + "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " + + + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void" + ), + multiLineStrings: true, + blockKeywords: words("catch class enum do else finally for forSome if match switch try while"), + defKeywords: words("class enum def object package trait type val var"), + atoms: words("true false null"), + indentStatements: false, + indentSwitch: false, + isOperatorChar: /[+\-*&%=<>!?|\/#:@]/, + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + if (!stream.match('""')) return false; + state.tokenize = tokenTripleString; + return state.tokenize(stream, state); + }, + "'": function(stream) { + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + return "atom"; + }, + "=": function(stream, state) { + var cx = state.context + if (cx.type == "}" && cx.align && stream.eat(">")) { + state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev) + return "operator" + } else { + return false + } + }, + + "/": function(stream, state) { + if (!stream.eat("*")) return false + state.tokenize = tokenNestedComment(1) + return state.tokenize(stream, state) + } + }, + modeProps: {closeBrackets: {pairs: '()[]{}""', triples: '"'}} + }); + + function tokenKotlinString(tripleString){ + return function (stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!tripleString && !escaped && stream.match('"') ) {end = true; break;} + if (tripleString && stream.match('"""')) {end = true; break;} + next = stream.next(); + if(!escaped && next == "$" && stream.match('{')) + stream.skipTo("}"); + escaped = !escaped && next == "\\" && !tripleString; + } + if (end || !tripleString) + state.tokenize = null; + return "string"; + } + } + + def("text/x-kotlin", { + name: "clike", + keywords: words( + /*keywords*/ + "package as typealias class interface this super val operator " + + "var fun for is in This throw return annotation " + + "break continue object if else while do try when !in !is as? " + + + /*soft keywords*/ + "file import where by get set abstract enum open inner override private public internal " + + "protected catch finally out final vararg reified dynamic companion constructor init " + + "sealed field property receiver param sparam lateinit data inline noinline tailrec " + + "external annotation crossinline const operator infix suspend actual expect setparam" + ), + types: words( + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray " + + "ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy " + + "LazyThreadSafetyMode LongArray Nothing ShortArray Unit" + ), + intendSwitch: false, + indentStatements: false, + multiLineStrings: true, + number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i, + blockKeywords: words("catch class do else finally for if where try while enum"), + defKeywords: words("class val var object interface fun"), + atoms: words("true false null this"), + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '*': function(_stream, state) { + return state.prevToken == '.' ? 'variable' : 'operator'; + }, + '"': function(stream, state) { + state.tokenize = tokenKotlinString(stream.match('""')); + return state.tokenize(stream, state); + }, + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenNestedComment(1); + return state.tokenize(stream, state) + }, + indent: function(state, ctx, textAfter, indentUnit) { + var firstChar = textAfter && textAfter.charAt(0); + if ((state.prevToken == "}" || state.prevToken == ")") && textAfter == "") + return state.indented; + if ((state.prevToken == "operator" && textAfter != "}" && state.context.type != "}") || + state.prevToken == "variable" && firstChar == "." || + (state.prevToken == "}" || state.prevToken == ")") && firstChar == ".") + return indentUnit * 2 + ctx.indented; + if (ctx.align && ctx.type == "}") + return ctx.indented + (state.context.type == (textAfter || "").charAt(0) ? 0 : indentUnit); + } + }, + modeProps: {closeBrackets: {triples: '"'}} + }); + + def(["x-shader/x-vertex", "x-shader/x-fragment"], { + name: "clike", + keywords: words("sampler1D sampler2D sampler3D samplerCube " + + "sampler1DShadow sampler2DShadow " + + "const attribute uniform varying " + + "break continue discard return " + + "for while do if else struct " + + "in out inout"), + types: words("float int bool void " + + "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " + + "mat2 mat3 mat4"), + blockKeywords: words("for while do if else struct"), + builtin: words("radians degrees sin cos tan asin acos atan " + + "pow exp log exp2 sqrt inversesqrt " + + "abs sign floor ceil fract mod min max clamp mix step smoothstep " + + "length distance dot cross normalize ftransform faceforward " + + "reflect refract matrixCompMult " + + "lessThan lessThanEqual greaterThan greaterThanEqual " + + "equal notEqual any all not " + + "texture1D texture1DProj texture1DLod texture1DProjLod " + + "texture2D texture2DProj texture2DLod texture2DProjLod " + + "texture3D texture3DProj texture3DLod texture3DProjLod " + + "textureCube textureCubeLod " + + "shadow1D shadow2D shadow1DProj shadow2DProj " + + "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " + + "dFdx dFdy fwidth " + + "noise1 noise2 noise3 noise4"), + atoms: words("true false " + + "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " + + "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " + + "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " + + "gl_FogCoord gl_PointCoord " + + "gl_Position gl_PointSize gl_ClipVertex " + + "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " + + "gl_TexCoord gl_FogFragCoord " + + "gl_FragCoord gl_FrontFacing " + + "gl_FragData gl_FragDepth " + + "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " + + "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " + + "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " + + "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " + + "gl_ProjectionMatrixInverseTranspose " + + "gl_ModelViewProjectionMatrixInverseTranspose " + + "gl_TextureMatrixInverseTranspose " + + "gl_NormalScale gl_DepthRange gl_ClipPlane " + + "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " + + "gl_FrontLightModelProduct gl_BackLightModelProduct " + + "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " + + "gl_FogParameters " + + "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " + + "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " + + "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " + + "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " + + "gl_MaxDrawBuffers"), + indentSwitch: false, + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-nesc", { + name: "clike", + keywords: words(cKeywords + " as atomic async call command component components configuration event generic " + + "implementation includes interface module new norace nx_struct nx_union post provides " + + "signal task uses abstract extends"), + types: cTypes, + blockKeywords: words(cBlockKeywords), + atoms: words("null true false"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-objectivec", { + name: "clike", + keywords: words(cKeywords + " " + objCKeywords), + types: objCTypes, + builtin: words(objCBuiltins), + blockKeywords: words(cBlockKeywords + " @synthesize @try @catch @finally @autoreleasepool @synchronized"), + defKeywords: words(cDefKeywords + " @interface @implementation @protocol @class"), + dontIndentStatements: /^@.*$/, + typeFirstDefinitions: true, + atoms: words("YES NO NULL Nil nil true false nullptr"), + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + }, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-objectivec++", { + name: "clike", + keywords: words(cKeywords + " " + objCKeywords + " " + cppKeywords), + types: objCTypes, + builtin: words(objCBuiltins), + blockKeywords: words(cBlockKeywords + " @synthesize @try @catch @finally @autoreleasepool @synchronized class try catch"), + defKeywords: words(cDefKeywords + " @interface @implementation @protocol @class class namespace"), + dontIndentStatements: /^@.*$|^template$/, + typeFirstDefinitions: true, + atoms: words("YES NO NULL Nil nil true false nullptr"), + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + "u": cpp11StringHook, + "U": cpp11StringHook, + "L": cpp11StringHook, + "R": cpp11StringHook, + "0": cpp14Literal, + "1": cpp14Literal, + "2": cpp14Literal, + "3": cpp14Literal, + "4": cpp14Literal, + "5": cpp14Literal, + "6": cpp14Literal, + "7": cpp14Literal, + "8": cpp14Literal, + "9": cpp14Literal, + token: function(stream, state, style) { + if (style == "variable" && stream.peek() == "(" && + (state.prevToken == ";" || state.prevToken == null || + state.prevToken == "}") && + cppLooksLikeConstructor(stream.current())) + return "def"; + } + }, + namespaceSeparator: "::", + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-squirrel", { + name: "clike", + keywords: words("base break clone continue const default delete enum extends function in class" + + " foreach local resume return this throw typeof yield constructor instanceof static"), + types: cTypes, + blockKeywords: words("case catch class else for foreach if switch try while"), + defKeywords: words("function local class"), + typeFirstDefinitions: true, + atoms: words("true false null"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + // Ceylon Strings need to deal with interpolation + var stringTokenizer = null; + function tokenCeylonString(type) { + return function(stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!escaped && stream.match('"') && + (type == "single" || stream.match('""'))) { + end = true; + break; + } + if (!escaped && stream.match('``')) { + stringTokenizer = tokenCeylonString(type); + end = true; + break; + } + next = stream.next(); + escaped = type == "single" && !escaped && next == "\\"; + } + if (end) + state.tokenize = null; + return "string"; + } + } + + def("text/x-ceylon", { + name: "clike", + keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" + + " exists extends finally for function given if import in interface is let module new" + + " nonempty object of out outer package return satisfies super switch then this throw" + + " try value void while"), + types: function(word) { + // In Ceylon all identifiers that start with an uppercase are types + var first = word.charAt(0); + return (first === first.toUpperCase() && first !== first.toLowerCase()); + }, + blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"), + defKeywords: words("class dynamic function interface module object package value"), + builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" + + " native optional sealed see serializable shared suppressWarnings tagged throws variable"), + isPunctuationChar: /[\[\]{}\(\),;\:\.`]/, + isOperatorChar: /[+\-*&%=<>!?|^~:\/]/, + numberStart: /[\d#$]/, + number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i, + multiLineStrings: true, + typeFirstDefinitions: true, + atoms: words("true false null larger smaller equal empty finished"), + indentSwitch: false, + styleDefs: false, + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single"); + return state.tokenize(stream, state); + }, + '`': function(stream, state) { + if (!stringTokenizer || !stream.match('`')) return false; + state.tokenize = stringTokenizer; + stringTokenizer = null; + return state.tokenize(stream, state); + }, + "'": function(stream) { + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + return "atom"; + }, + token: function(_stream, state, style) { + if ((style == "variable" || style == "type") && + state.prevToken == ".") { + return "variable-2"; + } + } + }, + modeProps: { + fold: ["brace", "import"], + closeBrackets: {triples: '"'} + } + }); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/javascript/javascript.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/javascript/javascript.js new file mode 100644 index 0000000..e85077a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/javascript/javascript.js @@ -0,0 +1,959 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var trackScope = parserConfig.trackScope !== false + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + return { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, + "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, + "await": C + }; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^@]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (expressionAllowed(stream, state, 1)) { + readRegexp(stream); + stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eat("="); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#" && stream.peek() == "!") { + stream.skipToEnd(); + return ret("meta", "meta"); + } else if (ch == "#" && stream.eatWhile(wordRE)) { + return ret("variable", "property") + } else if (ch == "<" && stream.match("!--") || + (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { + stream.skipToEnd() + return ret("comment", "comment") + } else if (isOperatorChar.test(ch)) { + if (ch != ">" || !state.lexical || state.lexical.type != ">") { + if (stream.eat("=")) { + if (ch == "!" || ch == "=") stream.eat("=") + } else if (/[<>*+\-|&?]/.test(ch)) { + stream.eat(ch) + if (ch == ">") stream.eat(ch) + } + } + if (ch == "?" && stream.eat(".")) return ret(".") + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current() + if (state.lastType != ".") { + if (keywords.propertyIsEnumerable(word)) { + var kw = keywords[word] + return ret(kw.type, kw.style, word) + } + if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false)) + return ret("async", "keyword", word) + } + return ret("variable", "variable", word) + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + if (isTS) { // Try to skip TypeScript return type declarations after the arguments + var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) + if (m) arrow = m.index + } + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) { if (ch == "(") sawSomething = true; break; } + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/`]/.test(ch)) { + for (;; --pos) { + if (pos == 0) return + var next = stream.string.charAt(pos - 1) + if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } + } + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, + "regexp": true, "this": true, "import": true, "jsonld-keyword": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + if (!trackScope) return false + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function inList(name, list) { + for (var v = list; v; v = v.next) if (v.name == name) return true + return false; + } + function register(varname) { + var state = cx.state; + cx.marked = "def"; + if (!trackScope) return + if (state.context) { + if (state.lexical.info == "var" && state.context && state.context.block) { + // FIXME function decls are also not block scoped + var newContext = registerVarScoped(varname, state.context) + if (newContext != null) { + state.context = newContext + return + } + } else if (!inList(varname, state.localVars)) { + state.localVars = new Var(varname, state.localVars) + return + } + } + // Fall through means this is global + if (parserConfig.globalVars && !inList(varname, state.globalVars)) + state.globalVars = new Var(varname, state.globalVars) + } + function registerVarScoped(varname, context) { + if (!context) { + return null + } else if (context.block) { + var inner = registerVarScoped(varname, context.prev) + if (!inner) return null + if (inner == context.prev) return context + return new Context(inner, context.vars, true) + } else if (inList(varname, context.vars)) { + return context + } else { + return new Context(context.prev, new Var(varname, context.vars), false) + } + } + + function isModifier(name) { + return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" + } + + // Combinators + + function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } + function Var(name, next) { this.name = name; this.next = next } + + var defaultVars = new Var("this", new Var("arguments", null)) + function pushcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, false) + cx.state.localVars = defaultVars + } + function pushblockcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, true) + cx.state.localVars = null + } + function popcontext() { + cx.state.localVars = cx.state.context.vars + cx.state.context = cx.state.context.prev + } + popcontext.lex = true + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + function exp(type) { + if (type == wanted) return cont(); + else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); + else return cont(exp); + }; + return exp; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); + if (type == "debugger") return cont(expect(";")); + if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); + if (type == ";") return cont(); + if (type == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); + } + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex); + if (type == "class" || (isTS && value == "interface")) { + cx.marked = "keyword" + return cont(pushlex("form", type == "class" ? type : value), className, poplex) + } + if (type == "variable") { + if (isTS && value == "declare") { + cx.marked = "keyword" + return cont(statement) + } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { + cx.marked = "keyword" + if (value == "enum") return cont(enumdef); + else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); + else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) + } else if (isTS && value == "namespace") { + cx.marked = "keyword" + return cont(pushlex("form"), expression, statement, poplex) + } else if (isTS && value == "abstract") { + cx.marked = "keyword" + return cont(statement) + } else { + return cont(pushlex("stat"), maybelabel); + } + } + if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, + block, poplex, poplex, popcontext); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); + if (type == "export") return cont(pushlex("stat"), afterExport, poplex); + if (type == "import") return cont(pushlex("stat"), afterImport, poplex); + if (type == "async") return cont(statement) + if (value == "@") return cont(expression, statement) + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function maybeCatchBinding(type) { + if (type == "(") return cont(funarg, expect(")")) + } + function expression(type, value) { + return expressionInner(type, value, false); + } + function expressionNoComma(type, value) { + return expressionInner(type, value, true); + } + function parenExpr(type) { + if (type != "(") return pass() + return cont(pushlex(")"), maybeexpression, expect(")"), poplex) + } + function expressionInner(type, value, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef, maybeop); + if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } + if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); + if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + if (type == "quasi") return pass(quasi, maybeop); + if (type == "new") return cont(maybeTarget(noComma)); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(maybeexpression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); + if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { return pass(quasi, me); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } + if (type == "regexp") { + cx.state.lastType = cx.marked = "operator" + cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) + return cont(expr) + } + } + function quasi(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasi); + return cont(maybeexpression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expressionNoComma); + } + function maybeTarget(noComma) { + return function(type) { + if (type == ".") return cont(noComma ? targetNoComma : target); + else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) + else return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } + } + function targetNoComma(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "async") { + cx.marked = "property"; + return cont(objprop); + } else if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params + if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) + cx.state.fatArrowAt = cx.stream.pos + m[0].length + return cont(afterprop); + } else if (type == "number" || type == "string") { + cx.marked = jsonldMode ? "property" : (cx.style + " property"); + return cont(afterprop); + } else if (type == "jsonld-keyword") { + return cont(afterprop); + } else if (isTS && isModifier(value)) { + cx.marked = "keyword" + return cont(objprop) + } else if (type == "[") { + return cont(expression, maybetype, expect("]"), afterprop); + } else if (type == "spread") { + return cont(expressionNoComma, afterprop); + } else if (value == "*") { + cx.marked = "keyword"; + return cont(objprop); + } else if (type == ":") { + return pass(afterprop) + } + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end, sep) { + function proceed(type, value) { + if (sep ? sep.indexOf(type) > -1 : type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(function(type, value) { + if (type == end || value == end) return pass() + return pass(what) + }, proceed); + } + if (type == end || value == end) return cont(); + if (sep && sep.indexOf(";") > -1) return pass(what) + return cont(expect(end)); + } + return function(type, value) { + if (type == end || value == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type, value) { + if (isTS) { + if (type == ":") return cont(typeexpr); + if (value == "?") return cont(maybetype); + } + } + function maybetypeOrIn(type, value) { + if (isTS && (type == ":" || value == "in")) return cont(typeexpr) + } + function mayberettype(type) { + if (isTS && type == ":") { + if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) + else return cont(typeexpr) + } + } + function isKW(_, value) { + if (value == "is") { + cx.marked = "keyword" + return cont() + } + } + function typeexpr(type, value) { + if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") { + cx.marked = "keyword" + return cont(value == "typeof" ? expressionNoComma : typeexpr) + } + if (type == "variable" || value == "void") { + cx.marked = "type" + return cont(afterType) + } + if (value == "|" || value == "&") return cont(typeexpr) + if (type == "string" || type == "number" || type == "atom") return cont(afterType); + if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) + if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType) + if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) + if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) + if (type == "quasi") { return pass(quasiType, afterType); } + } + function maybeReturnType(type) { + if (type == "=>") return cont(typeexpr) + } + function typeprops(type) { + if (type.match(/[\}\)\]]/)) return cont() + if (type == "," || type == ";") return cont(typeprops) + return pass(typeprop, typeprops) + } + function typeprop(type, value) { + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property" + return cont(typeprop) + } else if (value == "?" || type == "number" || type == "string") { + return cont(typeprop) + } else if (type == ":") { + return cont(typeexpr) + } else if (type == "[") { + return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) + } else if (type == "(") { + return pass(functiondecl, typeprop) + } else if (!type.match(/[;\}\)\],]/)) { + return cont() + } + } + function quasiType(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasiType); + return cont(typeexpr, continueQuasiType); + } + function continueQuasiType(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasiType); + } + } + function typearg(type, value) { + if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) + if (type == ":") return cont(typeexpr) + if (type == "spread") return cont(typearg) + return pass(typeexpr) + } + function afterType(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + if (value == "|" || type == "." || value == "&") return cont(typeexpr) + if (type == "[") return cont(typeexpr, expect("]"), afterType) + if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } + if (value == "?") return cont(typeexpr, expect(":"), typeexpr) + } + function maybeTypeArgs(_, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + } + function typeparam() { + return pass(typeexpr, maybeTypeDefault) + } + function maybeTypeDefault(_, value) { + if (value == "=") return cont(typeexpr) + } + function vardef(_, value) { + if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } + if (type == "variable") { register(value); return cont(); } + if (type == "spread") return cont(pattern); + if (type == "[") return contCommasep(eltpattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + if (type == "spread") return cont(pattern); + if (type == "}") return pass(); + if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); + return cont(expect(":"), pattern, maybeAssign); + } + function eltpattern() { + return pass(pattern, maybeAssign) + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type, value) { + if (value == "await") return cont(forspec); + if (type == "(") return cont(pushlex(")"), forspec1, poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, forspec2); + if (type == "variable") return cont(forspec2); + return pass(forspec2) + } + function forspec2(type, value) { + if (type == ")") return cont() + if (type == ";") return cont(forspec2) + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } + return pass(expression, forspec2) + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) + } + function functiondecl(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} + if (type == "variable") {register(value); return cont(functiondecl);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) + } + function typename(type, value) { + if (type == "keyword" || type == "variable") { + cx.marked = "type" + return cont(typename) + } else if (value == "<") { + return cont(pushlex(">"), commasep(typeparam, ">"), poplex) + } + } + function funarg(type, value) { + if (value == "@") cont(expression, funarg) + if (type == "spread") return cont(funarg); + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } + if (isTS && type == "this") return cont(maybetype, maybeAssign) + return pass(pattern, maybetype, maybeAssign); + } + function classExpression(type, value) { + // Class expressions may have an optional name. + if (type == "variable") return className(type, value); + return classNameAfter(type, value); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) + if (value == "extends" || value == "implements" || (isTS && type == ",")) { + if (value == "implements") cx.marked = "keyword"; + return cont(isTS ? typeexpr : expression, classNameAfter); + } + if (type == "{") return cont(pushlex("}"), classBody, poplex); + } + function classBody(type, value) { + if (type == "async" || + (type == "variable" && + (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && + cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(classfield, classBody); + } + if (type == "number" || type == "string") return cont(classfield, classBody); + if (type == "[") + return cont(expression, maybetype, expect("]"), classfield, classBody) + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (isTS && type == "(") return pass(functiondecl, classBody) + if (type == ";" || type == ",") return cont(classBody); + if (type == "}") return cont(); + if (value == "@") return cont(expression, classBody) + } + function classfield(type, value) { + if (value == "!") return cont(classfield) + if (value == "?") return cont(classfield) + if (type == ":") return cont(typeexpr, maybeAssign) + if (value == "=") return cont(expressionNoComma) + var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" + return pass(isInterface ? functiondecl : functiondef) + } + function afterExport(type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); + return pass(statement); + } + function exportField(type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } + if (type == "variable") return pass(expressionNoComma, exportField); + } + function afterImport(type) { + if (type == "string") return cont(); + if (type == "(") return pass(expression); + if (type == ".") return pass(maybeoperatorComma); + return pass(importSpec, maybeMoreImports, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + if (value == "*") cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeMoreImports(type) { + if (type == ",") return cont(importSpec, maybeMoreImports) + } + function maybeAs(_type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(commasep(expressionNoComma, "]")); + } + function enumdef() { + return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) + } + function enummember() { + return pass(pattern, maybeAssign); + } + + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || + isOperatorChar.test(textAfter.charAt(0)) || + /[,.]/.test(textAfter.charAt(0)); + } + + function expressionAllowed(stream, state, backUp) { + return state.tokenize == tokenBase && + /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || + (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && new Context(null, null, false), + indented: basecolumn || 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top + // Kludge to prevent 'maybelse' from blocking lexical scope pops + if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse && c != popcontext) break; + } + while ((lexical.type == "stat" || lexical.type == "form") && + (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && + (top == maybeoperatorComma || top == maybeoperatorNoComma) && + !/^[,\.=+\-*:?[\(]/.test(textAfter)))) + lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + blockCommentContinue: jsonMode ? null : " * ", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + + helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, + jsonMode: jsonMode, + + expressionAllowed: expressionAllowed, + + skipExpression: function(state) { + parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null)) + } + }; +}); + +CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/x-javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", { name: "javascript", json: true }); +CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true }); +CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true }) +CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true }); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/simple/simple.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/simple/simple.js new file mode 100644 index 0000000..a4bdf69 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/simple/simple.js @@ -0,0 +1,215 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineSimpleMode = function(name, states) { + CodeMirror.defineMode(name, function(config) { + return CodeMirror.simpleMode(config, states); + }); + }; + + CodeMirror.simpleMode = function(config, states) { + ensureState(states, "start"); + var states_ = {}, meta = states.meta || {}, hasIndentation = false; + for (var state in states) if (state != meta && states.hasOwnProperty(state)) { + var list = states_[state] = [], orig = states[state]; + for (var i = 0; i < orig.length; i++) { + var data = orig[i]; + list.push(new Rule(data, states)); + if (data.indent || data.dedent) hasIndentation = true; + } + } + var mode = { + startState: function() { + return {state: "start", pending: null, + local: null, localState: null, + indent: hasIndentation ? [] : null}; + }, + copyState: function(state) { + var s = {state: state.state, pending: state.pending, + local: state.local, localState: null, + indent: state.indent && state.indent.slice(0)}; + if (state.localState) + s.localState = CodeMirror.copyState(state.local.mode, state.localState); + if (state.stack) + s.stack = state.stack.slice(0); + for (var pers = state.persistentStates; pers; pers = pers.next) + s.persistentStates = {mode: pers.mode, + spec: pers.spec, + state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state), + next: s.persistentStates}; + return s; + }, + token: tokenFunction(states_, config), + innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; }, + indent: indentFunction(states_, meta) + }; + if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop)) + mode[prop] = meta[prop]; + return mode; + }; + + function ensureState(states, name) { + if (!states.hasOwnProperty(name)) + throw new Error("Undefined state " + name + " in simple mode"); + } + + function toRegex(val, caret) { + if (!val) return /(?:)/; + var flags = ""; + if (val instanceof RegExp) { + if (val.ignoreCase) flags = "i"; + val = val.source; + } else { + val = String(val); + } + return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags); + } + + function asToken(val) { + if (!val) return null; + if (val.apply) return val + if (typeof val == "string") return val.replace(/\./g, " "); + var result = []; + for (var i = 0; i < val.length; i++) + result.push(val[i] && val[i].replace(/\./g, " ")); + return result; + } + + function Rule(data, states) { + if (data.next || data.push) ensureState(states, data.next || data.push); + this.regex = toRegex(data.regex); + this.token = asToken(data.token); + this.data = data; + } + + function tokenFunction(states, config) { + return function(stream, state) { + if (state.pending) { + var pend = state.pending.shift(); + if (state.pending.length == 0) state.pending = null; + stream.pos += pend.text.length; + return pend.token; + } + + if (state.local) { + if (state.local.end && stream.match(state.local.end)) { + var tok = state.local.endToken || null; + state.local = state.localState = null; + return tok; + } else { + var tok = state.local.mode.token(stream, state.localState), m; + if (state.local.endScan && (m = state.local.endScan.exec(stream.current()))) + stream.pos = stream.start + m.index; + return tok; + } + } + + var curState = states[state.state]; + for (var i = 0; i < curState.length; i++) { + var rule = curState[i]; + var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); + if (matches) { + if (rule.data.next) { + state.state = rule.data.next; + } else if (rule.data.push) { + (state.stack || (state.stack = [])).push(state.state); + state.state = rule.data.push; + } else if (rule.data.pop && state.stack && state.stack.length) { + state.state = state.stack.pop(); + } + + if (rule.data.mode) + enterLocalMode(config, state, rule.data.mode, rule.token); + if (rule.data.indent) + state.indent.push(stream.indentation() + config.indentUnit); + if (rule.data.dedent) + state.indent.pop(); + var token = rule.token + if (token && token.apply) token = token(matches) + if (matches.length > 2 && rule.token && typeof rule.token != "string") { + for (var j = 2; j < matches.length; j++) + if (matches[j]) + (state.pending || (state.pending = [])).push({text: matches[j], token: rule.token[j - 1]}); + stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); + return token[0]; + } else if (token && token.join) { + return token[0]; + } else { + return token; + } + } + } + stream.next(); + return null; + }; + } + + function cmp(a, b) { + if (a === b) return true; + if (!a || typeof a != "object" || !b || typeof b != "object") return false; + var props = 0; + for (var prop in a) if (a.hasOwnProperty(prop)) { + if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false; + props++; + } + for (var prop in b) if (b.hasOwnProperty(prop)) props--; + return props == 0; + } + + function enterLocalMode(config, state, spec, token) { + var pers; + if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next) + if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p; + var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec); + var lState = pers ? pers.state : CodeMirror.startState(mode); + if (spec.persistent && !pers) + state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates}; + + state.localState = lState; + state.local = {mode: mode, + end: spec.end && toRegex(spec.end), + endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false), + endToken: token && token.join ? token[token.length - 1] : token}; + } + + function indexOf(val, arr) { + for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true; + } + + function indentFunction(states, meta) { + return function(state, textAfter, line) { + if (state.local && state.local.mode.indent) + return state.local.mode.indent(state.localState, textAfter, line); + if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1) + return CodeMirror.Pass; + + var pos = state.indent.length - 1, rules = states[state.state]; + scan: for (;;) { + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { + var m = rule.regex.exec(textAfter); + if (m && m[0]) { + pos--; + if (rule.next || rule.push) rules = states[rule.next || rule.push]; + textAfter = textAfter.slice(m[0].length); + continue scan; + } + } + } + break; + } + return pos < 0 ? 0 : state.indent[pos]; + }; + } +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/vb/vb.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/vb/vb.js new file mode 100644 index 0000000..a387560 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/vb/vb.js @@ -0,0 +1,275 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("vb", function(conf, parserConf) { + var ERRORCLASS = 'error'; + + function wordRegexp(words) { + return new RegExp("^((" + words.join(")|(") + "))\\b", "i"); + } + + var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]"); + var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]'); + var doubleOperators = new RegExp("^((==)|(<>)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))"); + var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); + var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); + var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); + + var openingKeywords = ['class','module', 'sub','enum','select','while','if','function', 'get','set','property', 'try', 'structure', 'synclock', 'using', 'with']; + var middleKeywords = ['else','elseif','case', 'catch', 'finally']; + var endKeywords = ['next','loop']; + + var operatorKeywords = ['and', "andalso", 'or', 'orelse', 'xor', 'in', 'not', 'is', 'isnot', 'like']; + var wordOperators = wordRegexp(operatorKeywords); + + var commonKeywords = ["#const", "#else", "#elseif", "#end", "#if", "#region", "addhandler", "addressof", "alias", "as", "byref", "byval", "cbool", "cbyte", "cchar", "cdate", "cdbl", "cdec", "cint", "clng", "cobj", "compare", "const", "continue", "csbyte", "cshort", "csng", "cstr", "cuint", "culng", "cushort", "declare", "default", "delegate", "dim", "directcast", "each", "erase", "error", "event", "exit", "explicit", "false", "for", "friend", "gettype", "goto", "handles", "implements", "imports", "infer", "inherits", "interface", "isfalse", "istrue", "lib", "me", "mod", "mustinherit", "mustoverride", "my", "mybase", "myclass", "namespace", "narrowing", "new", "nothing", "notinheritable", "notoverridable", "of", "off", "on", "operator", "option", "optional", "out", "overloads", "overridable", "overrides", "paramarray", "partial", "private", "protected", "public", "raiseevent", "readonly", "redim", "removehandler", "resume", "return", "shadows", "shared", "static", "step", "stop", "strict", "then", "throw", "to", "true", "trycast", "typeof", "until", "until", "when", "widening", "withevents", "writeonly"]; + + var commontypes = ['object', 'boolean', 'char', 'string', 'byte', 'sbyte', 'short', 'ushort', 'int16', 'uint16', 'integer', 'uinteger', 'int32', 'uint32', 'long', 'ulong', 'int64', 'uint64', 'decimal', 'single', 'double', 'float', 'date', 'datetime', 'intptr', 'uintptr']; + + var keywords = wordRegexp(commonKeywords); + var types = wordRegexp(commontypes); + var stringPrefixes = '"'; + + var opening = wordRegexp(openingKeywords); + var middle = wordRegexp(middleKeywords); + var closing = wordRegexp(endKeywords); + var doubleClosing = wordRegexp(['end']); + var doOpening = wordRegexp(['do']); + + var indentInfo = null; + + CodeMirror.registerHelper("hintWords", "vb", openingKeywords.concat(middleKeywords).concat(endKeywords) + .concat(operatorKeywords).concat(commonKeywords).concat(commontypes)); + + function indent(_stream, state) { + state.currentIndent++; + } + + function dedent(_stream, state) { + state.currentIndent--; + } + // tokenizers + function tokenBase(stream, state) { + if (stream.eatSpace()) { + return null; + } + + var ch = stream.peek(); + + // Handle Comments + if (ch === "'") { + stream.skipToEnd(); + return 'comment'; + } + + + // Handle Number Literals + if (stream.match(/^((&H)|(&O))?[0-9\.a-f]/i, false)) { + var floatLiteral = false; + // Floats + if (stream.match(/^\d*\.\d+F?/i)) { floatLiteral = true; } + else if (stream.match(/^\d+\.\d*F?/)) { floatLiteral = true; } + else if (stream.match(/^\.\d+F?/)) { floatLiteral = true; } + + if (floatLiteral) { + // Float literals may be "imaginary" + stream.eat(/J/i); + return 'number'; + } + // Integers + var intLiteral = false; + // Hex + if (stream.match(/^&H[0-9a-f]+/i)) { intLiteral = true; } + // Octal + else if (stream.match(/^&O[0-7]+/i)) { intLiteral = true; } + // Decimal + else if (stream.match(/^[1-9]\d*F?/)) { + // Decimal literals may be "imaginary" + stream.eat(/J/i); + // TODO - Can you have imaginary longs? + intLiteral = true; + } + // Zero by itself with no other piece of number. + else if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; } + if (intLiteral) { + // Integer literals may be "long" + stream.eat(/L/i); + return 'number'; + } + } + + // Handle Strings + if (stream.match(stringPrefixes)) { + state.tokenize = tokenStringFactory(stream.current()); + return state.tokenize(stream, state); + } + + // Handle operators and Delimiters + if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) { + return null; + } + if (stream.match(doubleOperators) + || stream.match(singleOperators) + || stream.match(wordOperators)) { + return 'operator'; + } + if (stream.match(singleDelimiters)) { + return null; + } + if (stream.match(doOpening)) { + indent(stream,state); + state.doInCurrentLine = true; + return 'keyword'; + } + if (stream.match(opening)) { + if (! state.doInCurrentLine) + indent(stream,state); + else + state.doInCurrentLine = false; + return 'keyword'; + } + if (stream.match(middle)) { + return 'keyword'; + } + + if (stream.match(doubleClosing)) { + dedent(stream,state); + dedent(stream,state); + return 'keyword'; + } + if (stream.match(closing)) { + dedent(stream,state); + return 'keyword'; + } + + if (stream.match(types)) { + return 'keyword'; + } + + if (stream.match(keywords)) { + return 'keyword'; + } + + if (stream.match(identifiers)) { + return 'variable'; + } + + // Handle non-detected items + stream.next(); + return ERRORCLASS; + } + + function tokenStringFactory(delimiter) { + var singleline = delimiter.length == 1; + var OUTCLASS = 'string'; + + return function(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"]/); + if (stream.match(delimiter)) { + state.tokenize = tokenBase; + return OUTCLASS; + } else { + stream.eat(/['"]/); + } + } + if (singleline) { + if (parserConf.singleLineStringErrors) { + return ERRORCLASS; + } else { + state.tokenize = tokenBase; + } + } + return OUTCLASS; + }; + } + + + function tokenLexer(stream, state) { + var style = state.tokenize(stream, state); + var current = stream.current(); + + // Handle '.' connected identifiers + if (current === '.') { + style = state.tokenize(stream, state); + if (style === 'variable') { + return 'variable'; + } else { + return ERRORCLASS; + } + } + + + var delimiter_index = '[({'.indexOf(current); + if (delimiter_index !== -1) { + indent(stream, state ); + } + if (indentInfo === 'dedent') { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + delimiter_index = '])}'.indexOf(current); + if (delimiter_index !== -1) { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + + return style; + } + + var external = { + electricChars:"dDpPtTfFeE ", + startState: function() { + return { + tokenize: tokenBase, + lastToken: null, + currentIndent: 0, + nextLineIndent: 0, + doInCurrentLine: false + + + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + state.currentIndent += state.nextLineIndent; + state.nextLineIndent = 0; + state.doInCurrentLine = 0; + } + var style = tokenLexer(stream, state); + + state.lastToken = {style:style, content: stream.current()}; + + + + return style; + }, + + indent: function(state, textAfter) { + var trueText = textAfter.replace(/^\s+|\s+$/g, '') ; + if (trueText.match(closing) || trueText.match(doubleClosing) || trueText.match(middle)) return conf.indentUnit*(state.currentIndent-1); + if(state.currentIndent < 0) return 0; + return state.currentIndent * conf.indentUnit; + }, + + lineComment: "'" + }; + return external; +}); + +CodeMirror.defineMIME("text/x-vb", "vb"); + +}); \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/xml/xml.js b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/xml/xml.js new file mode 100644 index 0000000..46806ac --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/mode/xml/xml.js @@ -0,0 +1,413 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +var htmlConfig = { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true, 'menuitem': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true, + caseFold: true +} + +var xmlConfig = { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false, + allowMissingTagName: false, + caseFold: false +} + +CodeMirror.defineMode("xml", function(editorConf, config_) { + var indentUnit = editorConf.indentUnit + var config = {} + var defaults = config_.htmlMode ? htmlConfig : xmlConfig + for (var prop in defaults) config[prop] = defaults[prop] + for (var prop in config_) config[prop] = config_[prop] + + // Return variables for tokenizers + var type, setStyle; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + type = stream.eat("/") ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag bracket"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + inText.isInText = true; + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag bracket"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + state.state = baseState; + state.tagName = state.tagStart = null; + var next = state.tokenize(stream, state); + return next ? next + " tag error" : "tag error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + } + } + + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + function Context(state, tagName, startOfLine) { + this.prev = state.context; + this.tagName = tagName || ""; + this.indent = state.indented; + this.startOfLine = startOfLine; + if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + this.noIndent = true; + } + function popContext(state) { + if (state.context) state.context = state.context.prev; + } + function maybePopContext(state, nextTagName) { + var parentTagName; + while (true) { + if (!state.context) { + return; + } + parentTagName = state.context.tagName; + if (!config.contextGrabbers.hasOwnProperty(parentTagName) || + !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(state); + } + } + + function baseState(type, stream, state) { + if (type == "openTag") { + state.tagStart = stream.column(); + return tagNameState; + } else if (type == "closeTag") { + return closeTagNameState; + } else { + return baseState; + } + } + function tagNameState(type, stream, state) { + if (type == "word") { + state.tagName = stream.current(); + setStyle = "tag"; + return attrState; + } else if (config.allowMissingTagName && type == "endTag") { + setStyle = "tag bracket"; + return attrState(type, stream, state); + } else { + setStyle = "error"; + return tagNameState; + } + } + function closeTagNameState(type, stream, state) { + if (type == "word") { + var tagName = stream.current(); + if (state.context && state.context.tagName != tagName && + config.implicitlyClosed.hasOwnProperty(state.context.tagName)) + popContext(state); + if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) { + setStyle = "tag"; + return closeState; + } else { + setStyle = "tag error"; + return closeStateErr; + } + } else if (config.allowMissingTagName && type == "endTag") { + setStyle = "tag bracket"; + return closeState(type, stream, state); + } else { + setStyle = "error"; + return closeStateErr; + } + } + + function closeState(type, _stream, state) { + if (type != "endTag") { + setStyle = "error"; + return closeState; + } + popContext(state); + return baseState; + } + function closeStateErr(type, stream, state) { + setStyle = "error"; + return closeState(type, stream, state); + } + + function attrState(type, _stream, state) { + if (type == "word") { + setStyle = "attribute"; + return attrEqState; + } else if (type == "endTag" || type == "selfcloseTag") { + var tagName = state.tagName, tagStart = state.tagStart; + state.tagName = state.tagStart = null; + if (type == "selfcloseTag" || + config.autoSelfClosers.hasOwnProperty(tagName)) { + maybePopContext(state, tagName); + } else { + maybePopContext(state, tagName); + state.context = new Context(state, tagName, tagStart == state.indented); + } + return baseState; + } + setStyle = "error"; + return attrState; + } + function attrEqState(type, stream, state) { + if (type == "equals") return attrValueState; + if (!config.allowMissing) setStyle = "error"; + return attrState(type, stream, state); + } + function attrValueState(type, stream, state) { + if (type == "string") return attrContinuedState; + if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;} + setStyle = "error"; + return attrState(type, stream, state); + } + function attrContinuedState(type, stream, state) { + if (type == "string") return attrContinuedState; + return attrState(type, stream, state); + } + + return { + startState: function(baseIndent) { + var state = {tokenize: inText, + state: baseState, + indented: baseIndent || 0, + tagName: null, tagStart: null, + context: null} + if (baseIndent != null) state.baseIndent = baseIndent + return state + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) + state.indented = stream.indentation(); + + if (stream.eatSpace()) return null; + type = null; + var style = state.tokenize(stream, state); + if ((style || type) && style != "comment") { + setStyle = null; + state.state = state.state(type || style, stream, state); + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + } + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + if (state.tagStart == state.indented) + return state.stringStartCol + 1; + else + return state.indented + indentUnit; + } + if (context && context.noIndent) return CodeMirror.Pass; + if (state.tokenize != inTag && state.tokenize != inText) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (config.multilineTagIndentPastTag !== false) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1); + } + if (config.alignCDATA && /$/, + blockCommentStart: "", + + configuration: config.htmlMode ? "html" : "xml", + helperType: config.htmlMode ? "html" : "xml", + + skipAttribute: function(state) { + if (state.state == attrValueState) + state.state = attrState + }, + + xmlCurrentTag: function(state) { + return state.tagName ? {name: state.tagName, close: state.type == "closeTag"} : null + }, + + xmlCurrentContext: function(state) { + var context = [] + for (var cx = state.context; cx; cx = cx.prev) + context.push(cx.tagName) + return context.reverse() + } + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); + +}); diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/theme/lucario.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/theme/lucario.css new file mode 100644 index 0000000..1e0efcb --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/codemirror/5.59.1/theme/lucario.css @@ -0,0 +1,37 @@ +/* + Name: lucario + Author: Raphael Amorim + + Original Lucario color scheme (https://github.com/raphamorim/lucario) +*/ + +.cm-s-lucario.CodeMirror, .cm-s-lucario .CodeMirror-gutters { + background-color: #2b3e50 !important; + color: #f8f8f2 !important; + border: none; +} +.cm-s-lucario .CodeMirror-gutters { color: #2b3e50; } +.cm-s-lucario .CodeMirror-cursor { border-left: solid thin #E6C845; } +.cm-s-lucario .CodeMirror-linenumber { color: #f8f8f2; } +.cm-s-lucario .CodeMirror-selected { background: #000000; } +.cm-s-lucario .CodeMirror-line::selection, .cm-s-lucario .CodeMirror-line > span::selection, .cm-s-lucario .CodeMirror-line > span > span::selection { background: #243443; } +.cm-s-lucario .CodeMirror-line::-moz-selection, .cm-s-lucario .CodeMirror-line > span::-moz-selection, .cm-s-lucario .CodeMirror-line > span > span::-moz-selection { background: #243443; } +.cm-s-lucario span.cm-comment { color: #5c98cd; } +.cm-s-lucario span.cm-string, .cm-s-lucario span.cm-string-2 { color: #E6DB74; } +.cm-s-lucario span.cm-number { color: #ca94ff; } +.cm-s-lucario span.cm-variable { color: #f8f8f2; } +.cm-s-lucario span.cm-variable-2 { color: #f8f8f2; } +.cm-s-lucario span.cm-def { color: #72C05D; } +.cm-s-lucario span.cm-operator { color: #66D9EF; } +.cm-s-lucario span.cm-keyword { color: #ff6541; } +.cm-s-lucario span.cm-atom { color: #bd93f9; } +.cm-s-lucario span.cm-meta { color: #f8f8f2; } +.cm-s-lucario span.cm-tag { color: #ff6541; } +.cm-s-lucario span.cm-attribute { color: #66D9EF; } +.cm-s-lucario span.cm-qualifier { color: #72C05D; } +.cm-s-lucario span.cm-property { color: #f8f8f2; } +.cm-s-lucario span.cm-builtin { color: #72C05D; } +.cm-s-lucario span.cm-variable-3, .cm-s-lucario span.cm-type { color: #ffb86c; } + +.cm-s-lucario .CodeMirror-activeline-background { background: #243443; } +.cm-s-lucario .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/codemirror-hints.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/codemirror-hints.css new file mode 100644 index 0000000..5041016 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/codemirror-hints.css @@ -0,0 +1,146 @@ +/* +.workflow-params-converter-container .CodeMirror { + min-height: 400px; + max-height: calc(100vh - 280px) !important; + height: 100%; + font-size: 0.85rem; +} +*/ + +.wf-converter-container { + position: relative; +} + +.wf-converter-textarea-container { + align-items: stretch; + align-content: stretch; + flex-flow: row wrap; +} + +.wf-converter-codebox { + flex-grow: 1; + padding: 5px 5px; + overflow: hidden; +} + +.wf-converter-codebox-0 { + flex-basis: 0; + display: none; +} + +.wf-converter-codebox-50 { + flex-basis: 50%; +} + +.wf-converter-codebox-100 { + flex-basis: 100%; +} + +.wf-converter-codetextarea { + width: 100%; + min-height: 50em; +} + +.wf-converter-convertbutton { + flex-grow: 2; + padding: 0 5px; + display: flex; +} + +.wf-converter-veiewbutton-div { + flex-grow: 2; + width: 100%; + display: flex; +} + +.wf-converter-veiewbutton { + width: 90px; +} +.wf-converter-overlay { + position: absolute; + width: 100%; + height: 100%; + background-color: #3d516b; + top: 0; + opacity: 0.7; + z-index: 100; + display: flex; + justify-content: center; +} + +.wf-converter-syntax-error { + vertical-align: baseline; + color: red; +} + +.wf-converter-message { + vertical-align: baseline; + color: lightgrey; +} + + +.cm-s-lucario span.cm-comment { color: #999999; } +.cm-s-lucario span.cm-variable1 { color: #caddca; } +.cm-s-lucario span.cm-variable2 { color: #7add7a; } +.cm-s-lucario span.cm-variable3 { color: #1aff1a; } +.cm-s-lucario span.cm-argument { color: #3399ff; } +.cm-s-lucario span.cm-type { color: #9D9D9D; } + +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + + background-color: #202f3d; + background-clip: padding-box; + border: 0.0625rem solid #00acff; + border-radius: 0.1875rem; + box-shadow: 0 0.1875rem 1.125rem rgba(0 0 0 0.75); + outline: 0; + + background: #2e4057; + font-family: monospace; + font-size: 8pt; + + max-height: 20em; + overflow-y: auto; + + width: auto; + max-width: 80%; +} + +.CodeMirror-hint { + margin-left: 4px; + border-radius: 2px; + color: white; + cursor: pointer; +} + +li.CodeMirror-hint-active { + margin-left: 4px; + background: #1966b5; + color: white; +} + +.cm-hint-outer { +} + +.cm-hint-name { + margin-left: 4px; + color: white; +} + +.cm-hint-type { + margin-left: 4px; + color: #45cbfd; + overflow-wrap: break-word; +} + +.cm-hint-desc { + margin-left: 4px; + color: yellow; + overflow-wrap: break-word; +} diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/font-awesome.min.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/font-awesome.min.css new file mode 100644 index 0000000..656a507 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/font-awesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/FONT-LICENSE b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 0000000..a1dc03f --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/ICON-LICENSE b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/README.md b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/README.md new file mode 100644 index 0000000..e34bd86 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](https://github.com/iconic/open-iconic) +=========== + +### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 0000000..4664f2e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 0000000..f98177d Binary files /dev/null and b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.eot differ diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.otf new file mode 100644 index 0000000..f6bd684 Binary files /dev/null and b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.otf differ diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.svg new file mode 100644 index 0000000..32b2c4e --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 0000000..fab6048 Binary files /dev/null and b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf differ diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 0000000..f930998 Binary files /dev/null and b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.woff differ diff --git a/Rms.Risk.Mango.Pivot.UI/wwwroot/css/pivot.css b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/pivot.css new file mode 100644 index 0000000..f9f7cc0 --- /dev/null +++ b/Rms.Risk.Mango.Pivot.UI/wwwroot/css/pivot.css @@ -0,0 +1,127 @@ +.form-mw { + min-width: 150pt; +} + +.t-zero { + color: #586d85; + text-align: right; + cursor: pointer; +} +.t-neg { + color: red; + text-align: right; + cursor: pointer; +} +.t-pos { + color: lightgreen; + text-align: right; + cursor: pointer; +} + +.tiny-separator { + border-left: #2f7ee8; + border-left-width: 1px; + border-left-style: solid; +} + +.tiny-button { + width: 16pt !important; + height: 16pt !important; + padding: 0; + border-left: none; +} + +.hidden { + display: none; +} + +.filter-list { + min-width: 350px; + padding-left: 0; + padding-right: 0; +} + +@media only screen and (max-width: 500px) { + .tiny-separator { + display: none; + } + + .filter-list { + display: none; + } +} + +.last-updated { + padding-top: 5px; + font-size: smaller; + color: #586d85; + text-align: right; + } + +.pivot-navigator { + display: flex; + flex-direction: row; + flex-wrap: wrap +} + +.datepicker-control { + cursor: pointer; + padding: 5px 10px; + border: 1px solid #556880; + line-height: 14px; + border-radius: 2px; + padding-top: 3px !important; +} + +.table-forge btn { + color: #FFFFFF; + font-weight: 600; + font-size: 0.875rem; +} + +.table-forge td:hover { + border-left-color: #2f7ee8 !important; + border-left-width: 1px !important; + border-left-style: solid !important; +} + +.table-forge th { + height: 1px; +} + +.table-forge td { + padding: 1px; + vertical-align: middle; + width: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-left-width: 1px !important; + border-left-style: solid !important; + border-left-color: transparent; +} + +.table-forge-top-align td { + vertical-align: top !important; +} + +.table-forge th:hover { + background-color: #213e69; +} + +.table-forge-striped tr:nth-child(even) { + background-color: #1f2d3f; +} + +.table-forge td .tc-row-number { + width: 1%; + white-space: nowrap; +} + +.table-nowrap{ + white-space: nowrap; +} + +.table-forge-hover tbody tr:hover { + background-color: #213e69 !important; +} diff --git a/Rms.Risk.Mango/App.razor b/Rms.Risk.Mango/App.razor new file mode 100644 index 0000000..233a973 --- /dev/null +++ b/Rms.Risk.Mango/App.razor @@ -0,0 +1,46 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + + + + + +
Authentication in progress
+
+
+
+
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
+
diff --git a/Rms.Risk.Mango/Components/AggregationExample.razor b/Rms.Risk.Mango/Components/AggregationExample.razor new file mode 100644 index 0000000..96afc1a --- /dev/null +++ b/Rms.Risk.Mango/Components/AggregationExample.razor @@ -0,0 +1,78 @@ +@using System.Text.Json.Serialization.Metadata +@using Rms.Risk.Mango.Language +@using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + + + + + + + + + + + +
Aggregation for HumansMongoDB Aggregation
+ + + +
+ +@code { + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string Text { get; set; } = ""; + private string Json { get; set; } = ""; + + private static readonly JsonSerializerOptions _prettyPrint = new() + { + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + + protected override void OnParametersSet() + { + if (!string.IsNullOrWhiteSpace(Text)) + { + try + { + var ast = LanguageParser.ParseScriptToAST($"FROM \"\" PIPELINE {{ {Text} }}"); + Json = ast.AsJson()!.ToJsonString(_prettyPrint); + } + catch (Exception e) + { + Json = e.ToString(); + } + } + } + +} diff --git a/Rms.Risk.Mango/Components/AuthorizedOnly.razor b/Rms.Risk.Mango/Components/AuthorizedOnly.razor new file mode 100644 index 0000000..ad2404c --- /dev/null +++ b/Rms.Risk.Mango/Components/AuthorizedOnly.razor @@ -0,0 +1,86 @@ +@using Microsoft.Extensions.Options +@inject IUserSession UserSession +@inject IOptions Settings + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + @ChildContent + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User name:@UserSession.User.GetEmail()
Database config:@UserSession.Database
MongoDB database:@UserSession.DatabaseConfig.MongoDbDatabase
 
Read access group:@UserSession.LdapGroups.ReadOnly
Write access group:@UserSession.LdapGroups.ReadWrite
Admin access group:@UserSession.LdapGroups.Admin
+
+ @if (!string.IsNullOrWhiteSpace(Settings.Value.RequestAccessURL)) + { +
+

Group access can be requested via @Settings.Value.RequestAccessLabel

+
+ } +
+
+
+ +@code { + [Parameter] public string Policy { get; set; } = "Admin"; + [Parameter] public string Resource { get; set; } = ""; + + /// + /// The content that will be displayed if the user is authorized. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } +} diff --git a/Rms.Risk.Mango/Components/BouncingArrow.razor b/Rms.Risk.Mango/Components/BouncingArrow.razor new file mode 100644 index 0000000..399d7ab --- /dev/null +++ b/Rms.Risk.Mango/Components/BouncingArrow.razor @@ -0,0 +1,52 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + +
+ + + + + + +
+ +@code +{ + [Parameter] + public string Class { get; set; } = ""; +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdBalancerStatus.razor b/Rms.Risk.Mango/Components/Commands/CmdBalancerStatus.razor new file mode 100644 index 0000000..db77e12 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdBalancerStatus.razor @@ -0,0 +1,63 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Balancer status", "Admin")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""balancerStatus"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + Parameters.NeedConfirmation = false; + } + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdBase.razor b/Rms.Risk.Mango/Components/Commands/CmdBase.razor new file mode 100644 index 0000000..d6a56fb --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdBase.razor @@ -0,0 +1,230 @@ +@code { + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + [Inject] protected IUserSession UserSession { get; set; } = null!; + + public class CommandParams(string name, Action changed) + { + public string Name { get; } = name; + + public string CommandJson + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + changed(this); + } + } = ""; + + public List Result + { + get; + set + { + if (field == value) + return; + field = value; + ResultChanged(this); + } + } = []; + + public Action ResultChanged { get; set; } = _ => { }; + + public bool CanExecute + { + get; + set + { + if (field == value) + return; + field = value; + changed(this); + } + } + + public enum DatabaseConnectionType + { + Cluster, Admin, Shard + } + + public DatabaseConnectionType RequiredConnectionType + { + get; + set + { + if (field == value) + return; + field = value; + changed(this); + } + } + + public bool NeedConfirmation + { + get; + set + { + if (field == value) + return; + field = value; + changed(this); + } + } = true; + } + + // inout + [Parameter] + public string Collection + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(OnCollectionChangedInternal); + } + } = ""; + + [Parameter] public CommandParams? Parameters { + get; + set + { + if (field == value) + return; + field = value; + if (field != null) + InvokeAsync(OnParametersSetInternal); + } + } = null!; + [Parameter] public string Class { get; set; } = ""; + + protected string Result => Parameters?.Result.FirstOrDefault()?.ToJson(new() { Indent = true }) ?? ""; + + protected virtual string CommandTemplate { get; } = "{}"; + + protected void OnCommandChanged(string val) + { + if (Parameters == null) + return; + if (string.IsNullOrWhiteSpace(Collection)) + return; + + Parameters.CommandJson = val.Replace("[DATABASE]", UserSession.DatabaseConfig.MongoDbDatabase).Replace("[COLLECTION]", Collection); + Validate(); + //StateHasChanged(); + } + + private void OnParametersSetInternal() + { + if (Parameters != null) + { + Parameters.CommandJson = CommandTemplate.Replace("[DATABASE]", UserSession.DatabaseConfig.MongoDbDatabase).Replace("[COLLECTION]", Collection); + if (!string.IsNullOrWhiteSpace(Collection)) + Parameters.CommandJson = Parameters.CommandJson.Replace("[COLLECTION]", Collection); + + Validate(); + + Parameters.ResultChanged = _ => OnResultReceived(); + + StateHasChanged(); + } + } + + protected virtual void OnResultReceived() + { + InvokeAsync(StateHasChanged); + } + + private Task OnCollectionChangedInternal() + { + if (Parameters != null && !string.IsNullOrWhiteSpace(Collection)) + { + Parameters.CommandJson = CommandTemplate.Replace("[DATABASE]", UserSession.DatabaseConfig.MongoDbDatabase).Replace("[COLLECTION]", Collection); + Parameters.CanExecute = true; + } + Validate(); + return OnCollectionChanged(); + } + + protected virtual void Validate() + { + if (Parameters != null ) + Parameters.CanExecute = !string.IsNullOrWhiteSpace(Collection); + } + + protected virtual async Task OnCollectionChanged() + { + await InvokeAsync(StateHasChanged); + } + + protected static List> FormatDocument(BsonDocument doc, List<(string, string, bool)> descriptor) + => FormatDocument(doc.ToDictionary(), descriptor); + + protected static List> FormatDocument(Dictionary dict, List<(string, string, bool)> descriptor) + { + var res = new List>(); + + foreach (var (name, title, shouldFormat) in descriptor) + { + if (!dict.TryGetValue(name, out var o)) + continue; + + string v; + if (shouldFormat) + { + v = o switch + { + int when long.TryParse(o.ToString(), out var ll) => ToHumanReadable(ll), + long when long.TryParse(o.ToString(), out var l) => ToHumanReadable(l), + double when double.TryParse(o.ToString(), out var d) => $"{d:N2}", + _ => o.ToString() ?? "" + }; + } + else + v = o.ToString() ?? ""; + res.Add(new(title, v)); + } + + return res; + } + + public static string ToHumanReadable(long len) + { + string[] sizes = ["B", "KB", "MB", "GB", "TB"]; + var order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + + // Adjust the format string to your preferences. For example "{0:0.#}{1}" would + // show a single decimal place, and no space. + var result = $"{len:0.##} {sizes[order]}"; + return result; + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdBuildInfo.razor b/Rms.Risk.Mango/Components/Commands/CmdBuildInfo.razor new file mode 100644 index 0000000..041087b --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdBuildInfo.razor @@ -0,0 +1,62 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Build info", "Informational")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""buildInfo"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.NeedConfirmation = false; + } + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdCheckMetadata.razor b/Rms.Risk.Mango/Components/Commands/CmdCheckMetadata.razor new file mode 100644 index 0000000..c9821b1 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCheckMetadata.razor @@ -0,0 +1,62 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Check metadata", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""checkMetadataConsistency"" : ""[COLLECTION]"" +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.NeedConfirmation = false; + } + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdCloneAsCapped.razor b/Rms.Risk.Mango/Components/Commands/CmdCloneAsCapped.razor new file mode 100644 index 0000000..fd70514 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCloneAsCapped.razor @@ -0,0 +1,55 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Clone as capped", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "text", Name = "Dest collection name", Path = "$.toCollection", Icon ="icon-wrench-outline-sm" }, + new() { Type = "int", Name = "Capped size", Path = "$.size", Icon ="icon-wrench-outline-sm" }, + ] + }; + + protected override string CommandTemplate => @"{ + ""cloneCollectionAsCapped"": ""[COLLECTION]"", + ""toCollection"": """", + ""size"": 0 +}"; +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdCollStats.razor b/Rms.Risk.Mango/Components/Commands/CmdCollStats.razor new file mode 100644 index 0000000..9dae681 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCollStats.razor @@ -0,0 +1,60 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Stats", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""collStats"" : ""[COLLECTION]"", + ""scale"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + Parameters.NeedConfirmation = false; + } +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdCompact.razor b/Rms.Risk.Mango/Components/Commands/CmdCompact.razor new file mode 100644 index 0000000..0700c39 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCompact.razor @@ -0,0 +1,66 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Compact", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "bool", Name = "Dry run", Path = "$.dryRun", Icon ="icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Force", Path = "$.force", Icon ="icon-wrench-outline-sm" }, + new() { Type = "int", Name = "Free space target (MB)", Path = "$.freeSpaceTargetMB", Icon ="icon-alarm-clock-sm"}, + ] + }; + + protected override string CommandTemplate => @"{ + ""compact"" : ""[COLLECTION]"", + ""dryRun"" : ""True"", + ""force"" : ""False"", + ""freeSpaceTargetMB"" : ""20"" +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Shard; + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdCreateIndex.razor b/Rms.Risk.Mango/Components/Commands/CmdCreateIndex.razor new file mode 100644 index 0000000..aa28976 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCreateIndex.razor @@ -0,0 +1,100 @@ +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Create index", "Indexes")] + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ +
+
+ +
+
+ +
+
+ +@code { + private string IndexJson + { + get; + set + { + if (field == value) + return; + field = value; + UpdateCommand(); + } + } = "{}"; + + private bool IsValid + { + get; + set + { + if (field == value) + return; + field = value; + Validate(); + } + } + + private void UpdateCommand() + { + if (Parameters == null) + return; + + Parameters.CommandJson = JsonUtils.FormatJson($@"{{ + ""createIndexes"": ""{Collection}"", + ""indexes"": [ {IndexJson} ], + ""comment"" : ""Created by {UserSession.User.GetEmail()}"" +}}"); + Parameters.NeedConfirmation = true; + Validate(); + InvokeAsync(StateHasChanged); + } + + protected override void Validate() + { + if (Parameters != null && Parameters.CanExecute != IsValid) + { + Parameters.CanExecute = IsValid; + InvokeAsync(StateHasChanged); + } + + } + + protected override Task OnCollectionChanged() + { + IndexJson = "{}"; + UpdateCommand(); + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdCreateTimeseries.razor b/Rms.Risk.Mango/Components/Commands/CmdCreateTimeseries.razor new file mode 100644 index 0000000..dd928c5 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdCreateTimeseries.razor @@ -0,0 +1,68 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Create timeseries", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "bool", Name = "Capped", Path = "$.capped", Icon="icon-wrench-outline-sm" }, + new() { Type = "text", Name = "Time field", Path = "$.timeseries.timeField", Icon="icon-annotate-sm" }, + new() { Type = "text", Name = "Meta field", Path = "$.timeseries.metaField", Icon="icon-annotate-sm" }, + new() { Type = "-" }, + new() { Type = "text", Name = "Granularity", Path = "$.timeseries.granularity", Icon="icon-wrench-outline-sm" }, + new() { Type = "int", Name = "bucketMaxSpanSeconds", Path = "$.timeseries.bucketMaxSpanSeconds", Icon="icon-alarm-clock-sm" }, + new() { Type = "int", Name = "bucketRoundingSeconds", Path = "$.timeseries.bucketRoundingSeconds", Icon="icon-alarm-clock-sm"}, + ] + }; + + protected override string CommandTemplate => @"{ + ""create"": ""[COLLECTION]"", + ""capped"": false, + ""timeseries"": { + ""timeField"": """", + ""metaField"": """", + ""granularity"": """", + ""bucketMaxSpanSeconds"": 600, + ""bucketRoundingSeconds"": 1 + } +}"; + + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdDeleteDocuments.razor b/Rms.Risk.Mango/Components/Commands/CmdDeleteDocuments.razor new file mode 100644 index 0000000..9202343 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdDeleteDocuments.razor @@ -0,0 +1,60 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Write, "Delete document", "Documents")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "zeroone", Name = "One document", Path = "$.deletes[0].limit", Icon ="icon-wrench-outline-sm" }, + new() { Type = "-" }, + new() { Type = "json", Name = "Filter", Path = "$.deletes[0].q", Icon ="icon-wrench-outline-sm" }, + ] + }; + + protected override string CommandTemplate => @"{ + ""delete"": ""[COLLECTION]"", + ""deletes"": [{ + ""q"": { ""_id"" : ""XXX"" }, + ""limit"": 1 + }] +}"; + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdDropIndex.razor b/Rms.Risk.Mango/Components/Commands/CmdDropIndex.razor new file mode 100644 index 0000000..c8a22a9 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdDropIndex.razor @@ -0,0 +1,79 @@ +@using Rms.Risk.Mango.Components.JForms +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Drop index", "Indexes")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private List _indexes = []; + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "select", TypeArg=typeof(string).ToString(), Name = "Index name", Path = "$.index", Icon ="icon-wrench-outline-sm" }, + ] + }; + + protected override string CommandTemplate => @"{ + ""dropIndexes"": ""[COLLECTION]"", + ""index"": """", +}"; + + + protected override async Task OnCollectionChanged() + { + try + { + var indexes = await UserSession.MongoDbAdmin.GetIndexes(Collection); + _indexes = indexes.Indexes + .Where(x => x.Key.Count != 1 || !x.Key.ContainsKey("_id")) + .Select(x => x.Name) + .OrderBy(x => x) + .ToList(); + + await InvokeAsync(StateHasChanged); + } + catch (Exception) + { + _indexes = []; + } + } + + private object? GetArguments(TemplateRec.ParamRec arg) => _indexes; + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdHello.razor b/Rms.Risk.Mango/Components/Commands/CmdHello.razor new file mode 100644 index 0000000..0f653ef --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdHello.razor @@ -0,0 +1,47 @@ +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Hello", "Informational")] + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+ +
+
+ +
+
+ +@code { + + protected override string CommandTemplate => @"{ + ""hello"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender || Parameters == null) + return; + + Parameters.NeedConfirmation = false; + Validate(); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdInsertDocument.razor b/Rms.Risk.Mango/Components/Commands/CmdInsertDocument.razor new file mode 100644 index 0000000..3b3d41c --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdInsertDocument.razor @@ -0,0 +1,60 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Write, "Insert document", "Documents")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "json", Name = "Document to insert", Path = "$.documents[0]", Icon = "icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Ordered", Path = "$.ordered", Icon = "icon-wrench-outline-sm" } + ] + }; + + protected override string CommandTemplate => @"{ + ""insert"": ""[COLLECTION]"", + ""documents"": [ + { + ""_id"" : ""XXX"" + } + ], + ""ordered"": ""False"" +}"; + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdListDatabases.razor b/Rms.Risk.Mango/Components/Commands/CmdListDatabases.razor new file mode 100644 index 0000000..d32f964 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdListDatabases.razor @@ -0,0 +1,107 @@ +@using Newtonsoft.Json.Linq +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "List databases", "Informational")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ + + + + + +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""listDatabases"" : 1 +}"; + + private class ResultItem + { + public string Name { get; set; } = string.Empty; + public long Size { get; set; } + public bool IsEmpty { get; set; } + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + Parameters.NeedConfirmation = false; + } + } + + private IReadOnlyCollection GetParsedResult() + { + var list = new List(); + if (Result == null) + return list; + + var res = JsonUtils.FromJson(Result); + if (res == null) + return list; + + var databases = res["databases"]?.Value(); + if (databases == null) + return list; + + foreach (var doc in databases) + { + var name = doc["name"]?.Value() ?? "???"; + var size = doc["sizeOnDisk"]?.Value() ?? 0L; + var isEmpty = doc["empty"]?.Value() ?? true; + + list.Add(new() + { + Name = name, + Size = size, + IsEmpty = isEmpty + }); + } + + return list; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdListIndexes.razor b/Rms.Risk.Mango/Components/Commands/CmdListIndexes.razor new file mode 100644 index 0000000..1fcb0c0 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdListIndexes.razor @@ -0,0 +1,47 @@ +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "List indexes", "Indexes")] + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+ +
+
+ +
+
+ +@code { + + protected override string CommandTemplate => @"{ + ""listIndexes"" : ""[COLLECTION]"" +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender || Parameters == null) + return; + + Parameters.NeedConfirmation = false; + Validate(); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdListShards.razor b/Rms.Risk.Mango/Components/Commands/CmdListShards.razor new file mode 100644 index 0000000..25ad15e --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdListShards.razor @@ -0,0 +1,62 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "List shards", "Admin")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""listShards"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + Parameters.NeedConfirmation = false; + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/CmdMapShards.razor b/Rms.Risk.Mango/Components/Commands/CmdMapShards.razor new file mode 100644 index 0000000..c3b052f --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdMapShards.razor @@ -0,0 +1,63 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Map shards", "Admin")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""getShardMap"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + Parameters.NeedConfirmation = false; + } + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdReIndex.razor b/Rms.Risk.Mango/Components/Commands/CmdReIndex.razor new file mode 100644 index 0000000..dda21f7 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdReIndex.razor @@ -0,0 +1,39 @@ +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Reindex", "Indexes")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+} + +@code { + protected override string CommandTemplate => @"{ + ""reIndex"": ""[COLLECTION]"" +}"; +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdServerStatus.razor b/Rms.Risk.Mango/Components/Commands/CmdServerStatus.razor new file mode 100644 index 0000000..b630086 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdServerStatus.razor @@ -0,0 +1,63 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Server status", "Informational")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + ] + }; + + protected override string CommandTemplate => @"{ + ""serverStatus"" : 1 +}"; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + { + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + Parameters.NeedConfirmation = false; + } + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdShardCollection.razor b/Rms.Risk.Mango/Components/Commands/CmdShardCollection.razor new file mode 100644 index 0000000..863c33d --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdShardCollection.razor @@ -0,0 +1,122 @@ +@using MongoDB.Bson.IO +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Shard", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "bool", Name = "Unique", Path = "$.unique", Icon = "icon-wrench-outline-sm" }, + new() { Type = "int", Name = "Initial Chunks", Path = "$.numInitialChunks", Icon = "icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "PresplitHashedZones", Path = "$.presplitHashedZones", Icon = "icon-wrench-outline-sm" } + ] + }; + + protected override string CommandTemplate => @"{ + shardCollection : ""[DATABASE].[COLLECTION]"", + key : { ""_id"": ""hashed""}, + unique: false, + presplitHashedZones: false, + numInitialChunks: 8182, +}"; + + private string IndexJson + { + get; + set + { + if (field == value) + return; + field = value; + UpdateCommand(); + } + } = "{ key : { _id : \"hashed\"} }"; + + private bool IsValid + { + get; + set + { + if (field == value) + return; + field = value; + Validate(); + } + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + } + + private void UpdateCommand() + { + if (Parameters == null) + return; + + var doc = BsonDocument.Parse(Parameters.CommandJson); + doc["key"] = BsonDocument.Parse(IndexJson)["key"]; + + Parameters.CommandJson = doc.ToJson(new() { Indent = true, OutputMode = JsonOutputMode.RelaxedExtendedJson }); + Parameters.NeedConfirmation = true; + Validate(); + InvokeAsync(StateHasChanged); + } + + protected override void Validate() + { + if (Parameters != null && Parameters.CanExecute != IsValid) + { + Parameters.CanExecute = IsValid; + InvokeAsync(StateHasChanged); + } + + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdStatistics.razor b/Rms.Risk.Mango/Components/Commands/CmdStatistics.razor new file mode 100644 index 0000000..857ee7f --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdStatistics.razor @@ -0,0 +1,114 @@ +@inherits CmdBase +@attribute [MongoCommand(CmdType.Read, "Statistics", "Informational")] + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ +
+ +
+
+ + + + +
+ +
+
+ + +@code { + // + + private string Json { get; set; } = ""; + private List Stats { get; set; } = []; + + private static readonly List<(string, string, bool)> _descriptor = + [ + ("ns" , "Full name" , false), + ("sharded" , "Is sharded" , false), + ("count" , "Number of documents" , false), + ("size" , "Size" , false), + ("storageSize" , "Storage size" , true ), + ("totalIndexSize" , "Total index size" , true ), + ("totalSize" , "Total size" , true ), + ("avgObjSize" , "Average object size" , true ), + ("maxSize" , "Max size" , true ), + ("nindexes" , "Number of indexes" , false), + ("nchunks" , "Number of chunks" , false) + ]; + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender ) + UpdateCommand(); + return Task.CompletedTask; + } + + private void UpdateCommand() + { + if (Parameters == null) + return; + Parameters.CommandJson = $@"{{ + ""collStats"": ""{Collection}"", + ""scale"": 1 +}}"; + Parameters.NeedConfirmation = false; + Validate(); + } + + protected override Task OnCollectionChanged() + { + UpdateCommand(); + return Task.CompletedTask; + } + + protected override void OnResultReceived() + { + if (Parameters?.Result.Count > 0) + { + Json = Parameters.Result[0].ToJson(new() { Indent = true }); + Stats = FormatDocument(Parameters.Result[0], _descriptor).Cast().ToList(); + } + else + { + Json = ""; + Stats = []; + } + + InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdUnshardCollection.razor b/Rms.Risk.Mango/Components/Commands/CmdUnshardCollection.razor new file mode 100644 index 0000000..2fef06d --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdUnshardCollection.razor @@ -0,0 +1,64 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Admin, "Unshard", "Collections")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+ +
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "text", Name = "To shard", Path = "$.toShard", Icon = "icon-wrench-outline-sm" } + ] + }; + + protected override string CommandTemplate => @"{ + ""unshardCollection"" : ""[DATABASE].[COLLECTION]"", + ""toShard"": """" +}"; + + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + if (Parameters != null) + Parameters.RequiredConnectionType = CommandParams.DatabaseConnectionType.Admin; + } + +} diff --git a/Rms.Risk.Mango/Components/Commands/CmdUpdateDocuments.razor b/Rms.Risk.Mango/Components/Commands/CmdUpdateDocuments.razor new file mode 100644 index 0000000..6075b6b --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/CmdUpdateDocuments.razor @@ -0,0 +1,69 @@ +@using Rms.Risk.Mango.Components.JForms +@inherits CmdBase +@attribute [MongoCommand(CmdType.Write, "Update documents", "Documents")] + +@if (Parameters != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ +
+
+ +
+
+ +
+
+ +
+
+} + +@code { + + private readonly TemplateRec _template = new() + { + Parameters = + [ + new() { Type = "json", Name = "Filter", Path = "$.updates[0].q", Icon ="icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Update or insert", Path = "$.updates[0].upsert", Icon = "icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Multiple docs", Path = "$.updates[0].multi", Icon = "icon-wrench-outline-sm" }, + new() { Type = "-" }, + new() { Type = "json", Name = "Update", Path = "$.updates[0].u", Icon ="icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Ordered", Path = "$.ordered", Icon = "icon-wrench-outline-sm" }, + new() { Type = "bool", Name = "Bypass validation", Path = "$.bypassDocumentValidation", Icon = "icon-wrench-outline-sm" }, + ] + }; + + protected override string CommandTemplate => @"{ + ""update"": ""[COLLECTION]"", + ""updates"": [ + { + ""q"": { ""_id"" : ""XXX""}, + ""u"": {}, + ""upsert"": false, + ""multi"": false, + } + ], + ""ordered"": false, + ""bypassDocumentValidation"": false +}"; + +} diff --git a/Rms.Risk.Mango/Components/Commands/IndexEditComponent.razor b/Rms.Risk.Mango/Components/Commands/IndexEditComponent.razor new file mode 100644 index 0000000..b65c336 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/IndexEditComponent.razor @@ -0,0 +1,219 @@ +@using System.Text + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+ @if (!HideIndexParams) + { +
+ + + +
+ } +
+ +
+
+ +@code { + [Parameter] public string Class { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } = true; + + [Parameter] + public string Collection + { + get; + set + { + if (field == value) + return; + field = value; + ParseJson("{}"); + } + } = ""; + + [Parameter] + public string Json + { + get; + set + { + if (field == value) + return; + field = value; + ParseJson(value); + } + } = "{}"; + + [Parameter] public EventCallback JsonChanged { get; set; } + [Parameter] public bool IsValid { get; set; } + [Parameter] public EventCallback IsValidChanged { get; set; } + [Parameter] public bool HideIndexParams { get; set; } + + private string IndexName + { + get; + set + { + if (field == value) + return; + field = value; + UpdateCommand(); + } + } = "NewIndex"; + + private string ExpireAfter + { + get; + set + { + if (field == value) + return; + field = value; + UpdateCommand(); + } + } = "0"; + + private bool Unique + { + get; + set + { + if (field == value) + return; + field = value; + UpdateCommand(); + } + } + + private List.NameValuePair> _keyFields = []; + + private void UpdateCommand() + { + Json = JsonUtils.FormatJson($@"{{ + ""key"": {{ + {GetIndexKeys()} + }}, + ""name"": ""{IndexName}"" +{ExpireAtOption} +{UniqueOption} + }}"); + + IsValid = (HideIndexParams || !string.IsNullOrWhiteSpace(IndexName)) && _keyFields.Any(x => !string.IsNullOrWhiteSpace(x.Name)); + + JsonChanged.InvokeAsync(Json); + IsValidChanged.InvokeAsync(IsValid); + InvokeAsync(StateHasChanged); + } + + private string ExpireAtOption => int.Parse(ExpireAfter) > 0 ? $",\"expireAfterSeconds\": {ExpireAfter}" : ""; + private string UniqueOption => Unique ? ",\"unique\": 1" : ""; + + private string GetIndexKeys() + { + var sb = new StringBuilder(); + foreach (var def in _keyFields) + { + if (def.Value == DatabaseStructureLoader.FieldSorting.Hashed) + sb.AppendLine($" \"{def.Name}\" : \"hashed\","); + else + sb.AppendLine($" \"{def.Name}\" : {(def.Value == DatabaseStructureLoader.FieldSorting.Asc ? 1 : -1)},"); + } + + return sb.ToString(); + } + + private void ParseJson(string json) + { + _keyFields = []; + var dict = BsonDocument.Parse(json) + .ToDictionary() + .ToDictionary(StringComparer.OrdinalIgnoreCase) + ; + + if (dict.TryGetValue("name", out var n) && n != null) + IndexName = n.ToString() ?? ""; + else + IndexName = "NewIndex"; + + if (dict.TryGetValue("unique", out var u) && u != null) + Unique = u.ToString() == "1" || u.ToString() == "True"; + else + Unique = false; + + if (dict.TryGetValue("expireAfterSeconds", out var eas) && eas != null) + ExpireAfter = eas.ToString()!; + else + ExpireAfter = "0"; + + if (dict.TryGetValue("key", out var keys) && keys is Dictionary kd) + { + _keyFields.AddRange( + kd.Select( + x => new FormItemNameValue.NameValuePair + { + Name = x.Key, + Value = ToFieldSorting(x.Value) + } + ) + ); + } + + UpdateCommand(); + } + + private DatabaseStructureLoader.FieldSorting ToFieldSorting(object argValue) + => argValue switch + { + 1 => DatabaseStructureLoader.FieldSorting.Asc, + -1 => DatabaseStructureLoader.FieldSorting.Desc, + "hashed" => DatabaseStructureLoader.FieldSorting.Hashed, + _ => DatabaseStructureLoader.FieldSorting.Asc + }; + + private void SetSorting(FormItemNameValue.NameValuePair tuple, string v) + { + tuple.Value = Enum.TryParse(v, true, out var vv) + ? vv + : DatabaseStructureLoader.FieldSorting.Asc + ; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/Commands/MongoCommandAttribute.cs b/Rms.Risk.Mango/Components/Commands/MongoCommandAttribute.cs new file mode 100644 index 0000000..f22fb58 --- /dev/null +++ b/Rms.Risk.Mango/Components/Commands/MongoCommandAttribute.cs @@ -0,0 +1,34 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Components.Commands; + +public enum CmdType +{ + Hidden, + Read, + Write, + Admin +} + +public class MongoCommandAttribute(CmdType _type, string _name, string _group) : Attribute +{ + public CmdType Type => _type; + public string Name => _name; + public string Group => _group; +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/DatabaseInfo.razor b/Rms.Risk.Mango/Components/DatabaseInfo.razor new file mode 100644 index 0000000..dedcdc3 --- /dev/null +++ b/Rms.Risk.Mango/Components/DatabaseInfo.razor @@ -0,0 +1,207 @@ +@using System.Text.RegularExpressions +@attribute [Authorize] + +@inject IMongoDbServiceFactory MongoDbServiceFactory +@inject IUserSession UserSession +@inject IAuthorizationService Auth + +@if (Error != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ + +} +else if (_loading) +{ + +} +else +{ + + @foreach (var item in _stats.Where(x => !_exclude.Contains(x.Key))) + { + + } +
@(FormatName(item)):
@FormatValue(item)
+} + +@code { + + /// + /// This parameter value is unused, but any change initiating stats reloading + /// + [Parameter] + public string? Database + { + get; + set + { + if (field == value) + return; + + field = value; + InvokeAsync(StateHasChanged); + } + } + + /// + /// This parameter value is unused, but any change initiating stats reloading + /// + [Parameter] + public string? DatabaseInstance + { + get; + set + { + if (field == value) + return; + + field = value; + InvokeAsync(StateHasChanged); + } + } + + private Dictionary _stats = new(); + private string _loadedFor = ""; + private Exception? Error { get; set; } + private bool _loading; + private HashSet _exclude = + [ + "raw", + "ok", + "$clusterTime", + "operationTime", + ]; + + protected override Task OnAfterRenderAsync(bool firstRender) + { + var loadingFor = $"{UserSession.Database},{UserSession.DatabaseInstance}"; + if (_loading || _loadedFor == loadingFor) + return Task.CompletedTask; + + _ = Task.Run(Refresh); + + return Task.CompletedTask; + } + + private async Task Refresh() + { + var loadingFor = $"{UserSession.Database},{UserSession.DatabaseInstance}"; + + if (_loading || _loadedFor == loadingFor) + return; + _loading = true; + _loadedFor = loadingFor; + try + { + Error = null; + _stats.Clear(); + await InvokeAsync(StateHasChanged); + + await RefreshInternal(); + } + catch (Exception e) + { + Error = e; + } + finally + { + _loading = false; + } + await InvokeAsync(StateHasChanged); + } + + private async Task RefreshInternal() + { + var admin = UserSession.DatabaseInstance == null + ? UserSession.MongoDbAdminForAdminDatabase + : UserSession.MongoDbAdmin + ; + + var stats = await admin.RunCommand( + BsonDocument.Parse( @"{ +dbStats: 1, +scale: 1, +freeStorage: 0 +}")); + _stats = stats.ToDictionary(); + + var readAccess = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + Database, + [new ReadAccessRequirement()]); + var writeAccess = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + Database, + [new WriteAccessRequirement()]); + var adminAccess = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + Database, + [new AdminAccessRequirement()]); + + _stats["Read access"] = readAccess.Succeeded; + _stats["Write access"] = writeAccess.Succeeded; + _stats["Admin access"] = adminAccess.Succeeded; + + } + + private static HashSet _byteMeasures = + [ + "avgObjSize", + "dataSize", + "storageSize", + "totalSize", + "indexSize", + "fileSize", + "fsUsedSize", + "fsTotalSize", + ]; + + private static HashSet _numberMeasures = + [ + "objects", + "indexes", + "scaleFactor", + ]; + + private static string SplitCamelCase(string input) + { + return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim(); + } + + private string FormatName(KeyValuePair item) + { + var s = SplitCamelCase(item.Key) + .Split(" ", StringSplitOptions.RemoveEmptyEntries) + .Select((x, i) => i == 0 ? char.ToUpper(x[0]) + x[1..] : char.ToLower(x[0]) + x[1..]) + ; + return string.Join(" ", s); + } + + private string FormatValue(KeyValuePair item) + { + if (_byteMeasures.Contains(item.Key)) + return NumbersUtils.ToHumanReadable(Convert.ToDouble(item.Value)); + if (_numberMeasures.Contains(item.Key)) + return Convert.ToDouble(item.Value).ToString("N0"); + return item.Value?.ToString() ?? ""; + } + +} diff --git a/Rms.Risk.Mango/Components/DatabaseOnboardingControl.razor b/Rms.Risk.Mango/Components/DatabaseOnboardingControl.razor new file mode 100644 index 0000000..8e0f7e9 --- /dev/null +++ b/Rms.Risk.Mango/Components/DatabaseOnboardingControl.razor @@ -0,0 +1,388 @@ +@using Microsoft.Extensions.Options +@using MongoDB.Driver +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@using Rms.Risk.Mango.Services.Context +@using Rms.Service.Bootstrap.Security + +@inject IDatabaseConfigurationService DatabaseConfig +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IPasswordManager PasswordManager +@inject IOptions Settings +@inject IUserService UserService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + +
+
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ + + + + +
+ +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Name { get; set; } = "New connection"; + [Parameter] public DatabasesConfig.DatabaseConfig Value { get; set; } = new(); + [Parameter] public EventCallback OnRefresh { get; set; } + + private EditContext EditContext => _editContext!; + + private EditContext? _editContext; + + private string UserUser + { + get => Value.Config.Auth?.User ?? ""; + set + { + Value.Config.Auth ??= new(); + Value.Config.Auth.User = value; + } + } + + private string UserPassword + { + get => Value.Config.Auth?.Password ?? ""; + set + { + Value.Config.Auth ??= new(); + Value.Config.Auth.Password = value; + } + } + + private string AuthDatabase + { + get => Value.Config.Auth?.AuthDatabase ?? ""; + set + { + Value.Config.Auth ??= new(); + Value.Config.Auth.AuthDatabase = value; + } + } + + private string AuthMethod + { + get => Value.Config.Auth?.Method ?? ""; + set + { + Value.Config.Auth ??= new(); + Value.Config.Auth.Method = value; + } + } + + private string AdminUser + { + get => Value.Config.AdminAuth?.User ?? ""; + set + { + Value.Config.AdminAuth ??= new(); + Value.Config.AdminAuth.User = value; + } + } + + private string AdminPassword + { + get => Value.Config.AdminAuth?.Password ?? ""; + set + { + Value.Config.AdminAuth ??= new(); + Value.Config.AdminAuth.Password = value; + } + } + + protected override void OnInitialized() + { + _editContext = new(this); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + + private async Task Update() + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Update configuration", + $"Are you sure want to update configuration for {Name}?" + ); + if (res.Cancelled) + return; + + try + { + PrepareForSaving(); + await DatabaseConfig.Update(Name, Value, UserSession.User.GetEmail()); + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", $"Configuration {Name} was updated successfully."); + + // exception in Audit() call can lead to 2 dialogs shown in total: Success in config change + error recording audit + try + { + await Audit(ticket, "update", Name); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + + } + catch (Exception ex) + { + try + { + await Audit(ticket, "update", Name, ex); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + + await OnRefresh.InvokeAsync(); + } + + private async Task Audit(string ticket, string action, string configName, Exception? exception = null) + { + var doc = new BsonDocument + { + ["dbMangoOnboarding"] = exception?.Message ?? "Success", + ["action"] = action, + ["configName"] = configName, + ["ticket"] = ticket + }; + + var auditRecord = new AuditRecord(UserSession.Database, DateTime.UtcNow, UserService.GetEmail(), ticket, exception == null, doc, exception?.Message); + await UserSession.Audit.Record(auditRecord); + } + + + private async Task Delete() + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Delete configuration", + $"Are you sure want to delete configuration for {Name}? (Note that pre-configured databases can't be deleted.)" + ); + if (res.Cancelled) + return; + + try + { + await DatabaseConfig.Delete(Name, UserSession.User.GetEmail()); + + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", $"Configuration {Name} was deleted successfully."); + try + { + await Audit(ticket, "delete", Name); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + } + catch (Exception ex) + { + try + { + await Audit(ticket, "delete", Name, ex); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + + await OnRefresh.InvokeAsync(); + } + + private void PrepareForSaving() + { + if (string.IsNullOrWhiteSpace(Value.Config.Auth?.Password) || string.IsNullOrWhiteSpace(Value.Config.Auth?.User)) + Value.Config.Auth = null; + if (string.IsNullOrWhiteSpace(Value.Config.AdminAuth?.Password) || string.IsNullOrWhiteSpace(Value.Config.AdminAuth?.User)) + Value.Config.AdminAuth = null; + } + + private class ClipboardFormat + { + public string Name { get; set; } = ""; + public DatabasesConfig.DatabaseConfig Value { get; set; } = new(); + } + + private async Task Copy() + { + try + { + PrepareForSaving(); + + var obj = new ClipboardFormat + { + Name = Name, + Value = Value + }; + + var json = JsonUtils.ToJson(obj, new() {WriteIndented = true}); + var clone = JsonUtils.FromJson(json); + if (clone == null) + return; + + if (!string.IsNullOrWhiteSpace(clone.Value.Config.Auth?.Password)) + clone.Value.Config.Auth.Password = PasswordManager.EncryptPassword(clone.Value.Config.Auth.Password); + if (!string.IsNullOrWhiteSpace(clone.Value.Config.AdminAuth?.Password)) + clone.Value.Config.AdminAuth.Password = PasswordManager.EncryptPassword(clone.Value.Config.AdminAuth.Password); + + json = JsonUtils.ToJson(clone, new() { WriteIndented = true }); + + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", json); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", $"Configuration {Name} was copied to clipboard."); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + + private async Task Paste() + { + try + { + var json = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + + var clone = JsonUtils.FromJson(json); + if (clone == null) + return; + + if (clone.Value.Config.Auth?.Password.StartsWith('*') ?? false) + clone.Value.Config.Auth.Password = PasswordManager.DecryptPassword(clone.Value.Config.Auth.Password); + if (clone.Value.Config.AdminAuth?.Password.StartsWith('*') ?? false) + clone.Value.Config.AdminAuth.Password = PasswordManager.DecryptPassword(clone.Value.Config.AdminAuth.Password); + + Name = clone.Name; + Value = clone.Value; + + await InvokeAsync(StateHasChanged); + + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", $"Configuration {Name} was successfully parsed. You still need to save it!"); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + + private async Task Test() + { + try + { + var databaseInstance = !string.IsNullOrWhiteSpace(Value.Config.MongoDbDatabase) ? Value.Config.MongoDbDatabase : "admin"; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var db = MongoDbHelper.GetDatabase(Value.Config, Settings.Value.Settings, databaseInstance); + _ = await db.RunCommandAsync( + new BsonDocumentCommand(BsonDocument.Parse( @"{ ""ping"" : 1 }")), + cancellationToken: cts.Token); + + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", $"Successfully connected to {Name}."); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } +} diff --git a/Rms.Risk.Mango/Components/EncryptControl.razor b/Rms.Risk.Mango/Components/EncryptControl.razor new file mode 100644 index 0000000..f86c816 --- /dev/null +++ b/Rms.Risk.Mango/Components/EncryptControl.razor @@ -0,0 +1,226 @@ +@using System.IO +@using Rms.Service.Bootstrap.Security + +@inject IPasswordManager PasswordManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
+
+ +
+
+
+ + +
+ +
+
+ + +
+ +
+ +
+
+ + @if (ShowFile) + { +
+
+ + +
+
+ } + +
+ @if (!string.IsNullOrWhiteSpace(EncryptedText)) + { +

As password:

+ + } + @if (ShowFile && !string.IsNullOrWhiteSpace(EncryptedFile)) + { +

As file:

+ + } +
+ +
+ +@code { + + [Parameter] public string ClearText + { + get; + set + { + if (field == value) + return; + field = value; + ClearTextChanged.InvokeAsync(field); + InvokeAsync(StateHasChanged); + } + } = ""; + [Parameter] public EventCallback ClearTextChanged { get; set; } + + [Parameter] public string EncryptedText { get; set; } = ""; + [Parameter] public EventCallback EncryptedTextChanged { get; set; } + + [Parameter] public string EncryptedFile { get; set; } = ""; + [Parameter] public EventCallback EncryptedFileChanged { get; set; } + + [Parameter] public bool ShowFile { get; set; } = false; + + private bool IsEncryptDisabled => string.IsNullOrWhiteSpace(ClearText); + + private void GeneratePassword() => ClearText = GenerateGoodPassword(); + + private async Task DoEncryption() + { + try + { + EncryptedText = PasswordManager.EncryptPassword(ClearText); + await EncryptedTextChanged.InvokeAsync(EncryptedText); + } + catch (Exception e) + { + EncryptedText = e.Message; + } + try + { + EncryptedFile = PasswordManager.EncryptFileContents(ClearText); + await EncryptedFileChanged.InvokeAsync(EncryptedFile); + } + catch (Exception e) + { + EncryptedFile = e.Message; + } + + ClearText = ""; + await InvokeAsync(StateHasChanged); + } + + private async Task OnFileUploaded(InputFileChangeEventArgs arg) + { + await using var mem = new MemoryStream(); + const long oneHundredMb = 100 * 1024 * 1024; + await arg.File.OpenReadStream(maxAllowedSize: oneHundredMb).CopyToAsync(mem); + + var byteArray = mem.ToArray(); + + try + { + EncryptedText = ""; + } + catch (Exception e) + { + EncryptedText = e.Message; + } + try + { + EncryptedFile = PasswordManager.EncryptBinaryFileContents(byteArray); + } + catch (Exception e) + { + EncryptedFile = e.Message; + } + + ClearText = ""; + await InvokeAsync(StateHasChanged); + } + + private static string GenerateGoodPassword() + { + const int generatedPasswordLength = 24; + const string specialChars = "!$%^&*()_+#~/?-="; + const string letters = "qwertyuioplkjhgfdsazxcvbnm"; + const string digits = "0123456789"; + var alphabet = specialChars + + letters + + letters.ToUpper() + + digits + ; + + var password = ""; + var count = 1000; + while (!IsGoodPassword(password) && count > 0) + { + password = ""; + count -= 1; + + for (var i = 0; i < generatedPasswordLength; i++) + { + var pos = Random.Shared.Next(alphabet.Length); + password += alphabet[pos]; + } + } + + return password; + + bool IsGoodPassword(string s) + { + var hasLower = false; + var hasUpper = false; + var hasDigit = false; + var hasSpecial = false; + + var upper = letters.ToUpper(); + + foreach (var c in s) + { + if (letters.IndexOf(c) >= 0) + { + hasLower = true; + continue; + } + if (upper.IndexOf(c) >= 0) + { + hasUpper = true; + continue; + } + if (digits.IndexOf(c) >= 0) + { + hasDigit = true; + continue; + } + if (specialChars.IndexOf(c) >= 0) + { + hasSpecial = true; + } + } + + return hasLower && hasUpper && hasDigit && hasSpecial; + } + } + +} diff --git a/Rms.Risk.Mango/Components/ExceptionControl.razor b/Rms.Risk.Mango/Components/ExceptionControl.razor new file mode 100644 index 0000000..757827b --- /dev/null +++ b/Rms.Risk.Mango/Components/ExceptionControl.razor @@ -0,0 +1,80 @@ +@inject IJSRuntime JsRuntime + +@if (Exception != null) + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +{ + +} + +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + [Parameter] public Exception? Exception { get; set; } + + private bool _expanded = false; + private string ExpandedClass => _expanded ? "" : "hidden"; + + private void OnClick() + { + _expanded = !_expanded; + StateHasChanged(); + } + + private async Task OnCopy() + { + if (Exception == null ) + return; + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", Exception.ToString()); + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", "Copied to the clipboard."); + + } + +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormBase.razor b/Rms.Risk.Mango/Components/JForms/JFormBase.razor new file mode 100644 index 0000000..d8f80aa --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormBase.razor @@ -0,0 +1,43 @@ +@code { + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + [Parameter] public string Class { get; set; } = ""; + [Parameter] public string Name { get; set; } = ""; + [Parameter] public string Icon { get; set; } = ""; + [Parameter] public bool Enabled { get; set; } + + [Parameter] public string Text + { + get; + set + { + var v = value.Replace("\r", ""); + if (field == v) + return; + field = v; + OnTextChanged(); + TextChanged.InvokeAsync(field); + } + } = ""; + + [Parameter] public EventCallback TextChanged { get; set; } + + protected virtual void OnTextChanged() { } +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormBool.razor b/Rms.Risk.Mango/Components/JForms/JFormBool.razor new file mode 100644 index 0000000..8c890f4 --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormBool.razor @@ -0,0 +1,51 @@ +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code +{ + private bool BoolValue + { + get; + set + { + if (field == value) + return; + field = value; + BoolValueChanged.InvokeAsync(field); + Text = value ? "True" : "False"; + } + } + + private EventCallback BoolValueChanged { get; set; } + + protected override void OnTextChanged() + { + BoolValue = Text.ToLower() switch + { + "1" => true, + "true" => true, + "yes" => true, + "on" => true, + _ => false + }; + } +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormCommand.razor b/Rms.Risk.Mango/Components/JForms/JFormCommand.razor new file mode 100644 index 0000000..1cdf6fe --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormCommand.razor @@ -0,0 +1,186 @@ +@using Microsoft.AspNetCore.Components.Rendering + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+
Documentation: @DocUri
+
+
+ @InnerContents +
+
+
+ +@code { + + [Parameter] public TemplateRec Template { get; set; } = new(); + [Parameter] public string CommandTemplate { get; set; } = @"{}"; + [Parameter] public EventCallback CommandChanged { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public string Class { get; set; } = ""; + [Parameter] public Func GetArguments { get; set; } = _ => null; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (Template.Parameters.Count <= 0 || !JsonUtils.IsValidJson(CommandTemplate)) + return; + + Template.UpdateText(CommandTemplate); + StateHasChanged(); + } + + public const string MongoDocUrl = "https://www.mongodb.com/docs/manual/reference/command/"; + + private string DocUri + { + get + { + if (Template.DocUri != null) + return Template.DocUri.ToString(); + try + { + if ( !BsonDocument.TryParse(CommandTemplate, out var json) ) + return MongoDocUrl; + + var cmd = json.Elements.FirstOrDefault().Name; + return string.IsNullOrWhiteSpace(cmd) + ? MongoDocUrl + : $"{MongoDocUrl}{cmd}/"; + } + catch (Exception) + { + return MongoDocUrl; + } + } + } + + private RenderFragment InnerContents => (builder) => + { + List> groups = []; + + List current = []; + + foreach (var paramRec in Template.Parameters) + { + if (paramRec.Type.StartsWith("-")) + { + groups.Add(current); + current = []; + continue; + } + + current.Add(paramRec); + } + + if (current.Count > 0 ) + groups.Add(current); + + var groupIdx = 0; + foreach (var group in groups) + { + builder.OpenElement(groupIdx++, "div"); + builder.AddAttribute(0, "class", "flex-stack-vertical mr-3"); + + var i = 0; + foreach (var parameter in group) + { + AddFragment(builder, i++, parameter); + } + + builder.CloseElement(); + } + }; + + + private RenderTreeBuilder AddFragment(RenderTreeBuilder builder, int index, TemplateRec.ParamRec p) + { + switch (p.Type) + { + case "text": + builder.OpenComponent(index); + AddAttributes(builder, p); + builder.CloseComponent(); + break; + case "json": + builder.OpenComponent(index); + AddAttributes(builder, p); + builder.CloseComponent(); + break; + case "bool": + builder.OpenComponent(index); + AddAttributes(builder, p); + builder.CloseComponent(); + break; + case "zeroone": + builder.OpenComponent(index); + AddAttributes(builder, p); + builder.CloseComponent(); + break; + case "int": + builder.OpenComponent(index); + AddAttributes(builder, p); + builder.AddAttribute(5, "InputType", "number"); + builder.CloseComponent(); + break; + case "enum": + Type enumType = typeof(JFormEnum<>).MakeGenericType(Type.GetType(p.TypeArg)!); + builder.OpenComponent(index, enumType); + AddAttributes(builder, p); + builder.CloseComponent(); + break; + case "select": + { + Type selectType = typeof(JFormSelect<>).MakeGenericType(Type.GetType(p.TypeArg)!); + builder.OpenComponent(index, selectType); + var idx = AddAttributes(builder, p); + + var values = GetArguments(p); + + builder.AddAttribute(idx, "Values", values); + builder.CloseComponent(); + } + break; + default: + builder.AddMarkupContent(index, "
 
"); + break; + } + + return builder; + } + + private int AddAttributes(RenderTreeBuilder builder, TemplateRec.ParamRec p) + { + builder.AddAttribute(0, "Text", p.Text); + builder.AddAttribute(1, "TextChanged", + EventCallback.Factory.Create(this, value => + { + p.Text = value; + CommandChanged.InvokeAsync(Template.UpdateCommand(CommandTemplate)); + })); + builder.AddAttribute(2, "Name", p.Name); + builder.AddAttribute(3, "Icon", p.Icon); + builder.AddAttribute(4, "Class", p.Class); + return 5; + } + +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormEnum.razor b/Rms.Risk.Mango/Components/JForms/JFormEnum.razor new file mode 100644 index 0000000..66c8ebd --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormEnum.razor @@ -0,0 +1,44 @@ +@typeparam TEnum +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code +{ + private TEnum? EnumValue + { + get; + set + { + if (field?.Equals(value) ?? false) + return; + field = value; + Text = value!.ToString()!; + } + } + + private EventCallback EnumValueChanged { get; set; } + + protected override void OnTextChanged() + { + EnumValue = (TEnum)Enum.Parse(typeof(TEnum), Text); + } +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormObject.razor b/Rms.Risk.Mango/Components/JForms/JFormObject.razor new file mode 100644 index 0000000..f5cad17 --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormObject.razor @@ -0,0 +1,22 @@ +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + diff --git a/Rms.Risk.Mango/Components/JForms/JFormSelect.razor b/Rms.Risk.Mango/Components/JForms/JFormSelect.razor new file mode 100644 index 0000000..e4a6be7 --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormSelect.razor @@ -0,0 +1,59 @@ +@typeparam T +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code +{ + [Parameter] + public IReadOnlyCollection Values + { + get; + set + { + if (field == value) + return; + field = value; + if ( !field.Contains(TypedValue)) + TypedValue = field.FirstOrDefault(); + } + } = []; + + private T? TypedValue + { + get; + set + { + if (field?.Equals(value) ?? false) + return; + field = value; + Text = value?.ToString() ?? ""; + } + } + + private EventCallback TypedValueChanged { get; set; } + + protected override void OnTextChanged() + { + TypedValue = (T)Convert.ChangeType(Text, typeof(T)); + } + +} diff --git a/Rms.Risk.Mango/Components/JForms/JFormText.razor b/Rms.Risk.Mango/Components/JForms/JFormText.razor new file mode 100644 index 0000000..6779214 --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormText.razor @@ -0,0 +1,25 @@ +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code{ + [Parameter] public string InputType { get; set; } = "text"; +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/JForms/JFormZeroOne.razor b/Rms.Risk.Mango/Components/JForms/JFormZeroOne.razor new file mode 100644 index 0000000..562823f --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/JFormZeroOne.razor @@ -0,0 +1,50 @@ +@inherits JFormBase + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +@code +{ + private bool BoolValue + { + get; + set + { + if (field == value) + return; + field = value; + Text = value ? "1" : "0"; + } + } + + private EventCallback BoolValueChanged { get; set; } + + protected override void OnTextChanged() + { + BoolValue = Text.ToLower() switch + { + "1" => true, + "true" => true, + "yes" => true, + "on" => true, + _ => false + }; + } +} diff --git a/Rms.Risk.Mango/Components/JForms/TemplateRec.cs b/Rms.Risk.Mango/Components/JForms/TemplateRec.cs new file mode 100644 index 0000000..3dd01ad --- /dev/null +++ b/Rms.Risk.Mango/Components/JForms/TemplateRec.cs @@ -0,0 +1,149 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Newtonsoft.Json.Linq; +using Rms.Risk.Mango.Pivot.UI.Services; + +namespace Rms.Risk.Mango.Components.JForms; + +public class TemplateRec +{ + public class ParamRec + { + public string Name { get; init; } = ""; + public string? Icon { get; init; } + public string Type { get; init; } = "text"; + public string TypeArg { get; init; } = ""; + public string Path { get; init; } = "$"; + public string Class { get; init; } = ""; + public string Text { get; set; } = ""; + } + + public Uri? DocUri { get; set; } + public List Parameters { get; set; } = []; + + public void UpdateText(string value) + { + var root = JsonUtils.FromJson(value); + if (root == null) + return; + + foreach (var p in Parameters) + { + p.Text = GetText(root, p.Path); + } + } + + public string UpdateCommand(string value) + { + if (!JsonUtils.IsValidJson(value)) + return value; + + var root = JsonUtils.FromJson(value); + if (root == null) + return value; + + foreach (var p in Parameters) + { + if (p.Type == "json") + SetObject(root, p.Text, p.Path); + else if ( p.Type == "select" ) + SetText(root, p.Text, p.Path, p.TypeArg); + else if ( p.Type != "-" ) + SetText(root, p.Text, p.Path, p.Type); + } + + return JsonUtils.ToJson(root, new() { WriteIndented = true }); + } + + private static void SetText(JToken root, string value, string path, string typeName) + { + if (typeName == "string") + { + root.ReplacePath(path, value.Replace("\r", "")); + } + else + { + try + { + var type = GetType(typeName); + + var convertedValue = Convert.ChangeType(value, type); + root.ReplacePath(path, convertedValue); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to convert value to type '{typeName}': {ex.Message}", ex); + } + } + } + + private static Type GetType(string typeName) + { + switch (typeName) + { + case "text": + case "string": + return typeof(string); + case "zeroone": + case "int": + return typeof(int); + case "long": + return typeof(long); + case "bool": + return typeof(bool); + case "number": + case "double": + return typeof(double); + case "decimal": + return typeof(decimal); + case "DateTime": + return typeof(DateTime); + } + + if (string.IsNullOrEmpty(typeName)) + return typeof(string); + var type = Type.GetType(typeName); + if (type == null) + throw new InvalidOperationException($"Type '{typeName}' not found."); + return type; + } + + private static void SetObject(JToken root, string value, string path) + { + if (!JsonUtils.IsValidJson(value)) + return; + + var obj = JsonUtils.FromJson(value.Replace("\r", "")); + if (obj == null) + return; + + root.ReplacePath(path, obj); + } + + private string GetText(JToken root, string path) + { + var obj = root.GetOneByPath(path); + if (obj == null) + return ""; + if (obj.Type == JTokenType.Object) + return JsonUtils.ToJson(obj, new() { WriteIndented = true }); + return obj.ToString(); + } +} + diff --git a/Rms.Risk.Mango/Components/LoginControl.razor b/Rms.Risk.Mango/Components/LoginControl.razor new file mode 100644 index 0000000..702c438 --- /dev/null +++ b/Rms.Risk.Mango/Components/LoginControl.razor @@ -0,0 +1,77 @@ +@using System.Security.Claims +@inject NavigationManager NavManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + @GetEmail(context.User) + + + +
+ + +
+
+
+ +@code { + + [Parameter] + public bool ForceLogin { get; set; } + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && ForceLogin) + { + Login(); + } + } + + private void Login() + { + NavManager.NavigateTo($"/login/{Uri.EscapeDataString(Base64Encode(NavManager.ToBaseRelativePath(NavManager.Uri)))}", true, true); + } + + private static string Base64Encode(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + private void Logout() + { + NavManager.NavigateTo("/logout", true, true); + } + + private string GetEmail( ClaimsPrincipal principal ) + { + var email = principal.FindFirst(ClaimTypes.Email); + return email?.Value ?? ""; + } + +} diff --git a/Rms.Risk.Mango/Components/MessageBoxEncryptComponent.razor b/Rms.Risk.Mango/Components/MessageBoxEncryptComponent.razor new file mode 100644 index 0000000..1e9f7fc --- /dev/null +++ b/Rms.Risk.Mango/Components/MessageBoxEncryptComponent.razor @@ -0,0 +1,66 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ +
+ + + +

Use this form to encrypt sensitive data.

+ +
+ +
+ + + +
+ +@code +{ + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = null!; + + + [Parameter] public bool ShowCancel { get; set; } + [Parameter] public string InitialPassword { get; set; } = ""; + + private string EncryptedText { get; set; } = ""; + + private async Task OnOK() + { + if ( string.IsNullOrWhiteSpace(EncryptedText) ) + { + await BlazoredModal.CancelAsync(); + return; + } + + await BlazoredModal.CloseAsync(ModalResult.Ok(EncryptedText)); + } +} diff --git a/Rms.Risk.Mango/Components/MigrationJobControl.razor b/Rms.Risk.Mango/Components/MigrationJobControl.razor new file mode 100644 index 0000000..735f2c2 --- /dev/null +++ b/Rms.Risk.Mango/Components/MigrationJobControl.razor @@ -0,0 +1,393 @@ +@using Rms.Risk.Mango.Services.Context +@using Rms.Risk.Mango.Pivot.Core.MongoDb; + +@inject IUserSession UserSession +@inject IDatabaseConfigurationService DatabaseConfig +@inject IAuthorizationService Auth + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + +
+
+ + + +
+
+ + + +
+ @if (UserSession.IsInstanceSelectionAllowed(SourceDatabase)) + { +
+ +
+ } +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
@JobDescription
+ +
+ + +
+ +@code { + [CascadingParameter] public BlazoredModalInstance Modal { get; set; } = null!; + + private string SourceDatabase + { + get; + set + { + if (field == value) + return; + field = value; + Error = null; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(SourceDatabaseChanged); + } + } = ""; + + private string SourceDatabaseInstance + { + get; + set + { + if (field == value) + return; + field = value; + Error = null; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(SourceDatabaseChanged); + } + } = ""; + + private bool WipeDestination { get; set; } + private bool DisableIndexes { get; set; } + private string BatchSize { get; set; } = "1000"; + private bool Upsert { get; set; } + private bool IsReady { get; set; } + private bool IsReadyToRun => IsReady && SelectedCollections.Any(x => x.Selected); + private Exception? Error { get; set; } + private EditContext EditContext => _editContext!; + private List.SelectableItem> SelectedCollections { get; set; } = []; + private List SourceDatabaseInstances { get; set; } = []; + + private CancellationTokenSource? _cts; + + private Task SourceDatabaseChanged() + { + if ( _cts != null ) + { + _cts.Cancel(); + _cts.Dispose(); + } + + _cts = new (TimeSpan.FromSeconds(10)); + + _ = Task.Run(() => LoadInstances(_cts.Token)); + _ = Task.Run(() => LoadCollections(_cts.Token)); + return Task.CompletedTask; + } + + private List SourceDatabases { get; } = []; + + private MarkupString JobDescription => new( + SelectedCollections.Any(x => x.Selected) + ? $"Copy the following collections from {SourceDatabase} to {UserSession.Database}:" + + $"
{CollectionNamesToSync}." + + (DisableIndexes ? "
Indexes in destination collections will be dropped and re-created." : "") + + (WipeDestination ? "
Destination collections will be cleared first." : "") + + (Upsert ? "
Existing documents will be updated." : "") + : ""); + + + private string CollectionNamesToSync => + string.Join(", ", SelectedCollections.Where(x => x.Selected).Select(x => $"{x.Value}")); + + private EditContext? _editContext; + + protected override void OnInitialized() + { + _editContext = new(this); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + SourceDatabases.Clear(); + foreach (var name in DatabaseConfig.Databases.Keys + .Except([UserSession.Database]) + .OrderBy(x => x)) + { + if (!await IsUserAuthorized(name)) + continue; + SourceDatabases.Add(name); + } + + if (SourceDatabases.Count == 0) + return; + + SourceDatabase = SourceDatabases.First(); + + StateHasChanged(); + } + + private async Task LoadInstances(CancellationToken token) + { + if ( !UserSession.IsInstanceSelectionAllowed(SourceDatabase) ) + { + SourceDatabaseInstances.Clear(); + SourceDatabaseInstances.Add(UserSession.DatabaseInstance); + SourceDatabaseInstance = UserSession.DatabaseInstance; + await InvokeAsync(StateHasChanged); + return; + } + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + if (string.IsNullOrWhiteSpace(SourceDatabase)) + { + SourceDatabaseInstances.Clear(); + return; + } + + IReadOnlyCollection instances; + if (!await UserSession.CanAccess(Auth, DatabaseAccessPolicyExtensions.ReadAccessPolicy, SourceDatabase)) + { + instances = []; + } + else + { + var admin = UserSession.GetCustomAdmin(SourceDatabase, "admin"); + + var res = await admin.ListDatabases(token); + + instances = res.Select(x => x.Name).ToList(); + } + + if ( token.IsCancellationRequested ) + return; + + SourceDatabaseInstances.Clear(); + SourceDatabaseInstances.AddRange(instances); + SourceDatabaseInstance = SourceDatabaseInstances.First(); + } + catch (TaskCanceledException) + { + // Task was cancelled, do nothing + } + catch (Exception ex) + { + await Display(ex); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private async Task IsUserAuthorized(string database) + { + var readAccess = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + database, + [new ReadAccessRequirement()]); + return readAccess.Succeeded; + } + + private async Task LoadCollections(CancellationToken token) + { + try + { + if (UserSession.DatabaseInstance == null) + throw new("Migrations are not supported for instances with selectable databases."); + + Error = null; + IsReady = false; + await InvokeAsync(StateHasChanged); + + if (string.IsNullOrWhiteSpace(SourceDatabase)) + { + SelectedCollections.Clear(); + return; + } + + IReadOnlyCollection collections; + + if (!await UserSession.CanAccess(Auth, DatabaseAccessPolicyExtensions.ReadAccessPolicy, SourceDatabase)) + { + collections = []; + } + else + { + var admin = UserSession.GetCustomAdmin(SourceDatabase, UserSession.DatabaseInstance); + collections = await admin.ListCollections(token); + } + + var selectable = collections + .Select(x => + new FormItemCheckList.SelectableItem(x) + { + SelectedChanged = EventCallback.Factory.Create(this, _ => InvokeAsync(StateHasChanged)) + } + ) + .GroupBy(x => + x.Value.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) + ? " Meta" + : x.Value.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) + ? " Cache" + : " Data" + ); + + if ( token.IsCancellationRequested ) + return; + + SelectedCollections.Clear(); + foreach( var group in selectable) + { + SelectedCollections.Add(new (group.Key)); + SelectedCollections.AddRange(group.OrderBy(x => x.Value).Select(x => x)); + } + + Error = null; + } + catch (TaskCanceledException) + { + // Task was cancelled, do nothing + } + catch (Exception ex) + { + await Display(ex); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private bool IsSelectable(string arg) => !arg.StartsWith(" "); + + private Task Display(Exception e) + { + Error = e; + return InvokeAsync(StateHasChanged); + } + + + private async Task OnOK() + { + var job = new MigrationJob() + { + Type = MigrationJob.JobType.Copy, + SourceDatabase = SourceDatabase, + SourceDatabaseInstance = SourceDatabaseInstance, + DestinationDatabase = UserSession.Database, + DestinationDatabaseInstance = UserSession.DatabaseInstance, + Email = UserSession.User.GetEmail(), + Upsert = Upsert, + ClearDestinationBefore = WipeDestination, + DisableIndexes = DisableIndexes, + BatchSize = int.Parse(BatchSize), + Status = SelectedCollections + .Where(x => x.Selected) + .Select(x => new MigrationJob.CollectionJob + { + SourceCollection = x.Value, + DestinationCollection = x.Value, + }) + .ToList() + }; + + await Modal.CloseAsync(ModalResult.Ok(job)); + } + + public static Task ShowDialog(IModalService service) + { + var parameters = new ModalParameters(); + + var options = new ModalOptions + { + HideCloseButton = false, + DisableBackgroundCancel = true + }; + + var form = service.Show("New migration", parameters, options); + return form.Result; + } + +} diff --git a/Rms.Risk.Mango/Components/NameValueControl.razor b/Rms.Risk.Mango/Components/NameValueControl.razor new file mode 100644 index 0000000..f88a96f --- /dev/null +++ b/Rms.Risk.Mango/Components/NameValueControl.razor @@ -0,0 +1,385 @@ +@using System.IO +@using System.Reflection +@using log4net +@using Newtonsoft.Json.Linq +@using Rms.Service.Bootstrap.Security + +@inject IPasswordManager _passwordManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +
+ + @if (AllowImport) + { + + } + @if (ChildContent != null) + { + @ChildContent + } +
+
+ + + + + + + + + + + + + +
+ +@code { + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public class NameValuePair + { + public string Id { get; } = Guid.NewGuid().ToString("N"); + + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + } + + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public List Value { get; set; } = []; + [Parameter] public RenderFragment? ChildContent { get; set; } + + [Parameter] public bool AllowImport { get; set; } + [Parameter] public bool Multiline { get; set; } + [Parameter] public bool EnableEncryption { get; set; } = true; + [Parameter] public bool EncryptAsFile { get; set; } + [Parameter] public bool ShowLabel { get; set; } = true; + [Parameter] public bool ImportSeparateValues { get; set; } + [Parameter] public List PossibleNames { get; set; } = []; + [Parameter] public Func GetDefaultValue { get; set; } = _ => string.Empty; + + private static int CalculateSize(string value) => Math.Max(value.Split('\n').Length, value.Split('\r').Length); + +#pragma warning disable 1998 + private async Task SetValue(ChangeEventArgs e, NameValuePair data) + { + var val = e.Value?.ToString(); + if (string.IsNullOrWhiteSpace(val)) + return; + + var dataRef = Value.First(x => x.Id == data.Id); + dataRef.Value = val; + data.Value = val; + StateHasChanged(); + } + + private Task SetName(ChangeEventArgs e, NameValuePair data) => SetName(data, e.Value?.ToString()); + + private Task SetName(NameValuePair data, string? newName) + { + if ( string.IsNullOrWhiteSpace(newName)) + return Task.CompletedTask; + + var dataRef = Value.First(x => x.Id == data.Id); + dataRef.Name = newName; + data.Name = newName; + + var v = GetDefaultValue(newName); + dataRef.Value = v; + data.Value = v; + + StateHasChanged(); + return Task.CompletedTask; + } + + private void AddNewKeyValue() + { + Value.Add(new () { Name = string.Empty, Value = string.Empty }); + StateHasChanged(); + } + + private Task Delete(NameValuePair data) + { + Value.Remove(data); + return InvokeAsync(StateHasChanged); + } + +#pragma warning restore 1998 + private async void OnInputFileChange(InputFileChangeEventArgs e) + { + try + { + using var reader = new StreamReader(e.File.OpenReadStream()); + var text = await reader.ReadToEndAsync(); + + if (ImportSeparateValues) + { + if (text.TrimStart().StartsWith("{")) // json + ImportJson(text); + else // try name=value + ImportText(text); + } + else + ImportAsWhole(e, text); + + StateHasChanged(); + } + catch (Exception ex) + { + _log.Error(ex.Message, ex); + } + } + + private void ImportAsWhole(InputFileChangeEventArgs e, string text) + { + Value.Add(new () + { + Name = e.File.Name, + Value = text + }); + } + + private void ImportText(string text) + { + foreach (var s in text.Replace("\r","").Split("\n", StringSplitOptions.RemoveEmptyEntries)) + { + var pos = s.IndexOf('='); + if (s.StartsWith('#') || pos > 0) + continue; + + var name = s[..pos]; + var val = s[(pos+1)..]; + + Value.Add(new () + { + Name = name, + Value = val + }); + } + } + + private void ImportJson(string text) + { + var o = JObject.Parse(text); + foreach (var element in o.Properties()) + { + var name = element.Name; + var val = element.Value.ToString(); + + Value.Add(new () + { + Name = name, + Value = val + }); + } + } + + private bool IsUpDisabled(NameValuePair data) + { + var index = GetIndex(data); + return index <= 0; + } + + private int GetIndex(NameValuePair data) + { + var index = Value.IndexOf(data); + if (index < 0) + { + var dataRef = Value.FirstOrDefault(x => x.Name == data.Name); + if ( dataRef == null) + return -1; // not found + index = Value.IndexOf(dataRef); + } + + return index; + } + + private Task Up(NameValuePair data) + { + var index = GetIndex(data); + if (index <= 0) + return Task.CompletedTask; + + var dataRef = Value[index]; + Value.RemoveAt(index); + Value.Insert(index - 1, dataRef); + return InvokeAsync(StateHasChanged); + } + + private bool IsDownDisabled(NameValuePair data) + { + var index = GetIndex(data); + return index >= Value.Count - 1; + } + + private Task Down(NameValuePair data) + { + var index = GetIndex(data); + if (index >= Value.Count - 1) + return Task.CompletedTask; + + var dataRef = Value[index]; + Value.RemoveAt(index); + Value.Insert(index + 1, dataRef); + return InvokeAsync(StateHasChanged); + } + + private bool IsEncryptDisabled(NameValuePair data) => !EnableEncryption || (!EncryptAsFile && data.Value?.StartsWith('*') == true); + + private async Task Encrypt(NameValuePair data) + { + var index = GetIndex(data); + if (index < 0 ) + return; + + var dataRef = Value[index]; + + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Encrypt", $"Do you really want to encrypt entry \"{dataRef.Name}\"?
Note that this operation is irreversible!"); + if ( res?.Cancelled ?? true ) + return; + + if (EncryptAsFile) + { + var enc = _passwordManager.EncryptFileContents(dataRef.Value); + var dec = _passwordManager.DecryptFileContents(enc); + + if (dataRef.Value != dec) + throw new ApplicationException("Encryption is broken"); + dataRef.Value = enc; + } + else + { + var enc = _passwordManager.EncryptPassword(dataRef.Value); + var dec = _passwordManager.DecryptPassword(enc); + + if (dataRef.Value != dec) + throw new ApplicationException("Encryption is broken"); + dataRef.Value = enc; + } + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Components/RoleEditControl.razor b/Rms.Risk.Mango/Components/RoleEditControl.razor new file mode 100644 index 0000000..e45477b --- /dev/null +++ b/Rms.Risk.Mango/Components/RoleEditControl.razor @@ -0,0 +1,299 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+
+ + @if (string.IsNullOrWhiteSpace(Value.Db)) + { + + } + else + { + + } +
+ +
+ +
+ +
+
+
+ +
+ + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + + + + + + + +
+ +
+
+
+ + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Name { get; set; } = $"newRole{++_count}"; + + [Parameter] + public RoleInfoModel Value + { + get => field; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = new(); + + [Parameter] + public List AllRoles + { + get; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = []; + + [Parameter] + public List AllActions + { + get; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = []; + + [Parameter] public List AllDatabases { get; set; } = []; + [Parameter] public bool IsEnabled { get; set; } = true; + [Parameter] public bool IsNew { get; set; } + + private static int _count; + + private EditContext EditContext => _editContext!; + private EditContext? _editContext; + + public class SelectableString(string name, bool isSelected, Action update) + { + public override string ToString() => $"[{(IsSelected ? "X" : " ")}] {Name} {Db}"; + + public string Name { get; } = name; + public string Db { get; init; } = string.Empty; + + public bool IsSelected + { + get; + set + { + if (field == value) + return; + field = value; + update(this, field); + } + } = isSelected; + } + + private List SelectableActions { get; set; } = []; + + private PrivilegeModel SelectedPrivilege + { + get; + set + { + field = value; + UpdateSelectable(); + } + } = new(); + + protected override void OnInitialized() + { + _editContext = new(this); + } + + private void UpdateSelectable() + { + SelectableActions = AllActions + .Select(x => new SelectableString + ( + x, + SelectedPrivilege.Actions.Contains(x), + UpdateAllowedActions + )) + .ToList(); + + InvokeAsync(StateHasChanged); + } + + private void UpdateAllowedActions(SelectableString name, bool isSelected) + { + if (isSelected) + { + if (!SelectedPrivilege.Actions.Contains(name.Name)) + SelectedPrivilege.Actions.Add(name.Name); + } + else + { + SelectedPrivilege.Actions.Remove(name.Name); + } + } + + private async Task SelectedPrivilegeChanged((dynamic row, string col) arg) + { + if (arg.row is not PrivilegeModel privilege) + return; + + SelectedPrivilege = privilege; + await InvokeAsync(StateHasChanged); + } + + private Task AddPrivileges() + { + var newPrivilege = new PrivilegeModel(); + Value.Privileges.Add(newPrivilege); + SelectedPrivilege = newPrivilege; + return InvokeAsync(StateHasChanged); + } + + private Task DeletePrivileges() + { + Value.Privileges.Remove(SelectedPrivilege); + SelectedPrivilege = Value.Privileges.FirstOrDefault() ?? new PrivilegeModel(); + return InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Components/RolesSelector.razor b/Rms.Risk.Mango/Components/RolesSelector.razor new file mode 100644 index 0000000..a09806f --- /dev/null +++ b/Rms.Risk.Mango/Components/RolesSelector.razor @@ -0,0 +1,168 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + + + + + + + + + +@code { + [Parameter] + public List Value + { + get => field; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = new(); + + [Parameter] + public List AllRoles + { + get; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = []; + + + [Parameter] public bool IsEnabled { get; set; } = true; + + public class SelectableString(string name, bool isSelected, Action update) + { + public override string ToString() => $"[{(IsSelected ? "X" : " ")}] {Name} {Db}"; + + public string Name { get; } = name; + public string Db { get; init; } = string.Empty; + + public bool IsSelected + { + get; + set + { + if (field == value) + return; + field = value; + update(this, field); + } + } = isSelected; + } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private List SelectableRoles { get; set; } = []; + + private void UpdateSelectable() + { + SelectableRoles = AllRoles + .Select(x => + new SelectableString(x.Role, Value.Any(y => IsSameRole(x, y)), UpdateSelectedRoles) + { + Db = x.Db + } + ) + .ToList(); + + InvokeAsync(StateHasChanged); + } + + private static bool IsSameRole(RoleInDbModel x, RoleInDbModel y) => x.Role == y.Role && (x.Db == y.Db || string.IsNullOrWhiteSpace(x.Db) || string.IsNullOrWhiteSpace(y.Db)); + private static bool IsSameRole(RoleInDbModel x, SelectableString y) => x.Role == y.Name && (x.Db == y.Db || string.IsNullOrWhiteSpace(x.Db) || string.IsNullOrWhiteSpace(y.Db)); + + + private void UpdateSelectedRoles(SelectableString role, bool isSelected) + { + var found = Value.FirstOrDefault(x => IsSameRole(x, role)); + if (isSelected) + { + if (found == null) + Value.Add(new () { Role = role.Name, Db = role.Db}); + } + else + { + if (found != null) + Value.Remove(found); + } + } + + private string GetIcon(RoleInDbModel role) + { + var found = Value.FirstOrDefault(x => IsSameRole(x, role)); + return found != null ? "icon-checkmark-sm" : ""; + } + + private void OnToggleClicked(RoleInDbModel role) + { + var found = Value.FirstOrDefault(x => IsSameRole(x, role)); + // Add logic to toggle the selection state of the role + if (found != null) + { + // Role is selected, remove it + Value.Remove(found); + } + else + { + // Role is not selected, add it + Value.Add(new () { Role = role.Role, Db = role.Db }); + } + + InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Components/TransformJobControl.razor b/Rms.Risk.Mango/Components/TransformJobControl.razor new file mode 100644 index 0000000..57fccc8 --- /dev/null +++ b/Rms.Risk.Mango/Components/TransformJobControl.razor @@ -0,0 +1,285 @@ +@using Rms.Risk.Mango.Services.Context + +@inject IUserSession UserSession +@inject IDatabaseConfigurationService DatabaseConfig + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + + +
+
+
+
+ +
+ @if( UserSession.IsInstanceSelectionAllowed(SourceDatabase) ) + { +
+ +
+ } +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
@JobDescription
+
+ + +
+ +@code { + [CascadingParameter] public BlazoredModalInstance ModalInstance { get; set; } = null!; + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + private string SourceDatabase + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(SourceDatabaseChanged); + } + } = ""; + + private string SourceDatabaseInstance + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(SourceDatabaseChanged); + } + } = ""; + + private string SourceCollection + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(StateHasChanged); + } + } = ""; + + private string DestCollection + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field)) + InvokeAsync(StateHasChanged); + } + } = ""; + + private string Find { get; set; } = "{ _id : { $ne : \"\" } }"; + private string Transform { get; set; } = ""; + + private string DestDatabase => UserSession.Database; + private bool WipeDestination { get; set; } + private string BatchSize { get; set; } = "1000"; + private bool Upsert { get; set; } + private bool IsReady { get; set; } + + private bool IsReadyToRun => IsReady + && !string.IsNullOrWhiteSpace(SourceCollection) + && !string.IsNullOrWhiteSpace(EffectiveDestCollection) + && (SourceDatabase != DestDatabase || SourceCollection != EffectiveDestCollection) + ; + + private string Error { get; set; } = ""; + private EditContext EditContext => _editContext!; + + private string EffectiveDestCollection => string.IsNullOrWhiteSpace(DestCollection) ? SourceCollection : DestCollection; + + private Task SourceDatabaseChanged() + { + _ = Task.Run(LoadCollections); + return Task.CompletedTask; + } + + private List SourceDatabases => DatabaseConfig.Databases.Keys + .OrderBy(x => x) + .ToList() + ; + + //TODO: load on change + private List SourceDatabaseInstances => []; + + private MarkupString JobDescription => new( + $"Copy collection {SourceCollection} from {SourceDatabase} to {EffectiveDestCollection} of {DestDatabase}." + + (!string.IsNullOrWhiteSpace(Transform) ? "
Applying documents transformation." : "
Without any documents transformation.") + + (WipeDestination ? "
Destination collections will be cleared first." : "") + + (Upsert ? "
Existing documents will be updated." : "") + ); + + + private EditContext? _editContext; + + protected override void OnInitialized() + { + _editContext = new(this); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (SourceDatabases.Count == 0) + return; + + SourceDatabase = SourceDatabases.First(); + + StateHasChanged(); + } + + private async Task LoadCollections() + { + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + if (string.IsNullOrWhiteSpace(SourceDatabase)) + return; + + var admin = UserSession.GetCustomAdmin(SourceDatabase, SourceDatabaseInstance); + + SourceCollections = await admin.ListCollections(); + SourceCollection = SourceCollections.FirstOrDefault() ?? ""; + } + catch (Exception ex) + { + await Display(ex); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private IReadOnlyCollection SourceCollections { get; set; } = []; + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + + private async Task OnOK() + { + try + { + var job = new MigrationJob() + { + SourceDatabase = SourceDatabase, + DestinationDatabase = DestDatabase, + Email = UserSession.User.GetEmail(), + Upsert = Upsert, + ClearDestinationBefore = WipeDestination, + BatchSize = int.Parse(BatchSize), + Status = + [ + new MigrationJob.CollectionJob + { + SourceCollection = SourceCollection, + DestinationCollection = EffectiveDestCollection, + Filter = BsonDocument.Parse(Find), + Projection = string.IsNullOrWhiteSpace(Transform) ? null : BsonDocument.Parse(Transform) + } + ] + }; + + await ModalInstance.CloseAsync(ModalResult.Ok(job)); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e); + } + } + + public static Task ShowDialog(IModalService service) + { + var parameters = new ModalParameters + { + }; + + var options = new ModalOptions + { + HideCloseButton = false, + DisableBackgroundCancel = true + }; + + var form = service.Show("New transformation", parameters, options); + return form.Result; + } + +} diff --git a/Rms.Risk.Mango/Components/UserEditControl.razor b/Rms.Risk.Mango/Components/UserEditControl.razor new file mode 100644 index 0000000..6d82c8f --- /dev/null +++ b/Rms.Risk.Mango/Components/UserEditControl.razor @@ -0,0 +1,167 @@ +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+
+ + @if (Value.IsBuiltin || string.IsNullOrWhiteSpace(Value.Db)) + { + + } + else + { + + } +
+
+ +
+ +
+ +
+ +
+
+ + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string Name { get; set; } = $"newRole{++_count}"; + + [Parameter] + public UserInfoModel Value + { + get => field; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = new(); + + [Parameter] + public List AllRoles + { + get; + set + { + if (field == value) + return; + field = value; + UpdateSelectable(); + } + } = []; + + [Parameter] public bool IsEnabled { get; set; } = true; + [Parameter] public bool IsNew { get; set; } + [Parameter] public List AllDatabases { get; set; } = []; + + private static int _count; + private EditContext EditContext => _editContext!; + + private EditContext? _editContext; + + public class SelectableString(string name, bool isSelected, Action update) + { + public override string ToString() => $"[{(IsSelected ? "X" : " ")}] {Name} {Db}"; + + public string Name { get; } = name; + public string Db { get; init; } = string.Empty; + + public bool IsSelected + { + get; + set + { + if (field == value) + return; + field = value; + update(this, field); + } + } = isSelected; + } + + private List SelectableRoles { get; set; } = []; + + protected override void OnInitialized() + { + _editContext = new(this); + } + + private void UpdateSelectable() + { + SelectableRoles = AllRoles + .Select(x => + new SelectableString(x.Role, Value.Roles.Any(y => IsSameRole(x, y)),UpdateSelectedRoles) + { + Db = x.Db + } + ) + .ToList(); + + InvokeAsync(StateHasChanged); + } + + private static bool IsSameRole(RoleInDbModel x, RoleInDbModel y) => + x.Role == y.Role + && (x.Db == y.Db || string.IsNullOrWhiteSpace(x.Db) || string.IsNullOrWhiteSpace(y.Db)); + + private static bool IsSameRole(RoleInDbModel x, SelectableString y) => + x.Role == y.Name + && (x.Db == y.Db || string.IsNullOrWhiteSpace(x.Db) || string.IsNullOrWhiteSpace(y.Db)); + + + private void UpdateSelectedRoles(SelectableString role, bool isSelected) + { + var found = Value.Roles.FirstOrDefault(x => IsSameRole(x, role)); + if (isSelected) + { + if (found == null) + Value.Roles.Add(new () { Role = role.Name, Db = role.Db}); + } + else + { + if (found != null) + Value.Roles.Remove(found); + } + } + +} diff --git a/Rms.Risk.Mango/Controllers/AfhController.cs b/Rms.Risk.Mango/Controllers/AfhController.cs new file mode 100644 index 0000000..a168c26 --- /dev/null +++ b/Rms.Risk.Mango/Controllers/AfhController.cs @@ -0,0 +1,83 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Rms.Risk.Mango.Language; + + +namespace Rms.Risk.Mango.Controllers +{ + [AllowAnonymous] + [Route("api/[controller]")] + [ApiController] + public class AfhController : ControllerBase + { + [AllowAnonymous] + [HttpPost("from-json-to-script")] + [Produces("text/plain")] + [ProducesErrorResponseType(typeof(JsonObject))] + [ProducesResponseType(200)] + [Consumes("application/json")] + public string FromJsonToScript([FromBody] JsonArray json) + { + var ast = LanguageParser.ParseAggregationJsonToAST("",json); + var text = ast.AsText(); + return text; + } + + [AllowAnonymous] + [HttpPost("from-script-to-json")] + [Consumes("text/plain")] + [ProducesErrorResponseType(typeof(JsonObject))] + [ProducesResponseType(200)] + public async Task FromScriptToJson() + { + string script; + using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) + { + script = await reader.ReadToEndAsync(); + } + + var ast = LanguageParser.ParseScriptToAST(script); + var json = ast.AsJson(); + return (JsonArray)json!; + } + + [AllowAnonymous] + [HttpPost("format")] + [Consumes("text/plain")] + [Produces("text/plain")] + [ProducesErrorResponseType(typeof(JsonObject))] + [ProducesResponseType(200)] + public async Task Format() + { + string script; + using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) + { + script = await reader.ReadToEndAsync(); + } + + var ast = LanguageParser.ParseScriptToAST(script); + var text = ast.AsText(); + return text; + } + } +} diff --git a/Rms.Risk.Mango/Controllers/DownloadController.cs b/Rms.Risk.Mango/Controllers/DownloadController.cs new file mode 100644 index 0000000..c0bff4a --- /dev/null +++ b/Rms.Risk.Mango/Controllers/DownloadController.cs @@ -0,0 +1,118 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.AspNetCore.Authorization; +using Rms.Service.Bootstrap.Security; +using Rms.Risk.Mango.Services; + +namespace Rms.Risk.Mango.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class DownloadController( + ISingleUseTokenService _singleUseTokenService, + IPasswordManager _passwordManager + ) : ControllerBase +{ + private const string DownloadDataUrl = "data/[TOKEN]/[SECRET]?fileName=[FILE_NAME]"; + + public static async Task GetDownloadLink( + ITempFileStorage storage, + IPasswordManager passwordManager, + ISingleUseTokenService singleUseTokenService, + Func writeDataToFile, + string destFileName + ) + { + var fileName = storage.GetTempFileName("CsvData"); + await writeDataToFile(fileName); + + return GetDownloadLink(passwordManager, singleUseTokenService, fileName, destFileName); + } + + public static string GetDownloadLink( + IPasswordManager passwordManager, + ISingleUseTokenService singleUseTokenService, + string srcFilePath, + string destFileName + ) + { + var fileName = srcFilePath; + + var secret = passwordManager.EncryptPassword(fileName); + var url = DownloadDataUrl + .Replace("[TOKEN]", singleUseTokenService.GetSingleUseToken()) + .Replace("[SECRET]", Uri.EscapeDataString(secret)) + .Replace("[FILE_NAME]", Uri.EscapeDataString(destFileName)) + ; + + return $"api/download/{url}"; + } + + + public class TempFileStreamResult(string fileName, string mime) : PhysicalFileResult(fileName, mime) + { + public required string TempFileName { get; init; } + + public override async Task ExecuteResultAsync(ActionContext context) + { + try { + await base.ExecuteResultAsync(context); + } + finally { + System.IO.File.Delete(TempFileName); + } + } + } + + [HttpGet( "data/{token}/{secret}" )] + public Task GetCsv( + [FromRoute] string token, + [FromRoute] string secret, + [FromQuery] string? fileName + ) + { + CheckToken( token ); + + var tempFileName = _passwordManager.DecryptPassword(Uri.UnescapeDataString(secret)); + if ( !System.IO.File.Exists(tempFileName) ) + throw new ApplicationException("Invalid secret"); + + if ( !new FileExtensionContentTypeProvider().TryGetContentType(fileName ?? "data.bin", out var contentType) ) + contentType = "application/octet-stream"; + + return Task.FromResult((PhysicalFileResult)new TempFileStreamResult( tempFileName, contentType ) + { + FileDownloadName = fileName ?? "data.csv", + TempFileName = tempFileName + }); + } + + private void CheckToken( string token ) + { + if ( _singleUseTokenService.CheckSingleUseToken( token ) ) + return; + + throw new ApplicationException( "Download token is invalid or expired" ); + } + + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/DbMangoSettings.cs b/Rms.Risk.Mango/DbMangoSettings.cs new file mode 100644 index 0000000..10146c6 --- /dev/null +++ b/Rms.Risk.Mango/DbMangoSettings.cs @@ -0,0 +1,41 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango; + +public class DbMangoSettings +{ + public OracleConnectionSettings OracleConnectionSettings { get; set; } = new(); + public TimeSpan? DefaultTimeout { get; set; } + public int HierarchyId { get; set; } = 8; + public string InstanceName { get; set; } = "dbMango"; + public bool AuditLogsInOracle { get; set; } + public string Initial { get; set; } = "System"; + public int AuditExpireDays { get; set; } = 365; + public MongoDbSettings Settings { get; set; } = new(); + public bool EnableDatabaseOverrides { get; set; } + public string? MongoDbDocUrl { get; set; } + public string? MongoDbDocProxyUrl { get; set; } + public string RequestAccessURL { get; set; } = ""; + public string RequestAccessLabel { get; set; } = ""; + public string SupportLinkLabel { get; set; } = ""; + public string SupportLinkUrl { get; set; } = ""; +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/Admin/AdminCommands.razor b/Rms.Risk.Mango/Pages/Admin/AdminCommands.razor new file mode 100644 index 0000000..36edf25 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/AdminCommands.razor @@ -0,0 +1,405 @@ +@page "/admin/commands" +@page "/admin/commands/{DatabaseStr}/{CollectionStr}" +@page "/admin/commands/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] + +@using System.Reflection +@using Rms.Risk.Mango.Components.Commands + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Commands

+ + + + +
+ + + +
+ + +
+
+ +
+ + + @foreach (var (group, controls) in _availableCommands) + { + + @foreach (var (name, control) in controls) + { + var parameters = new Dictionary() + { + ["Collection"] = SelectedCollection, + ["Parameters"] = _commandParams[name] + }; + + + + + } + } + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + Error = ""; + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string SelectedCommand + { + get; + set + { + if (field == value || value == null || !_commandParams.ContainsKey(value)) + return; + field = value; + Error = ""; + InvokeAsync(StateHasChanged); + } + } = "Statistics"; + + private static string[] _groupsOrder = + [ + "Informational", + "Admin", + "Collections", + "Indexes", + "Documents" + ]; + + + private string Timeout { get; set; } = "20"; + private string Error { get; set; } = ""; + + private bool IsReady { get; set; } + private bool IsReadyToRun => IsReady && CanExecute && !string.IsNullOrWhiteSpace(CommandJson); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + private string CommandName => _commandParams[SelectedCommand].Name; + private string CommandJson => _commandParams[SelectedCommand].CommandJson; + private bool CanExecute => _commandParams[SelectedCommand].CanExecute; + private bool NeedConfirmation => _commandParams[SelectedCommand].NeedConfirmation; + private CmdBase.CommandParams.DatabaseConnectionType RequiredConnectionType => _commandParams[SelectedCommand].RequiredConnectionType; + + private static readonly List<(string Group, List<(string Name, Type Control)> Controls)> _availableCommands; + private Dictionary _commandParams = null!; + private readonly Dictionary _shardConnectionStrings = []; + + private string Shard { get; set; } = ""; + private IReadOnlyCollection Shards => _shardConnectionStrings.Keys.OrderBy(x => x).ToList(); + + private List Result + { + get => _commandParams[SelectedCommand].Result; + set => _commandParams[SelectedCommand].Result = value; + } + + static AdminCommands() + { + var types = typeof(AdminCommands).Assembly.GetTypes() + .Select(x => + { + var attr = x.GetCustomAttribute(typeof(MongoCommandAttribute)) as MongoCommandAttribute; + return (Type: x, Attr: attr); + }) + .Where(x => x.Attr != null) + .GroupBy(x => x.Attr!.Group) + .Select(x => (Group: x.Key, Commands: x.OrderBy(y => y.Attr!.Name).Select(y => (y.Attr!.Name, Control: y.Type) ).ToList())) + .Where(x => _groupsOrder.Contains(x.Group)) + .OrderBy(x => Array.IndexOf(_groupsOrder, x.Group)) + .ToList() + ; + + _availableCommands = types; + } + + protected override void OnInitialized() + { + _editContext = new(this); + + _commandParams = new( + _availableCommands + .SelectMany(x => x.Controls) + .Select(x => new KeyValuePair(x.Name, new (x.Name, OnChanged)) + ) + ); + } + + private void OnChanged(CmdBase.CommandParams arg) + { + InvokeAsync(StateHasChanged); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + + SyncUrl(); + + _ = Task.Run(async () => + { + try + { + await Task.WhenAll( LoadCollections(), LoadShards() ); + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + SyncUrl(); + StateHasChanged(); + } + + private async Task LoadCollections() + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + } + + private async Task LoadShards() + { + var shards = await Shell.LoadShards(UserSession); + _shardConnectionStrings.Clear(); + foreach (var shard in shards) + { + _shardConnectionStrings.Add(shard.Key, shard.Value); + } + } + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(List res) + { + Result = res; + return InvokeAsync(StateHasChanged); + } + + private async Task> RunCommand(BsonDocument doc, CancellationToken token) + { + var service = RequiredConnectionType switch + { + CmdBase.CommandParams.DatabaseConnectionType.Admin => UserSession.MongoDbAdminForAdminDatabase, + CmdBase.CommandParams.DatabaseConnectionType.Cluster => UserSession.MongoDbAdmin, + CmdBase.CommandParams.DatabaseConnectionType.Shard => _shardConnectionStrings.Count == 0 + ? UserSession.MongoDbAdmin + : UserSession.GetShardConnection(_shardConnectionStrings[Shard ?? throw new("Shard is not selected")].Host, _shardConnectionStrings[Shard].Port), + _ => throw new($"Unknown connection type {RequiredConnectionType}") + }; + + var result = await service.RunCommand(doc, token); + return [result]; + } + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/commands/{Database}"; + if ( !string.IsNullOrWhiteSpace(DatabaseInstance) ) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + + private async Task Execute() + { + Error = ""; + if (string.IsNullOrWhiteSpace(CommandJson)) + return; + + if (NeedConfirmation) + { + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Confirmation", $"Are you sure executing command \"{CommandName}\"? Some command's effects are irreversible!"); + if (!res.Confirmed) + return; + } + + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var doc = BsonDocument.Parse(CommandJson); + Shell.UpdateComment(doc, ticket, UserSession.User.GetEmail()); + + var res = await RunCommand(doc, cts.Token); + await Display(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + + } + + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Configuration.razor b/Rms.Risk.Mango/Pages/Admin/Configuration.razor new file mode 100644 index 0000000..e1f32ed --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Configuration.razor @@ -0,0 +1,150 @@ +@page "/admin/config" +@attribute [Authorize] + +@using Microsoft.Extensions.Options +@using Rms.Risk.Mango.Services.Context + +@* ReSharper disable once CSharpWarnings::CS8669 *@ +@inject IDatabaseConfigurationStorage? Storage +@inject IUserService UserService +@inject IConfiguration Config +@inject IOptions Settings +@inject IUserSession UserSession + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Configuration

+ + + + @if (Settings.Value.EnableDatabaseOverrides && Storage != null ) + { + + + + } + else + { +
+ Database is not enabled as a source for configuration parameters overrides in app configuration.
+ Set DbMangoSettings:EnableDatabaseOverrides to true. +
+ } + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + private List _data = []; + private List _possibleNames = []; + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender || Storage == null) + return; + + var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + var overrides = await Storage.GetConfiguration(UserService.GetEmail(), cts.Token); + _data = overrides.Select(o => new NameValueControl.NameValuePair + { + Name = o.Key, + Value = o.Value + }).ToList(); + + _possibleNames = GetConfigSections(Config); + StateHasChanged(); // Refresh the UI after loading new configuration + } + + private List GetConfigSections(IConfiguration config, string parentKey = "") + { + var sections = new List(); + foreach (var section in config.GetChildren()) + { + var fullKey = string.IsNullOrEmpty(parentKey) ? section.Key : $"{parentKey}:{section.Key}"; + sections.Add(fullKey); + sections.AddRange(GetConfigSections(section, fullKey)); + } + return sections; + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task Save() + { + if ( Storage == null ) + return; + + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + + try + { + // it's safer to build it this way as there can be duplicate names in the user input + var dict = new Dictionary(); + foreach (var pair in _data) + { + dict[pair.Name] = pair.Value; + } + var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + await Storage.UpdateConfiguration(dict, UserService.GetEmail(), cts.Token); + + await ModalDialogUtils.ShowInfoDialog(Modal, "Success", "The configuration has been successfully saved."); + + // exception in Audit() call can lead to 2 dialogs shown in total: Success in config change + error recording audit + try + { + await Audit(ticket); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + } + catch (Exception ex) + { + try + { + await Audit(ticket, ex); + } + catch (Exception ex2) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex2); + } + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + + private async Task Audit(string ticket, Exception? exception = null) + { + var doc = new BsonDocument + { + ["dbMangoConfigUpdate"] = exception?.Message ?? "Configuration updated" + }; + + var auditRecord = new AuditRecord(UserSession.Database, DateTime.UtcNow, UserService.GetEmail(), ticket, exception == null, doc, exception?.Message); + await UserSession.Audit.Record(auditRecord); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/DbStats.razor b/Rms.Risk.Mango/Pages/Admin/DbStats.razor new file mode 100644 index 0000000..b36f762 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/DbStats.razor @@ -0,0 +1,319 @@ +@page "/admin/db-stats" +@page "/admin/db-stats/{DatabaseStr}" +@page "/admin/db-stats/{DatabaseStr}/{DatabaseInstanceStr}" +@using ChartJs.Blazor +@using ChartJs.Blazor.BarChart +@using ChartJs.Blazor.BarChart.Axes +@using ChartJs.Blazor.Common.Axes +@using ChartJs.Blazor.Common.Enums +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@using Rms.Risk.Mango.Services.Models +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Storage size

+ + + @if (Error != null) + { + + } + +
+
+ +
+
+
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private Exception? Error { get; set; } + private DatabaseStatsModel Result { get; set; } = new(); + + private string ChartClass => Result.Raw.Count == 0 || Error != null + ? "d-none" + : "" + ; + + private static readonly string[] _colors = [ + "rgba(255, 99, 132, 0.6)", + "rgba(255, 159, 64, 0.6)", + "rgba(255, 205, 86, 0.6)", + "rgba(75, 192, 192, 0.6)", + "rgba(54, 162, 235, 0.6)", + // "rgba(153, 102, 255, 0.4)", + // "rgba(201, 203, 207, 0.4)" + ]; + + private string _unit = "B"; + private double _divider = 1.0; + + private readonly BarConfig _chartConfig = new() + { + Data = + { + Datasets = + { + new BarDataset + { + Label = "Index used", + BackgroundColor = _colors[2], + BorderColor = _colors[2] + }, + new BarDataset + { + Label = "Index free", + BackgroundColor = _colors[3], + BorderColor = _colors[3] + }, + new BarDataset + { + Label = "Storage used", + BackgroundColor = _colors[0], + BorderColor = _colors[0] + }, + new BarDataset + { + Label = "Storage free", + BackgroundColor = _colors[1], + BorderColor = _colors[1] + }, + // new BarDataset + // { + // Label = "Other free", + // BackgroundColor = _colors[4], + // BorderColor = _colors[4] + // } + } + }, + Options = new() + { + MaintainAspectRatio = true, + Responsive = true, + Legend = new() + { + Display = true, + Position = Position.Bottom + }, + Scales = new() + { + YAxes = new List + { + new BarLinearCartesianAxis + { + Stacked = true, + // OffsetGridLines = false, + // Offset = true, + // Display = AxisDisplay.False + } + }, + XAxes = new List + { + new BarCategoryAxis + { + Stacked = true, + // OffsetGridLines = false, + // Offset = false, + // Display = AxisDisplay.False, + // BarThickness = 45 + } + } + } + } + }; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + StateHasChanged(); + Task.Run(Run); + } + + private async Task Run() + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + await InvokeAsync(StateHasChanged); + + var res = await UserSession.MongoDbAdmin.DbStats(cts.Token); + await Display(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + await InvokeAsync(StateHasChanged); + } + } + + private void UpdateChart( + DatabaseStatsModel data + ) + { + _chartConfig.Data.XLabels.Clear(); + + foreach (var key in data.Raw.Keys) + { + var parts = key.Split('/'); + _chartConfig.Data.XLabels.Add(parts[0]); // replica set name + } + + var dataMax = data.Raw.Values.Max(x => x.StorageSize+x.IndexSize); + + // Decide unit + if (dataMax > 1024 * 1024 * 1024) // > 1 GB + { + _unit = "GB"; + _divider = 1024 * 1024 * 1024; + } + else + { + _unit = "MB"; + _divider = 1024 * 1024; + } + + var indexUsed = (BarDataset)_chartConfig.Data.Datasets[0]; + var indexFree = (BarDataset)_chartConfig.Data.Datasets[1]; + var storageUsed = (BarDataset)_chartConfig.Data.Datasets[2]; + var storageFree = (BarDataset)_chartConfig.Data.Datasets[3]; + // var otherFree = (BarDataset)_chartConfig.Data.Datasets[4]; + + storageUsed.Clear(); + storageFree.Clear(); + indexUsed .Clear(); + indexFree .Clear(); + // otherFree .Clear(); + + storageUsed.AddRange(data.Raw.Values.Select(x => (x.StorageSize - x.FreeStorageSize) / _divider)); + storageFree.AddRange(data.Raw.Values.Select(x => x.FreeStorageSize / _divider)); + indexUsed .AddRange(data.Raw.Values.Select(x => (x.IndexSize - x.IndexFreeStorageSize) / _divider)); + indexFree .AddRange(data.Raw.Values.Select(x => x.IndexFreeStorageSize / _divider)); + // otherFree.AddRange(data.Raw.Values.Select(x => (x.FsUsedSize - x.StorageSize - x.IndexSize) / _divider)); + + _chartConfig.Data.Datasets.Add(storageUsed); + _chartConfig.Data.Datasets.Add(storageFree); + _chartConfig.Data.Datasets.Add(indexUsed ); + _chartConfig.Data.Datasets.Add(indexFree ); + // _chartConfig.Data.Datasets.Add(otherFree ); + + + _chartConfig.Options.Scales.YAxes.Clear(); + _chartConfig.Options.Scales.YAxes.Add(new BarLinearCartesianAxis + { + Stacked = true, + ScaleLabel = new () + { + Display = true, + LabelString = $"Size {_unit}" + }, + Ticks = new () + { + Min = 0.0, + Max = dataMax / _divider, + } + }); + } + + private Task Display(DatabaseStatsModel model) + { + Error = null; + Result = model; + + UpdateChart(model); + + return InvokeAsync(StateHasChanged); + } + + + private Task Display(Exception e) + { + Error = e; + Result.Raw.Clear(); + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/db-stats/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Download.razor b/Rms.Risk.Mango/Pages/Admin/Download.razor new file mode 100644 index 0000000..4691d1f --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Download.razor @@ -0,0 +1,485 @@ +@page "/admin/download" +@page "/admin/download/{DatabaseStr}" +@page "/admin/download/{DatabaseStr}/{DatabaseInstanceStr}" + +@implements IDisposable + +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IMigrationEngine MigrationEngine + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Download

+ + + + +
+ + +
+
+ + + +
+
+ + +
+ @if ( item.Data == null ) // index label + { +
@item.Label
+ } + else if (item.Data.Collection != null ) // collection node + { + + if (item.Data.Collection.Name.EndsWith("-Meta")) + { +
@item.Label
+ } + else + { +
@item.Label
+ } + } +
+
+
+
+
+
+ +
+
+
+ + +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } + + @if ( SelectedJob.Complete && !string.IsNullOrWhiteSpace(SelectedJob.DownloadUrl) ) + { + Download results + } +
+
+
+
+ +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + public class IndexTreeItem + { + public DatabaseStructureLoader.CollectionStructure? Collection { get; set; } + public bool IsSelected { get; set; } + } + + private bool IsReady { get; set; } + private bool IsReadyToCancel => IsReady && !SelectedJob.Complete; + private string Error { get; set; } = ""; + private List RunningJobs { get; set; } = []; + + private CancellationTokenSource _cts = new(); + + private readonly List> _currentNodes = + [ + new () + { + Label = "Collections", + Children = [] + } + ]; + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private MigrationJob SelectedJob + { + get; + set + { + if (field.JobId == value.JobId) + return; + field = value; + StateHasChanged(); + } + } = new() { Complete = true }; + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + + try + { + RunningJobs = MigrationEngine.List().Where(x => x.Type == MigrationJob.JobType.Download).ToList(); + if (RunningJobs.Count > 0) + SelectedJob = RunningJobs[0]; + + _ = Task.Run(() => RefreshLoop(_cts.Token), _cts.Token); + + var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + + var (_, root) = await LoadStructure(UserSession.MongoDbAdmin, UserSession.MongoDbAdminForAdminDatabase, cts2.Token); + _currentNodes.Clear(); + _currentNodes.Add(root); + + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error loading structure for {Database}", ex); + return; + } + + IsReady = true; + StateHasChanged(); + } + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + + private async Task RefreshLoop(CancellationToken token) + { + // percents are always increasing, so it safe to compare sum() + var prevState = -1.0; + var prevJobId = SelectedJob.JobId; + var oldComplete = false; + + while (!token.IsCancellationRequested) + { + if (prevJobId != SelectedJob.JobId) + { + prevJobId = SelectedJob.JobId; + prevState = -1.0; + oldComplete = false; + await InvokeAsync(StateHasChanged); + } + else + { + var newState = SelectedJob.Status.Sum(x => x.Count + x.Copied + (x.Cleared ?? 0)); + + if (SelectedJob.Complete != oldComplete || Math.Abs(newState - prevState) > 0.01) + { + prevState = newState; + oldComplete = SelectedJob.Complete; + await InvokeAsync(StateHasChanged); + } + } + + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + } + + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/download/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + public static async Task<(List, TreeNode)> LoadStructure(IMongoDbDatabaseAdminService db, IMongoDbDatabaseAdminService admin, CancellationToken token) + { + var root = new TreeNode + { + Label = "Collections" + }; + + var collStructure = await DatabaseStructureLoader.LoadCollections(db, admin, token); + + var collections = (collStructure) + .Select(x => + new + { + Group = + x.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) + ? " Meta" + : x.Name.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) + ? " Cache" + : " Data", + Value = x + } + ) + .ToList(); + + var allCollections = collections + .OrderBy(x => x.Value.Name) + .Select( x => x.Value ) + .ToList(); + + // Add collections to the root node + var collectionNodes = collections + .GroupBy(x => x.Group) + .OrderBy(x => x.Key) + .Select(gr => new TreeNode + { + Label = gr.Key, + IsExpanded = true, + Children = gr + .OrderBy(x => x.Value.Name) + .Select(coll => new TreeNode + { + Label = coll.Value.Name, + IsExpanded = false, + Data = new() + { + Collection = coll.Value, + } + }) + .ToList() + }) + .ToList(); + + root.Children = collectionNodes; + root.IsExpanded = true; + Set(root, false); + + foreach( var node in root.Children) + Set(node, true, x => x.Data?.Collection?.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ?? false); + + return (collStructure, root); + } + + private static void Set(TreeNode item, bool value, Func, bool>? filter = null ) + { + filter ??= _ => true; + + if (item.Data != null && filter(item)) + { + item.Data.IsSelected = value; + } + foreach (var child in item.Children) + { + Set(child, value, filter); + } + } + + private Task OnSelectAllCurrent() + { + foreach( var node in _currentNodes) + Set(node, true); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectAllMeta() + { + foreach( var node in _currentNodes) + Set(node, true, x => x.Data?.Collection?.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ?? false); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectNoneCurrent() + { + foreach( var node in _currentNodes) + Set(node, false); + return InvokeAsync(StateHasChanged); + } + + private Task SetSourceItem(TreeNode item, bool b) + { + Set(item, b); + return InvokeAsync(StateHasChanged); + } + + private async Task OnDownload() + { + var newJob = new MigrationJob + { + Type = MigrationJob.JobType.Download, + SourceDatabase = UserSession.Database, + SourceDatabaseInstance = UserSession.DatabaseInstance, + DestinationDatabase = UserSession.Database, + DestinationDatabaseInstance = UserSession.DatabaseInstance!, + Email = UserSession.User.GetEmail(), + Upsert = false, + ClearDestinationBefore = false, + BatchSize = 1, + Status = _currentNodes[0].Children + .Where(x => (x.Data?.IsSelected ?? false) && x.Data.Collection != null) + .Select(x => new MigrationJob.CollectionJob + { + SourceCollection = x.Data!.Collection!.Name, + DestinationCollection = x.Data!.Collection!.Name, + }) + .ToList() + + }; + + if (newJob.Status.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Download data", "Please, select at least one collection."); + return; + } + + var info = GetInfo(newJob); + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Download data", + $"Are you sure to start downloading of {newJob.Status.Count} collection(s) from {newJob.SourceDatabase}?", + info + ); + + if (res.Cancelled) + return; + + await RegisterNewJob(newJob); + } + + private async Task CancelJob() + { + if (SelectedJob.Complete) + return; + + var info = GetInfo(SelectedJob); + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Migration", + $"Are you sure to cancel migration of {SelectedJob.Status.Count} collection(s) from {SelectedJob.SourceDatabase} to {SelectedJob.DestinationDatabase}?", + info + ); + if (res.Cancelled) + return; + + await MigrationEngine.Cancel(SelectedJob, UserSession.User.GetUser()); + await InvokeAsync(StateHasChanged); + } + + private Dictionary GetInfo(MigrationJob job) + { + var info = new Dictionary + { + ["from"] = job.SourceDatabase, + ["batchSize"] = job.BatchSize.ToString(), + }; + return info; + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task RegisterNewJob(MigrationJob newJob) + { + try + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + newJob.Ticket = ticket; + + await MigrationEngine.Add(newJob, UserSession.User.GetUser()); + SelectedJob = newJob; + RunningJobs = MigrationEngine.List(); + } + catch (Exception e) + { + Error = e.ToString(); + } + + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Encrypt.razor b/Rms.Risk.Mango/Pages/Admin/Encrypt.razor new file mode 100644 index 0000000..7c0f982 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Encrypt.razor @@ -0,0 +1,43 @@ +@page "/admin/encrypt" + +@attribute [Authorize] + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +Encrypt + +

Encrypt

+ + + + + + diff --git a/Rms.Risk.Mango/Pages/Admin/ListCommands.razor b/Rms.Risk.Mango/Pages/Admin/ListCommands.razor new file mode 100644 index 0000000..26d66e3 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/ListCommands.razor @@ -0,0 +1,168 @@ +@page "/admin/list-commands" +@page "/admin/list-commands/{DatabaseStr}" +@page "/admin/list-commands/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject ICommandListService CommandListService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Commands list

+ + + @if (!string.IsNullOrWhiteSpace(Error)) + { + + } + else + { + + + + + + + + + + + + + + + + } + + +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private string? Error { get; set; } + private List Result { get; } = []; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + StateHasChanged(); + Task.Run(Run); + } + + private async Task Run() + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + await InvokeAsync(StateHasChanged); + + var res = await CommandListService.GetCommands(UserSession.MongoDbAdminForAdminDatabase, cts.Token); + Result.AddRange(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + await InvokeAsync(StateHasChanged); + } + } + + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/list-commands/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private string GetCommandLink(string name) + => $"/admin/shell/{Database}/{DatabaseInstance}/{name}"; + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Migrate.razor b/Rms.Risk.Mango/Pages/Admin/Migrate.razor new file mode 100644 index 0000000..7ccbf02 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Migrate.razor @@ -0,0 +1,355 @@ +@page "/admin/migrate" +@page "/admin/migrate/{DatabaseStr}" +@page "/admin/migrate/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] + +@implements IDisposable + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IMigrationEngine MigrationEngine + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Migrate data to @Database

+ + + + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } + +
+

Explanation of Columns

+
    +
  • + ReadDPS (Read Documents Per Second): +

    This column shows the rate at which documents are being read from the source database. It is a single-threaded metric, reflecting the performance of a single thread responsible for reading documents.

    +
  • +
  • + WriteDPS (Write Documents Per Second): +

    This column shows the rate at which documents are being written to the destination database. Like ReadDPS, it is a single-threaded metric, representing the performance of a single thread responsible for writing documents.

    +
  • +
  • + DPS (Documents Per Second): +

    This column represents the effective multithreaded overall documents per second rate. It aggregates the performance of all threads involved in the migration process, providing a holistic view of the system's throughput.

    +
  • +
+

+ Key Difference: ReadDPS and WriteDPS focus on individual thread performance for reading and writing, respectively, while DPS reflects the total throughput of the migration process, leveraging multithreading to achieve higher performance. +

+
+
+
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private bool IsReady { get; set; } + private string Error { get; set; } = ""; + private List RunningJobs { get; set; } = []; + private EditContext EditContext => _editContext!; + private bool IsReadyToCancel => IsReady && !SelectedJob.Complete; + + private EditContext? _editContext; + private readonly CancellationTokenSource _refreshLoopCts = new(); + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private MigrationJob SelectedJob + { + get; + set + { + if (field.JobId == value.JobId) + return; + field = value; + StateHasChanged(); + } + } = new() { Complete = true }; + + protected override void OnInitialized() + { + _editContext = new(this); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + RunningJobs = MigrationEngine.List().Where(x => x.Type == MigrationJob.JobType.Copy).ToList(); + if (RunningJobs.Count > 0) + SelectedJob = RunningJobs[0]; + + IsReady = true; + + _ = Task.Run(() => RefreshLoop(_refreshLoopCts.Token), _refreshLoopCts.Token); + + SyncUrl(); + StateHasChanged(); + } + + private async Task RefreshLoop(CancellationToken token) + { + // percents are always increasing, so it safe to compare sum() + var prevState = -1.0; + var prevJobId = SelectedJob.JobId; + var oldComplete = false; + + while (!token.IsCancellationRequested) + { + if (prevJobId != SelectedJob.JobId) + { + prevJobId = SelectedJob.JobId; + prevState = -1.0; + oldComplete = false; + await InvokeAsync(StateHasChanged); + } + else + { + var newState = SelectedJob.Status.Sum(x => x.Count + x.Copied + (x.Cleared ?? 0)); + + if (SelectedJob.Complete != oldComplete || Math.Abs(newState - prevState) > 0.01) + { + prevState = newState; + oldComplete = SelectedJob.Complete; + await InvokeAsync(StateHasChanged); + } + } + + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + } + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/migrate/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task CancelJob() + { + if (SelectedJob.Complete) + return; + + var info = GetInfo(SelectedJob); + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Migration", + $"Are you sure to cancel migration of {SelectedJob.Status.Count} collection(s) from {SelectedJob.SourceDatabase} to {SelectedJob.DestinationDatabase}?", + info + ); + if (res.Cancelled) + return; + + await MigrationEngine.Cancel(SelectedJob, UserSession.User.GetUser()); + await InvokeAsync(StateHasChanged); + } + + private async Task NewMigration() + { + var res = await MigrationJobControl.ShowDialog(Modal); + if (res.Cancelled) + return; + + if (res.Data is not MigrationJob newJob) + return; + + var info = GetInfo(newJob); + + res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Migration", + $"Are you sure to start migration of {newJob.Status.Count} collection(s) from {newJob.SourceDatabase} to {newJob.DestinationDatabase}?", + info + ); + if (res.Cancelled) + return; + + await RegisterNewJob(newJob); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task RegisterNewJob(MigrationJob newJob) + { + try + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + newJob.Ticket = ticket; + + await MigrationEngine.Add(newJob, UserSession.User.GetUser()); + SelectedJob = newJob; + RunningJobs = MigrationEngine.List(); + } + catch (Exception e) + { + await Display(e); + } + + await InvokeAsync(StateHasChanged); + } + + private async Task NewTransformation() + { + var res = await TransformJobControl.ShowDialog(Modal); + if (res.Cancelled) + return; + + if (res.Data is not MigrationJob newJob) + return; + + var info = GetInfo(newJob); + + res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Transformation", + $"Are you sure to start transformation of {newJob.Status[0].SourceCollection} from {newJob.SourceDatabase} to {newJob.Status[0].DestinationCollection} of {newJob.DestinationDatabase}?", + info + ); + if (res.Cancelled) + return; + + await RegisterNewJob(newJob); + } + + private Dictionary GetInfo(MigrationJob job) + { + var info = new Dictionary + { + ["from"] = job.SourceDatabase, + ["to"] = job.DestinationDatabase, + ["upsert"] = job.Upsert.ToString(), + ["clearDestBefore"] = job.ClearDestinationBefore.ToString(), + ["batchSize"] = job.BatchSize.ToString(), + }; + return info; + } + + public void Dispose() + { + _refreshLoopCts.Cancel(); + _refreshLoopCts.Dispose(); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Onboarding.razor b/Rms.Risk.Mango/Pages/Admin/Onboarding.razor new file mode 100644 index 0000000..dab4393 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Onboarding.razor @@ -0,0 +1,101 @@ +@page "/admin/onboarding" +@using Microsoft.Extensions.Options +@using Rms.Risk.Mango.Services.Context +@attribute [Authorize] + +@inject IDatabaseConfigurationService DatabaseConfig +@inject IOptions Settings + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Onboarding

+ + + +
+ + + + + + + +
+ +
+
+ +
+ +@code { + + public class DisplayRecord + { + public string Name { get; set; } = ""; + public string Contacts => Value.Contacts; + public string MongoDbUrl => Value.Config.MongoDbUrl; + public string MongoDbDatabase => Value.Config.MongoDbDatabase; + public DatabasesConfig.DatabaseConfig Value { get; set; } = new(); + } + + private List _configs = []; + private DisplayRecord _selectedRecord = new(); + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return Task.CompletedTask; + + return Refresh(); + } + + private void OnSelectionChanged((dynamic Row, string Col) data) + { + _selectedRecord = (DisplayRecord)data.Row; + InvokeAsync(StateHasChanged); + } + + private async Task Refresh() + { + var oldSelected = _selectedRecord.Name; + + await DatabaseConfig.Reload(); + _configs = DatabaseConfig.Databases + .OrderBy(x => x.Key) + .Select(x => + new DisplayRecord + { + Name = x.Key , + Value = x.Value.Clone() + }) + .ToList(); + + _selectedRecord = _configs.FirstOrDefault(x => x.Name == oldSelected) + ?? _configs.First(); + + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Roles.razor b/Rms.Risk.Mango/Pages/Admin/Roles.razor new file mode 100644 index 0000000..a681f7c --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Roles.razor @@ -0,0 +1,468 @@ +@page "/admin/roles" +@page "/admin/roles/{DatabaseStr}" +@page "/admin/roles/{DatabaseStr}/{DatabaseInstanceStr}" +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IMongoDbServiceFactory MongoDbServiceFactory; + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Roles Management

+ + + @if (Error != null) + { + + } + + + +
+
+
+ + +
+ + + + +
+
@context.RoleName
+ @if (context.IsBuiltin) + { +
BuiltIn
+ } + else if (!string.IsNullOrWhiteSpace(context.Db)) + { +
@context.Db
+ } +
+
+
+
+
+
+ +
+
+
+ + + + +
+ + +
+
+
+
+ + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private Exception? Error { get; set; } + private bool IsReady { get; set; } + + private RoleInfoModel SelectedRole + { + get; + set + { + if ( field == value ) + return; + field = value; + + // Populate AllRoles with non-built-in roles from the same database and built-in roles paired with it. + AllRoles = RolesList.Where(x => !x.IsBuiltin && x.Db == SelectedRole.Db) + .Select(x => new RoleInDbModel + { + Role = x.RoleName, + Db = x.Db + } + ) + .Concat( + RolesList + .Where(x => x.IsBuiltin) + .Select(x => new RoleInDbModel + { + Role = x.RoleName, + Db = "" + }) + ) + .DistinctBy(x => $"{x.Db}, {x.Role}") + .OrderBy(x => $"{x.Db}, {x.Role}") + .ToList() + ; + + EditableSelectedRole = field.Clone(); + } + } = new(); + + private RoleInfoModel EditableSelectedRole { get; set; } = new(); + private List RolesList { get; set; } = []; + private bool IsNew => RolesList.All(x => x.RoleName != EditableSelectedRole.RoleName); + private List AllDatabases { get; set; } = []; + private List AllRoles { get; set; } = []; + private List AllActions { get; set; } = []; + private bool CanUpdate => !EditableSelectedRole.IsBuiltin && !string.IsNullOrWhiteSpace(EditableSelectedRole.Db); + private bool CanDelete => !EditableSelectedRole.IsBuiltin && !string.IsNullOrWhiteSpace(EditableSelectedRole.Db) && !IsNew; + + private bool IsSelectable(RoleInfoModel arg) => true; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + StateHasChanged(); + Task.Run(LoadRoles); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task Run(Func body) + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + await body(ticket, cts.Token); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private Task LoadRoles() + => Run(async (_, token) => + { + try + { + AllDatabases = (await UserSession.MongoDbAdminForAdminDatabase.ListDatabases(token)) + .Select(x => x.Name) + .ToList(); + } + catch (Exception) + { + AllDatabases = [UserSession.DatabaseInstance]; + } + + var adminTask = UserSession.MongoDbAdminForAdminDatabase.GetRolesInfo(showBuiltInRoles: true, token: token); + var customTasks = AllDatabases.Select(x => LoadRolesForDatabaseInstance(x, token)).ToList(); + + await Task.WhenAll(customTasks.Concat([adminTask])); + + var admin = await adminTask; + var all = new RolesInfoModel(); + all.Roles.AddRange(admin.Roles.Where(x => x.IsBuiltin)); + + foreach (var customTask in customTasks) + { + var custom = await customTask; + all.Roles.AddRange(custom.Roles); + } + await Display(all); + }); + + private Task LoadRolesForDatabaseInstance(string instanceName, CancellationToken token) + { + try + { + var db = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, instanceName); + return db.GetRolesInfo(showBuiltInRoles: false, token: token); + } + catch (Exception) + { + return Task.FromResult(new RolesInfoModel() ); + } + } + + + private Task Display(RolesInfoModel model) + { + Error = null; + + var oldRoleName = SelectedRole.RoleName; + + foreach (var role in model.Roles.Where(role => role.IsBuiltin || string.IsNullOrWhiteSpace(role.Db))) + { + role.Db = ""; + } + + RolesList = model.Roles + .OrderBy(x => $"{x.Db}, {x.RoleName}") + .ToList() + ; + + SelectedRole = !string.IsNullOrWhiteSpace(oldRoleName) + ? model.Roles.FirstOrDefault(x => x.RoleName == oldRoleName) ?? model.Roles.FirstOrDefault() ?? new() + : model.Roles.FirstOrDefault() ?? new RoleInfoModel() + ; + + AllActions = RolesList + .SelectMany(x => x.Privileges.SelectMany(y => y.Actions)) + .Distinct() + .OrderBy(x => x) + .ToList() + ; + + return InvokeAsync(StateHasChanged); + } + + + private Task Display(Exception e) + { + Error = e; + RolesList.Clear(); + AllRoles.Clear(); + AllActions.Clear(); + AddRole(); + + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/roles/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private static string GetRoleClass(RoleInfoModel r) => r.IsBuiltin ? "builtin" : ""; + + private static int _count; + + private Task AddRole() + { + SelectedRole = new () + { + RoleName = $"newRole{++_count}", + Db = UserSession.DatabaseInstance, + IsBuiltin = false + }; + + return InvokeAsync(StateHasChanged); + } + + private async Task DeleteRole() + { + if ( IsNew || string.IsNullOrWhiteSpace(SelectedRole.RoleName) || SelectedRole.IsBuiltin ) + { + await ModalDialogUtils.ShowInfoDialog( Modal, "Oops!", "Cannot delete the selected role."); + return; + } + + var rc = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Confirm Deletion", $"Are you sure you want to delete the role '{SelectedRole.RoleName}'?"); + if (!rc.Confirmed) + return; + + // Logic for deleting the selected role goes here + await Run(async (_, token) => + { + // Attention: use the admin service corresponding to the database you are going to delete role in! + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, SelectedRole.Db); + await service.DropRole(SelectedRole.RoleName, token); + await LoadRoles(); + }); + } + + private async Task UpdateRole() + { + var role = EditableSelectedRole; + var add = IsNew; + + if (role.IsBuiltin || string.IsNullOrWhiteSpace(role.Db)) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"Can't delete built-in role '{role.RoleName}'."); + return; + } + + + if (add && RolesList.Any(x => x.RoleName == role.RoleName && x.Db == role.Db )) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"Role '{role.RoleName}' already exists. Please choose a different name."); + return; + } + + var rc = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Warning", $"Are you sure you want to {(add ? "add" : "update")} the role '{role.RoleName}'?"); + if (!rc.Confirmed) + return; + + var command = role.CreateUpdateRoleCommand(add); + + // var json = command.ToJson( new () { Indent = true }); + // await ModalDialogUtils.ShowTextDialog(Modal, "Oops!", json, $"Role '{role.RoleName}'"); + + + // Proceed with updating or adding the role logic here + await Run(async (_, token) => + { + // Attention: use the admin service corresponding to the database you are going to create role in! + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, role.Db); + await service.RunCommand(command, token); + await LoadRoles(); + }); + } + + private async Task CopyRole() + { + try + { + var json = JsonUtils.ToJson(SelectedRole, new() { WriteIndented = true }); + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", json); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "Role copied to clipboard."); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + + } + + private async Task PasteRole() + { + try + { + var json = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + + var clone = JsonUtils.FromJson(json); + if (clone == null) + return; + + // Search for the existing role by name + var existingRole = RolesList.FirstOrDefault(x => x.RoleName == clone.RoleName); + if ( existingRole != null ) + { + if ( existingRole.IsBuiltin ) + { + // If the role is built-in, show a warning and do not overwrite + await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"Role '{clone.RoleName}' is a built-in role and cannot be changed."); + return; + } + + // If the role already exists, show a warning and do not overwrite + var rc = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Warning", $"Role '{clone.RoleName}' already exists. Please choose a different name."); + if (!rc.Confirmed) + return; + } + + SelectedRole = clone; + + await InvokeAsync(StateHasChanged); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", $"Role {SelectedRole.RoleName} was successfully parsed. You still need to save it!"); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Shell.razor b/Rms.Risk.Mango/Pages/Admin/Shell.razor new file mode 100644 index 0000000..bd2f880 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Shell.razor @@ -0,0 +1,692 @@ +@page "/admin/shell" +@page "/admin/shell/{DatabaseStr}" +@page "/admin/shell/{DatabaseStr}/{DatabaseInstanceStr}" +@page "/admin/shell/{DatabaseStr}/{DatabaseInstanceStr}/{Command}" +@attribute [Authorize] + +@using System.Text.RegularExpressions +@using Markdig +@using Markdown.ColorCode +@using Microsoft.Extensions.Options +@using MongoDB.Bson.Serialization +@using Rms.Risk.Mango.Components.JForms +@using Rms.Service.Bootstrap.Security + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IDocumentationService DocService +@inject IOptions Settings +@inject ICommandListService CommandListService +@inject IPasswordManager PasswordManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Shell, Command: @CommandName

+ + + + +
+ + + + @if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl)) + { + + } +
+
+ + + + + +
+
+ Documentation + Use encrypted sensitive data in form of [*...]. You can use JSON array to run multiple commands. +
+
+ +
+ +
+ + + +
+ + @if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl)) + { +
+                        @Doc
+                    
+ } +
+
+ +
+ @if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl) && ShowDoc) + { +
+ @Description +
+ } + else + { +
+                        @Result
+                    
+ } +
+
+
+
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? Command { get; set; } + + private string Text + { + get; + set + { + if (field == value) + return; + field = value; + CommandName = GetCommandName(field); + + if ( string.IsNullOrWhiteSpace(CommandName) || CommandName.Length < 4) + { + Doc = new(); + return; + } + + _ = Task.Run(async () => { await LoadDocumentation(); }); + } + } = + @"{ + ""ping"": 1 +}"; + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string Timeout { get; set; } = "20"; + private string FetchSize { get; set; } = "5000"; + private bool ShowDoc { get; set; } = true; + private MarkupString Result { get; set; } + private string ResultStr { get; set; } = ""; + private MarkupString Doc { get; set; } + private string Shard { get; set; } = "- cluster -"; + private bool IsReady { get; set; } + private MarkupString Description { get; set; } + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + private bool CanCopyUrl => JsonUtils.IsValidJson(Text); + private bool CanCopyResults => !string.IsNullOrWhiteSpace(ResultStr) && !ShowDoc; + + private readonly MarkdownPipeline _htmlPipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseColorCode() + .Build() + ; + + // ReSharper disable once NotAccessedPositionalProperty.Global + public record ShardProperties(string ReplicaSet, string Host, int Port); + + private readonly Dictionary _shardConnectionStrings = []; + private FormCodeEditor _codeEditor = null!; + + private IReadOnlyCollection Shards => new [] + {"- cluster -", "- admin -", "- config -"} + .Concat(_shardConnectionStrings.Keys.OrderBy(x => x)) + .ToList(); + + protected override void OnInitialized() + { + _editContext = new (this); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + ShowDoc = !string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl); + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if ( !string.IsNullOrWhiteSpace(Command) ) + { + if ( JsonUtils.IsValidJson(Command) ) + { + try + { + Text = JsonUtils.FormatJson(Command); + } + catch(Exception) + { + // ignore + } + } + else if ( Regex.IsMatch( Command.Trim(),@"^(\w|[0-9_])*$") ) + { + Text = $"{{\n \"{Command.Trim()}\" : 1\n}}"; + } + } + + SyncUrl(); + + Task.Run(LoadCommands); + Task.Run(LoadShards); + + IsReady = true; + StateHasChanged(); + } + + private async Task LoadCommands() + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var commands = await CommandListService.GetCommands(UserSession.MongoDbAdminForAdminDatabase, cts.Token); + await InvokeAsync(() => UpdateCommands(commands)); + } + + private async Task UpdateCommands(IReadOnlyCollection commands) + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.SetMongoDbCommands", [commands.Select(x => x.Name).ToArray()] ); + StateHasChanged(); + } + + public static async Task> LoadShards(IUserSession userSession) + { + try + { + if (!userSession.DatabaseConfig.AllowShardAccess) + return []; + + var listShards = BsonDocument.Parse(@"{ listShards : 1 }"); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var doc = await userSession.MongoDbAdminForAdminDatabase.RunCommand(listShards, cts.Token); + + var re = new Regex("(?[^/]*)/?(?[^:]+):(?[0-9]+)"); + + var shards = new Dictionary(); + foreach (var el in doc["shards"].AsBsonArray.Select( x => x.AsBsonDocument)) + { + var s = el["host"].AsString; + var m = re.Match(s); + if ( !m.Success) + continue; + + shards[el["_id"].AsString] = new( + m.Groups["RS"].Value, + m.Groups["HOST"].Value, + int.Parse(m.Groups["PORT"].Value) + ); + } + + return shards; + } + catch (Exception) + { + return []; + } + } + + private async Task LoadShards() + { + var shards = await LoadShards(UserSession); + _shardConnectionStrings.Clear(); + foreach (var shard in shards) + { + _shardConnectionStrings.Add(shard.Key, shard.Value); + } + await InvokeAsync(StateHasChanged); + } + + private static readonly Regex _commandRegex = new (@"\s*\{\s*""?(?[a-zA-Z]+)""?\s*\:.*"); + + private string? CommandName + { + get; + set + { + if (field == value) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } + + private static string? GetCommandName(string text) + { + var s = text[..Math.Min(text.Length, 30)]; + + var m = _commandRegex.Match(s); + if (!m.Success) + return null; + + var n = m.Groups["cmd"].Value; + return string.IsNullOrWhiteSpace(n) + ? null + : n; + } + + private async Task Run() + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + if (Text[..Math.Min(100, Text.Length)].Trim().StartsWith("[")) + { + await RunScript(ticket, cts.Token); + } + else + { + await RunSingle(ticket, cts.Token); + } + } + + private async Task RunSingle(string ticket, CancellationToken token) + { + + try + { + IsReady = false; + ShowDoc = false; + + await InvokeAsync(StateHasChanged); + + var clearText = DecryptPasswords(Text); + + var doc = BsonDocument.Parse(clearText); + UpdateComment(doc, ticket, UserSession.User.GetEmail()); + var res = await RunCommand(doc, Text, token); + await Display(res); + + + var json = doc.ToJson( new() { Indent = false }); + SyncUrl(json); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private async Task RunScript(string ticket, CancellationToken token) + { + List res = []; + try + { + IsReady = false; + ShowDoc = false; + + await InvokeAsync(StateHasChanged); + + var clearText = DecryptPasswords(Text); + + // Parse the Text JSON into a BsonArray + var docs = BsonSerializer.Deserialize(clearText).Select(p => p.AsBsonDocument); + foreach ( var doc in docs) + { + UpdateComment(doc, ticket, UserSession.User.GetEmail()); + res.Add(doc); + res.Add(new() { { "----------------------------------", "----------------------------------" } }); + res.AddRange(await RunCommand(doc, Text, token)); + res.Add(new() { { "==================================", "==================================" } }); + await Display(res); + } + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private string DecryptPasswords(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return text; + + var regex = new Regex(@"\[(\*.+)\]"); + var matches = regex.Matches(text); + + foreach (Match match in matches) + { + if (match.Groups.Count <= 1) + continue; + + var encryptedValue = match.Groups[1].Value; + var decryptedValue = PasswordManager.DecryptPassword(encryptedValue); + text = text.Replace(match.Value, decryptedValue); + } + + return text; + } + + private async Task Format() + { + Text = JsonUtils.FormatJson(Text); + await InvokeAsync(StateHasChanged); + } + + public static async Task CanExecuteCommand(IUserSession userSession, Func, Task> invokeAsync, IModalService modal) + { + var check = new [] { false }; + + await invokeAsync(async () => check[0] = await userSession.HasValidTask()); + + if (check[0]) + return userSession.TaskNumber; + + var res = await ModalDialogUtils.ShowConfirmationDialogWithInput( + modal, + "Elevated execution", + "Please enter a valid task number number to continue. It needs to be in execution state and has an open time window.", + "Valid task number:", + userSession.TaskNumber + ); + + if (string.IsNullOrWhiteSpace(res)) + return null; + + userSession.TaskNumber = res; + + await invokeAsync(async () => check[0] = await userSession.HasValidTask()); + + if (check[0]) + return userSession.TaskNumber; + + var msg = userSession.TaskCheckError + ?? "The given task number is not valid: " + res; + await ModalDialogUtils.ShowInfoDialog(modal, "Execution not allowed", msg); + + return null; + } + + public static void UpdateComment(BsonDocument parsed, string ticket, string email) + { + // comment field does not always work on database version 4.xx or lower + + /* + var comment = $"ticket: {ticket}, email: {email}"; + if (parsed == null) + throw new("Can't parse the command Json"); + // replace existing + parsed["comment"] = comment; + */ + } + + private Task CanExecuteCommand() + => CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private Task Display(Exception e) + { + Result = (MarkupString)e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + var json = res.ToJson(new() { Indent = true }); + + ResultStr = json; + + var md = $"```json\n{json}\n```"; + Result = new(Markdown.ToHtml(md, _htmlPipeline)); + + return InvokeAsync(StateHasChanged); + } + + private async Task> RunCommand(BsonDocument doc, string originalCommand, CancellationToken token) + { + var service = Shard switch + { + "- admin -" => UserSession.MongoDbAdminForAdminDatabase, + "- cluster -" => UserSession.MongoDbAdmin, + "- config -" => UserSession.GetCustomAdmin(Database, "config"), + _ => UserSession.GetShardConnection(_shardConnectionStrings[Shard].Host, _shardConnectionStrings[Shard].Port) + }; + + var result = await service.RunCommand(doc, originalCommand, token); + return [result]; + } + + private void SyncUrl(string? json = null) + { + var url = GetUrl(json); + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private string GetUrl(string? json) + { + var url = NavigationManager.BaseUri + $"admin/shell/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + if ( json != null ) + { + url += $"/{Uri.EscapeDataString(json)}"; + } + else if (CommandName != null) + { + url += $"/{CommandName}"; + } + + return url; + } + + private async Task LoadDocumentation() + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var hint = $"```javascript\n{await DocService.TryGetHint(CommandName!, cts.Token) ?? ""}\n```"; + + + Doc = new(Markdown.ToHtml(hint, _htmlPipeline) ); + + await InvokeAsync(StateHasChanged); + + var md = await DocService.TryGetMarkdown(CommandName!, cts.Token); + if (string.IsNullOrWhiteSpace(md)) + { + Description = new(); + return; + } + + var html = Markdown.ToHtml(md, _htmlPipeline); + Description = new(html); + + await InvokeAsync(StateHasChanged); + } + + private async Task CopyUrl() + { + try + { + var url = GetUrl(Text); + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", url); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "Url was copied to clipboard."); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + + private async Task CopyResults() + { + try + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", ResultStr); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "Results was copied to clipboard."); + } + catch ( Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + + /// + /// Display a user information dialog box, allows the user to click ok or cancel + /// + public static async Task ShowEncryptionDialog( + IModalService service, + string? initialPassword = null + ) + { + var parameters = new ModalParameters { { "ShowCancel", true } }; + + if (initialPassword != null) + parameters.Add("ClearText", initialPassword); + + var options = new ModalOptions + { + HideCloseButton = false, + DisableBackgroundCancel = false + }; + + var form = service.Show("Encrypt", parameters, options); + return await form.Result; + } + + + private async Task OnEncrypt() + { + var res = await ShowEncryptionDialog(Modal); + if (!res.Cancelled) + { + var encrypted = (string?)res.Data; + if ( string.IsNullOrWhiteSpace(encrypted) ) + return; + + var toInsert = $"[{encrypted}]"; + await _codeEditor.InsertAtCursor(toInsert); + await InvokeAsync(StateHasChanged); + } + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/SyncStructure.razor b/Rms.Risk.Mango/Pages/Admin/SyncStructure.razor new file mode 100644 index 0000000..defd3d5 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/SyncStructure.razor @@ -0,0 +1,716 @@ +@page "/admin/sync-structure" +@page "/admin/sync-structure/{DatabaseStr}" +@page "/admin/sync-structure/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] + +@using Rms.Risk.Mango.Components.Commands +@using Rms.Risk.Mango.Controllers +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@using Rms.Service.Bootstrap.Security + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject ITempFileStorage TempFileStorage +@inject IPasswordManager PasswordManager +@inject ISingleUseTokenService SingleUseTokenService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Sync structure

+ + + +
+
+
+ + + +
+
+ @if ( _loading ) + { + + } + else + { + + +
+ @if ( item.Data == null ) // index label + { + if ( item.Label != "Collections") + { +
@item.Label
+ } + else // root node + { +
@item.Label
+ } + } + else if ( item.Data.Index != null ) // index body + { +
+ +
+ } + else if (item.Data.Collection != null ) // collection node + { + + if (item.Data.Collection.Name.EndsWith("-Meta")) + { +
@item.Label
+ } + else + { +
@item.Label
+ } + if ( item.Data?.IsSharded ?? false ) + { +
Sharded
+ } + } +
+
+
+ } +
+
+
+
+ + + +
+
+ + + @if ( item.Data == null ) // index label + { + if ( !item.Label.Contains( "Differences", StringComparison.OrdinalIgnoreCase )) + { + @item.Label + } + else // root node + { + @item.Label + } + } + else if ( item.Data.Index != null ) // index body + { +
+ +
+ } + else if (item.Data.Collection != null ) // collection node + { + + if (item.Data.Collection.Name.EndsWith("-Meta")) + { + @item.Label + } + else + { + @item.Label + } + if ( item.Data.IsSharded ) + { + Sharded + } + } + else + { + @item.Label + if ( item.Data.IsSharded ) + { + Sharded + } + } +
+
+
+
+
+
+
+ + +
+ + + + + +
+
+ @foreach( var (success, message) in _progress ) + { +
+ @message +
+ } +
+
+
+ + @if (Difference.ToBeSharded.Count > 0 ) + { +
There are collections that needs to be sharded. This is needs to be done manually because you have to specify sharding index. Usually it's {_id: "hashed"}, but this is not always true.
+ } + @if (Difference.ToBeUnSharded.Count > 0 ) + { +
There are collections that needs to be unsharded. This is needs to be done manually because it's impossible to automatically select shard to host all the collection's data.
+ } + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private const string ToAddLabel = "To add/change"; + private const string ToRemoveLabel = "To remove"; + private const string ToBeShardedLabel = "To be sharded"; + private const string ToBeUnshardedLabel = "To be unsharded"; + + + public class IndexTreeItem + { + public DatabaseStructureLoader.CollectionStructure? Collection { get; set; } + public DatabaseStructureLoader.IndexStructure? Index { get; set; } + + public bool IsSelected { get; set; } + public bool IsSharded { get; init; } + } + + + private DatabaseStructureLoader.StructureDifference Difference { get; set; } = new(); + + private List<(bool Success, string Message)> _progress = new(); + + private List _currentCollections = []; + private List _targetCollections = []; + + private bool _loading; + + private readonly DatabaseStructureLoader.SyncStructureOptions _syncOptions = new() + { + RemoveCollections = false, + RemoveIndexes = false + }; + + private readonly List> _currentNodes = + [ + new () + { + Label = "Current", + Children = [] + } + ]; + + private readonly List> _targetNodes = + [ + new () + { + Label = "Upload to see differences...", + Children = [] + } + ]; + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + + try + { + _loading = true; + StateHasChanged(); + + var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + + var (collections, root) = await LoadStructure(UserSession.MongoDbAdmin, UserSession.MongoDbAdminForAdminDatabase, cts2.Token); + _currentCollections = collections; + _currentNodes.Clear(); + _currentNodes.Add(root); + + // #if DEBUG + // var json = await File.ReadAllTextAsync("C:\\shabale\\Downloads\\Forge UAT-structure.json"); + // _targetCollections = DatabaseStructureLoader.ParseCollections(json); + // CompareStructure(); + // #endif + } + catch (Exception ex) + { + _loading = false; + await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error loading structure for {Database}", ex); + } + finally + { + _loading = false; + StateHasChanged(); + } + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/sync-structure/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + public static async Task<(List, TreeNode)> LoadStructure(IMongoDbDatabaseAdminService db, IMongoDbDatabaseAdminService admin, CancellationToken token) + { + var root = new TreeNode + { + Label = "Collections" + }; + + var collections = (await DatabaseStructureLoader.LoadCollections(db, admin, token)) + .Select(x => + new + { + Group = + x.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) + ? " Meta" + : x.Name.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) + ? " Cache" + : " Data", + Value = x + } + ) + .ToList(); + + var allCollections = collections + .OrderBy(x => x.Value.Name) + .Select( x => x.Value ) + .ToList(); + + // Add collections to the root node + var collectionNodes = collections + .GroupBy(x => x.Group) + .OrderBy(x => x.Key) + .Select(gr => new TreeNode + { + Label = gr.Key, + IsExpanded = true, + Children = gr + .OrderBy(x => x.Value.Name) + .Select(coll => new TreeNode + { + Label = coll.Value.Name, + IsExpanded = false, + Data = new() + { + Collection = coll.Value, + IsSharded = coll.Value.IsSharded + }, + Children = coll.Value.Indexes + .OrderBy(idx => idx.Name) + .Select(idx => new TreeNode + { + Label = idx.Name, + IsExpanded = false, + Children = + [ + new TreeNode + { + Label = idx.Name, + LabelFragment = _ => null!, + Data = new() { Collection = coll.Value, Index = idx }, + } + ] + }) + .ToList() + }) + .ToList() + }) + .ToList(); + + root.Children = collectionNodes; + root.IsExpanded = true; + Set(root, true); + + return (allCollections, root); + } + + private string ConvertCurrentCollectionsToJson() => Newtonsoft.Json.JsonConvert.SerializeObject(_currentCollections.Where(IsSelected).ToList(), Newtonsoft.Json.Formatting.Indented); + + private bool IsSelected(DatabaseStructureLoader.CollectionStructure coll) + => IsSelected(_currentNodes, coll); + + private static bool IsSelected(List> nodes, DatabaseStructureLoader.CollectionStructure coll) + { + foreach (var node in nodes) + { + if (node.Data?.Collection?.Name.Equals(coll.Name, StringComparison.OrdinalIgnoreCase) ?? false) + { + return node.Data.IsSelected; + } + if (IsSelected(node.Children, coll)) + { + return true; + } + } + return false; + } + + private async Task OnDownload() + { + var url = await DownloadController.GetDownloadLink( + TempFileStorage, + PasswordManager, + SingleUseTokenService, + async fileName => await File.WriteAllTextAsync(fileName, ConvertCurrentCollectionsToJson()), + $"{FileUtils.Shield(Database)}-structure.json" + ); + + await JsRuntime.InvokeVoidAsync("open", $"{NavigationManager.BaseUri}{url}", "_blank"); + + } + + private async Task OnSync() + { + var toAdd = ExtractSelected(ToAddLabel); + var toRemove = ExtractSelected(ToRemoveLabel); + + if (toAdd.Count == 0 && toRemove.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Sync structure", "No changes detected."); + return; + } + + if ( !_syncOptions.DryRun ) + { + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Sync structure", "Are you sure you want to synchronize the database structure? This will add or remove collections and indexes as needed."); + if (res.Cancelled) + return; + } + var admin = UserSession.MongoDbAdmin; + + try + { + var newDiff = new DatabaseStructureLoader.StructureDifference(); + newDiff.ToAdd.AddRange(Difference.ToAdd.Where(x => toAdd.Contains(x.Name))); + newDiff.ToRemove.AddRange(Difference.ToRemove.Where(x => toRemove.Contains(x.Name))); + + _progress = await DatabaseStructureLoader.SyncStructure(admin, newDiff, _syncOptions); + await InvokeAsync(StateHasChanged); + await ModalDialogUtils.ShowInfoDialog(Modal, "Sync structure", "Synchronization completed successfully."); + } + catch (Exception ex3) + { + await InvokeAsync(StateHasChanged); + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error during synchronization", ex3); + } + + return; + + HashSet ExtractSelected(string label) + { + var idx = _targetNodes[0].Children.FindIndex(x => x.Label.Equals(label, StringComparison.OrdinalIgnoreCase)); + + var res = idx >= 0 + ? _targetNodes[0].Children[idx].Children + .Where(x => x.Data is { IsSelected: true, Collection: not null } ) + .Select(x => x.Data!.Collection!.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + : [] + ; + return res; + } + + } + + + private async Task OnFileUploaded(InputFileChangeEventArgs args) + { + try + { + if (args.FileCount == 0) + return; + var file = args.GetMultipleFiles(1).FirstOrDefault(); + if (file == null) + return; + + await using var stream = file.OpenReadStream(10 * 1024 * 1024); // 10 MB limit + var json = await new StreamReader(stream).ReadToEndAsync(); + + _targetCollections = DatabaseStructureLoader.ParseCollections(json); + if (_targetCollections.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal,"Sync structure", "No collections found in the uploaded file."); + return; + } + + CompareStructure(); + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error uploading structure", ex); + + } + } + + private bool SelectedInCurrent(DatabaseStructureLoader.CollectionStructure node) + { + var found = _currentNodes[0].Children.FirstOrDefault(x => x.Data?.Collection?.Name.Equals(node.Name, StringComparison.OrdinalIgnoreCase) ?? false); + if ( found == null ) + return true; + return found.Data?.IsSelected ?? true; + + } + + private void CompareStructure() + { + var targetCollections = _targetCollections.Where(SelectedInCurrent).ToList(); + if (targetCollections.Count == 0) + { + if (Difference.ToAdd.Count == 0 && Difference.ToRemove.Count == 0 && _targetNodes.Count == 1) + return; + + Difference = new(); + _targetNodes.Clear(); + _targetNodes.Add( + new() + { + Label = "Upload to see differences...", + Children = [] + } + ); + return; + } + + Difference = DatabaseStructureLoader.GetStructureDifference(_currentCollections.Where(SelectedInCurrent).ToList(), targetCollections); + + _targetNodes[0].IsExpanded = true; + _targetNodes[0].Label = $"Differences (Add: {Difference.ToAdd.Count + Difference.ToAdd.Sum(x => x.Indexes.Count)} Remove: {Difference.ToRemove.Count + Difference.ToRemove.Sum(x => x.Indexes.Count)})"; + _targetNodes[0].Children.Clear(); + + if (Difference.ToAdd.Count > 0) + { + _targetNodes[0].Children.Add( + new() + { + Label = ToAddLabel, + IsExpanded = true, + Children = Difference.ToAdd.OrderBy(x => x.Name).Select(coll => new TreeNode + { + Label = $"{coll.Name} ({coll.Type})", + Data = new() { Collection = coll }, + Children = coll.Indexes.OfType().Select(idx => new TreeNode + { + Label = $"{idx.Name} ({idx.Type})", + Data = new() { Collection = coll, Index = idx } + }).ToList() + }).ToList() + } + ); + } + + if (Difference.ToRemove.Count > 0) + { + _targetNodes[0].Children.Add( + new() + { + Label = ToRemoveLabel, + IsExpanded = true, + Children = Difference.ToRemove.OrderBy(x => x.Name).Select(coll => new TreeNode + { + Label = $"{coll.Name} ({coll.Type})", + Data = new() { Collection = coll }, + Children = coll.Indexes.OfType().Select(idx => new TreeNode + { + Label = $"{idx.Name} ({idx.Type})", + Data = new() { Collection = coll, Index = idx } + }).ToList() + }).ToList() + } + ); + } + + if ( Difference.ToBeSharded.Count > 0 ) + { + _targetNodes[0].Children.Add( + new() + { + Label = ToBeShardedLabel, + IsExpanded = true, + Children = Difference.ToBeSharded.OrderBy(x => x).Select(coll => new TreeNode + { + Label = $"{coll}", + Data = new() { IsSharded = false }, + }).ToList() + } + ); + } + + if ( Difference.ToBeUnSharded.Count > 0 ) + { + _targetNodes[0].Children.Add( + new() + { + Label = ToBeUnshardedLabel, + IsExpanded = true, + Children = Difference.ToBeUnSharded.OrderBy(x => x).Select(coll => new TreeNode + { + Label = $"{coll}", + Data = new() { IsSharded = false }, + }).ToList() + } + ); + } + + OnSelectAllTarget(); + } + + private static void Set(TreeNode item, bool value) + { + if (item.Data != null) + { + item.Data.IsSelected = value; + } + foreach (var child in item.Children) + { + Set(child, value); + } + } + + private Task OnSelectAllCurrent() + { + foreach( var node in _currentNodes) + Set(node, true); + CompareStructure(); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectNoneCurrent() + { + foreach( var node in _currentNodes) + Set(node, false); + CompareStructure(); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectAllTarget() + { + foreach( var node in _targetNodes) + Set(node, true); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectNoneTarget() + { + foreach( var node in _targetNodes) + Set(node, false); + return InvokeAsync(StateHasChanged); + } + + private Task SetSourceItem(TreeNode item, bool b) + { + Set(item, b); + CompareStructure(); + return InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Upload.razor b/Rms.Risk.Mango/Pages/Admin/Upload.razor new file mode 100644 index 0000000..f5d345e --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Upload.razor @@ -0,0 +1,473 @@ +@page "/admin/upload" +@page "/admin/upload/{DatabaseStr}" +@page "/admin/upload/{DatabaseStr}/{DatabaseInstanceStr}" + +@implements IDisposable +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IMigrationEngine MigrationEngine +@inject ITempFileStorage Storage + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Download

+ + + + +
+ + +
+
+ + + +
+
+ + + @if ( item.Data == null ) + { + @item.Label + } + else + { + + if (item.Label.EndsWith("-Meta")) + { + @item.Label + } + else + { + @item.Label + } + } + + +
+
+
+ +
+
+
+ + +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } +
+
+
+
+ +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + public class IndexTreeItem + { + public bool IsSelected { get; set; } + } + + private bool IsReady { get; set; } + private bool IsReadyToCancel => IsReady && !SelectedJob.Complete; + private string Error { get; set; } = ""; + private List RunningJobs { get; set; } = []; + private string BatchSize { get; set; } = "1000"; + private bool Upsert { get; set; } + private bool WipeDestination { get; set; } + + private CancellationTokenSource _cts = new(); + + private string _uploadedFileName = ""; + + private readonly List> _currentNodes = + [ + new () + { + Label = "Uploaded", + Children = [] + } + ]; + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private MigrationJob SelectedJob + { + get; + set + { + if (field.JobId == value.JobId) + return; + field = value; + StateHasChanged(); + } + } = new() { Complete = true }; + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + + try + { + RunningJobs = MigrationEngine.List().Where(x => x.Type == MigrationJob.JobType.Upload).ToList(); + if (RunningJobs.Count > 0) + SelectedJob = RunningJobs[0]; + + _ = Task.Run(() => RefreshLoop(_cts.Token), _cts.Token); + +// #if DEBUG +// await PrepareCollectionNames("C:\\shabale\\Downloads\\dbMango_data.zip"); +// #endif + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error loading structure for {Database}", ex); + return; + } + + IsReady = true; + StateHasChanged(); + } + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + + private async Task RefreshLoop(CancellationToken token) + { + // percents are always increasing, so it safe to compare sum() + var prevState = -1.0; + var prevJobId = SelectedJob.JobId; + var oldComplete = false; + + while (!token.IsCancellationRequested) + { + if (prevJobId != SelectedJob.JobId) + { + prevJobId = SelectedJob.JobId; + prevState = -1.0; + oldComplete = false; + await InvokeAsync(StateHasChanged); + } + else + { + var newState = SelectedJob.Status.Sum(x => x.Count + x.Copied + (x.Cleared ?? 0)); + + if (SelectedJob.Complete != oldComplete || Math.Abs(newState - prevState) > 0.01) + { + prevState = newState; + oldComplete = SelectedJob.Complete; + await InvokeAsync(StateHasChanged); + } + } + + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + } + + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/upload/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private static void Set(TreeNode item, bool value, Func, bool>? filter = null ) + { + filter ??= _ => true; + + if (item.Data != null && filter(item)) + { + item.Data.IsSelected = value; + } + foreach (var child in item.Children) + { + Set(child, value, filter); + } + } + + private Task OnSelectAll() + { + foreach( var node in _currentNodes) + Set(node, true); + return InvokeAsync(StateHasChanged); + } + + private Task OnSelectNone() + { + foreach( var node in _currentNodes) + Set(node, false); + return InvokeAsync(StateHasChanged); + } + + private Task SetSourceItem(TreeNode item, bool b) + { + Set(item, b); + return InvokeAsync(StateHasChanged); + } + + private async Task OnFileUploaded(InputFileChangeEventArgs args) + { + var fileName = Storage.GetTempFileName("Uploaded"); + + // Save the uploaded file to the temporary storage + { + // brackets to close the file + await using var fileStream = File.OpenWrite(fileName); + await args.File.OpenReadStream(1 * 1024 * 1024 * 1024L).CopyToAsync(fileStream); + } + + await PrepareCollectionNames(fileName); + } + + private async Task PrepareCollectionNames(string fileName) + { + // Treat the file as a zip archive and read its contents + var dir = GetZipDirectory(fileName); + + // Add collections to the root node + var collectionNodes = dir + .OrderBy(x => x) + .Select(coll => new TreeNode + { + Label = coll, + Data = new () { IsSelected = true }, + }).ToList(); + + var root = new TreeNode + { + Label = "Uploaded", + Children = collectionNodes, + IsExpanded = true + }; + + _currentNodes.Clear(); + _currentNodes.Add(root); + _uploadedFileName = fileName; + + await InvokeAsync(StateHasChanged); + } + + private static HashSet GetZipDirectory(string fileName) + { + using var zipArchive = new System.IO.Compression.ZipArchive(File.OpenRead(fileName), System.IO.Compression.ZipArchiveMode.Read); + + var dir = new HashSet(); + + foreach (var entry in zipArchive.Entries) + { + var folder = Path.GetDirectoryName(entry.FullName) ?? string.Empty; + dir.Add(folder); + } + + return dir; + } + + private async Task OnDoUpload() + { + if ( string.IsNullOrWhiteSpace(_uploadedFileName) || !File.Exists(_uploadedFileName) ) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Upload data", "Please, select a file to upload."); + return; + } + + var newJob = new MigrationJob + { + Type = MigrationJob.JobType.Upload, + SourceDatabase = UserSession.Database, + SourceDatabaseInstance = UserSession.DatabaseInstance, + DestinationDatabase = UserSession.Database, + DestinationDatabaseInstance = UserSession.DatabaseInstance!, + Email = UserSession.User.GetEmail(), + Upsert = Upsert, + ClearDestinationBefore = WipeDestination, + UploadedFileName = _uploadedFileName, + BatchSize = int.Parse(BatchSize), + Status = _currentNodes[0].Children + .Where(x => (x.Data?.IsSelected ?? false) ) + .Select(x => new MigrationJob.CollectionJob + { + SourceCollection = x.Label, + DestinationCollection = x.Label, + }) + .ToList() + + }; + + if (newJob.Status.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Upload data", "Please, select at least one collection."); + return; + } + + var info = GetInfo(newJob); + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Upload data", + $"Are you sure to start uploading of {newJob.Status.Count} collection(s) to {newJob.SourceDatabase}?", + info + ); + + if (res.Cancelled) + return; + + await RegisterNewJob(newJob); + } + + private async Task CancelJob() + { + if (SelectedJob.Complete) + return; + + var info = GetInfo(SelectedJob); + + var res = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Migration", + $"Are you sure to cancel migration of {SelectedJob.Status.Count} collection(s) from {SelectedJob.SourceDatabase} to {SelectedJob.DestinationDatabase}?", + info + ); + if (res.Cancelled) + return; + + await MigrationEngine.Cancel(SelectedJob, UserSession.User.GetUser()); + await InvokeAsync(StateHasChanged); + } + + private Dictionary GetInfo(MigrationJob job) + { + var info = new Dictionary + { + ["from"] = job.SourceDatabase, + ["batchSize"] = job.BatchSize.ToString(), + }; + return info; + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task RegisterNewJob(MigrationJob newJob) + { + try + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + newJob.Ticket = ticket; + + await MigrationEngine.Add(newJob, UserSession.User.GetUser()); + SelectedJob = newJob; + RunningJobs = MigrationEngine.List(); + } + catch (Exception e) + { + Error = e.ToString(); + } + + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Pages/Admin/Users.razor b/Rms.Risk.Mango/Pages/Admin/Users.razor new file mode 100644 index 0000000..a43c41b --- /dev/null +++ b/Rms.Risk.Mango/Pages/Admin/Users.razor @@ -0,0 +1,657 @@ +@page "/admin/users" +@page "/admin/users/{DatabaseStr}" +@page "/admin/users/{DatabaseStr}/{DatabaseInstanceStr}" +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@using Rms.Risk.Mango.Services.Context +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IMongoDbServiceFactory MongoDbServiceFactory; + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Users Management

+ + + @if (Error != null) + { + + } + + + +
+
+
+ + +
+ + + + +
+
@context.UserName
+ @if (context.IsBuiltin) + { +
BuiltIn
+ } + else if ( !string.IsNullOrWhiteSpace( context.Db )) + { +
@context.Db
+ } +
+
+
+
+
+
+ +
+
+
+ + + + +
+ + +
+
+
+
+ + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private Exception? Error { get; set; } + private bool IsReady { get; set; } + + private UserInfoModel SelectedUser + { + get; + set + { + if ( field == value ) + return; + field = value; + + // Populate AllRoles with non-built-in roles from the same database and built-in roles paired with it. + AllRoles = RolesList.Where(x => !x.IsBuiltin && x.Db == field.Db) + .Select(x => new RoleInDbModel + { + Role = x.RoleName, + Db = x.Db + } + ) + .Concat( + RolesList + .Where(x => x.IsBuiltin) + .Select(x => new RoleInDbModel + { + Role = x.RoleName, + Db = "" + }) + ) + .DistinctBy(x => $"{x.Db}, {x.Role}") + .OrderBy(x => $"{x.Db}, {x.Role}") + .ToList() + ; + + + EditableSelectedUser = field.Clone(); + } + } = new(); + + private UserInfoModel EditableSelectedUser { get; set; } = new(); + private List RolesList { get; set; } = []; + private List AllRoles { get; set; } = []; + private List AllDatabases { get; set; } = []; + private List UserList { get; set; } = []; + private bool IsNew => UserList.All(x => x.UserName != EditableSelectedUser.UserName); + private bool CanUpdate => !EditableSelectedUser.IsBuiltin; + private bool CanDelete => !EditableSelectedUser.IsBuiltin && !IsNew; + + private bool IsSelectable(UserInfoModel arg) => true; + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + StateHasChanged(); + Task.Run(LoadUsers); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + private async Task Run(Func body) + { + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + await body(ticket, cts.Token); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private Task LoadUsers() + => Run(async (_, token) => + { + try + { + AllDatabases = (await UserSession.MongoDbAdminForAdminDatabase.ListDatabases(token)) + .Select(x => x.Name) + .ToList(); + } + catch (Exception) + { + AllDatabases = [UserSession.DatabaseInstance]; + } + + var usersInfoTasks = AllDatabases.Select( async instanceName => + { + var db = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, instanceName); + var users = await db.GetUsersInfo(token: token); + foreach( var u in users.Users ) + { + u.Db = instanceName; + } + return users; // Add this line to return the users from the async function + }) + .ToList(); + var adminRolesTask = UserSession.MongoDbAdminForAdminDatabase.GetRolesInfo(showBuiltInRoles: true, token: token); + var customRolesTasks = AllDatabases.Select(x => LoadRolesForDatabaseInstance(x, token)).ToList(); + + await Task.WhenAll(customRolesTasks.Cast().Concat(usersInfoTasks).Concat([adminRolesTask])); + + var adminRoles = await adminRolesTask; + var all = new RolesInfoModel(); + all.Roles.AddRange(adminRoles.Roles.Where(x => x.IsBuiltin)); + + foreach (var customTask in customRolesTasks) + { + var custom = await customTask; + all.Roles.AddRange(custom.Roles); + } + + RolesList = all.Roles + .OrderBy(x => $"{x.Db}, {x.RoleName}") + .ToList() + ; + + UsersModel usersInfo = new(); + + foreach (var customTask in usersInfoTasks) + { + var custom = await customTask; + usersInfo.Users.AddRange(custom.Users); + } + + // Skip the admin user, as it is not editable + var adminUser = usersInfo.Users.FirstOrDefault(x => x.UserName == "admin"); + if ( adminUser != null ) + adminUser.IsBuiltin = true; + + await Display(usersInfo); + }); + + private Task LoadRolesForDatabaseInstance(string instanceName, CancellationToken token) + { + try + { + var db = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, instanceName); + return db.GetRolesInfo(showBuiltInRoles: false, token: token); + } + catch (Exception) + { + return Task.FromResult(new RolesInfoModel()); + } + } + + + private Task Display(UsersModel model) + { + Error = null; + + var oldUserName = SelectedUser.UserName; + + UserList = model.Users + .OrderBy(x => x.UserName) + .ToList() + ; + + SelectedUser = !string.IsNullOrWhiteSpace(oldUserName) + ? model.Users.FirstOrDefault(x => x.UserName == oldUserName) ?? model.Users.FirstOrDefault() ?? new() + : model.Users.FirstOrDefault() ?? new UserInfoModel() + ; + + return InvokeAsync(StateHasChanged); + } + + + private Task Display(Exception e) + { + Error = e; + UserList.Clear(); + AddUser(); + + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"admin/users/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private static string GetUserClass(UserInfoModel r) => r.IsBuiltin ? "builtin" : ""; + + private static int _count; + + private Task AddUser() + { + SelectedUser = new () + { + UserName = $"newUser{++_count}", + Db = UserSession.DatabaseInstance, + IsBuiltin = false + }; + + return InvokeAsync(StateHasChanged); + } + + private async Task DeleteUser() + { + if ( IsNew || string.IsNullOrWhiteSpace(SelectedUser.UserName) || SelectedUser.IsBuiltin ) + { + await ModalDialogUtils.ShowInfoDialog( Modal, "Oops!", "Cannot delete the selected user."); + return; + } + + var rc = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Confirm Deletion", + $"Are you sure you want to delete the user '{SelectedUser.UserName}' from '{SelectedUser.Db}'?" + ); + + if (!rc.Confirmed) + return; + + // Logic for deleting the selected user goes here + await Run(async (_, token) => + { + // Attention: use the admin service corresponding to the database you are going to delete user in! + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, SelectedUser.Db); + await service.DropUser(SelectedUser.UserName, token); + await LoadUsers(); + }); + } + + private async Task UpdateUser() + { + var user = EditableSelectedUser; + var origUser = UserList.FirstOrDefault(x => x.UserName == user.UserName && x.Db == user.Db); + + if (user.IsBuiltin || string.IsNullOrWhiteSpace(user.UserName)) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"Can't modify built-in user '{user.UserName}'."); + return; + } + + var diff = CompareRoles(origUser ?? new UserInfoModel(), user); + + var diffDict = diff + .ToDictionary(x => $"{x.Db} {x.Role}{(x.IsBuiltin ? " [Builtin]" : "")}", x => x.IsAdded ? "Added" : "Removed"); + + if (!string.IsNullOrWhiteSpace(user.Password)) + { + diffDict["Password"] = "Updated"; + } + + if (diffDict.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "No Changes", "No changes detected in the user roles and new password is not supplied."); + return; + } + + var rc = await ModalDialogUtils.ShowConfirmationDialog( + Modal, + "Warning", + $"Are you sure you want to {(origUser == null ? "add" : "update")} the user '{user.UserName}' within database '{user.Db}'?", + diffDict + ); + + if (!rc.Confirmed) + return; + + try + { + var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + + if ( string.IsNullOrWhiteSpace(user.Db) ) + user.Db = UserSession.DatabaseInstance; + + if (origUser == null) + await CreateUser(user, cts.Token); + else if (!string.IsNullOrWhiteSpace(user.Password)) + await UpdatePassword(user, cts.Token); + + await GrantAndRevokeRoles(user, diff, cts.Token); + } + catch (Exception ex) + { + await Display(ex); + } + } + + private async Task CreateUser(UserInfoModel user, CancellationToken token) + { + var command = new BsonDocument + { + { "createUser", user.UserName }, + { "pwd", user.Password }, + { "roles", new BsonArray() }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + + var redactedCommand = command.ToJson().Replace(user.Password ?? "******", "******"); + + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); + await service.RunCommand(command, redactedCommand, token); + } + + private async Task UpdatePassword(UserInfoModel user, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(user.Password)) + throw new("Password is not specified"); + + var command = new BsonDocument + { + { "updateUser", user.UserName }, // Changed from createUser to updateUser + { "pwd", user.Password }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + + var redactedCommand = command.ToJson().Replace(user.Password, "******"); + + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); + await service.RunCommand(command, redactedCommand, token); + } + + private async Task GrantAndRevokeRoles(UserInfoModel user, List diff, CancellationToken token) + { + var toAdd = diff + .Where(x => x.IsAdded) + .GroupBy(x => x.Db) + .Select(x => new { Db = x.Key, Roles = x.Select(y => new { y.Role, y.IsBuiltin }).ToList() }) + .ToList(); + + var toRemove = diff + .Where(x => !x.IsAdded) + .GroupBy(x => x.Db) + .Select(x => new { Db = x.Key, Roles = x.Select(y => new { y.Role, y.IsBuiltin }).ToList() }) // Update this to include IsBuiltin + .ToList(); + + foreach (var action in toAdd) + { + var command = new BsonDocument + { + { "grantRolesToUser", user.UserName }, + { "roles", new BsonArray(action.Roles + .Select(role => + role.IsBuiltin + ? BsonValue.Create(role.Role) + : new BsonDocument { { "role", role.Role }, { "db", action.Db } } + ) + ) + }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + // Attention: use the admin service corresponding to the database you are going to create user in! + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db ); + await service.RunCommand(command, token); + } + + foreach (var action in toRemove) + { + var command = new BsonDocument + { + { "revokeRolesFromUser", user.UserName }, + { "roles", new BsonArray(action.Roles + .Select(role => + role.IsBuiltin + ? BsonValue.Create(role.Role) + : new BsonDocument { { "role", role.Role }, { "db", action.Db } } + ) + ) + }, + { "writeConcern",new BsonDocument + { + { "w", "majority" } + } + } + }; + // Attention: use the admin service corresponding to the database you are going to create user in! + var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); + await service.RunCommand(command, token); + } + + await LoadUsers(); + } + + private async Task CopyUser() + { + try + { + var json = JsonUtils.ToJson(SelectedUser, new() { WriteIndented = true }); + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", json); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "User copied to clipboard."); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + + } + + private class RoleDiff + { + public string Role { get; set; } = string.Empty; + public string Db { get; set; } = string.Empty; + public bool IsBuiltin { get; set; } + public bool IsAdded { get; set; } + } + + private List CompareRoles(UserInfoModel user, UserInfoModel clone) + { + var builtinRoles = new HashSet(RolesList.Where(x => x.IsBuiltin).Select(x => x.RoleName)); + + // Identify roles present in the original user but missing in the clone + var diffs = user.Roles + .Where(role => !clone.Roles.Any(r => r.Role == role.Role && r.Db == role.Db)) + .Select(role => new RoleDiff + { + Role = role.Role, + Db = role.Db, + IsBuiltin = builtinRoles.Contains(role.Role), + IsAdded = false + } + ) + .ToList(); + + // Identify roles present in the clone but missing in the original user + diffs.AddRange(clone.Roles + .Where(role => !user.Roles.Any(r => r.Role == role.Role && r.Db == role.Db)) + .Select(role => new RoleDiff + { + Role = role.Role, + Db = role.Db, + IsBuiltin = builtinRoles.Contains(role.Role), + IsAdded = true + } + ) + ); + + return diffs; + } + + private async Task PasteUser() + { + try + { + var json = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + + var clone = JsonUtils.FromJson(json); + if (clone == null) + return; + + // Search for the existing user by name,db + var existingUser = UserList.FirstOrDefault(x => x.UserName == clone.UserName && + x.Db == clone.Db); + if ( existingUser != null ) + { + if ( existingUser.IsBuiltin ) + { + // If the user is built-in, show a warning and do not overwrite + await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"User '{clone.UserName}' is a built-in user and cannot be changed."); + return; + } + + // If the user already exists, show a warning and do not overwrite + var rc = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Warning", $"User '{clone.UserName}' already exists. Please choose a different name."); + if (!rc.Confirmed) + return; + } + + SelectedUser = clone; + + await InvokeAsync(StateHasChanged); + await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", $"User info for {SelectedUser.UserName} was successfully parsed. You still need to save it!"); + } + catch (Exception ex) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); + } + } + +} diff --git a/Rms.Risk.Mango/Pages/Doc/AfhDoc.razor b/Rms.Risk.Mango/Pages/Doc/AfhDoc.razor new file mode 100644 index 0000000..70fa453 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Doc/AfhDoc.razor @@ -0,0 +1,779 @@ +@page "/doc/afh" + +@inject IJSRuntime JsRuntime +@inject NavigationManager NavigationManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Aggregation for Humans documentation

+ + + + +

The MongoDB Aggregation Framework is a powerful tool for processing and transforming data within MongoDB. + It allows you to perform complex operations on collections of documents, similar to SQL queries + but with a more flexible and expressive syntax. The framework operates on the concept of a + "pipeline," where data flows through a series of stages, each stage performing a specific + transformation.

+ +

Here's a breakdown of key concepts and common stages:

+ + Key Concepts: +
    +
  • Pipeline: A sequence of data processing stages. Each stage takes the output of the previous stage as its input and applies a transformation.
  • +
  • Stages: Individual operations within the pipeline, such as filtering, grouping, projecting, sorting, and more.
  • +
  • Documents: The basic unit of data in MongoDB, represented as JSON-like objects.
  • +
  • Fields: Key-value pairs within a document.
  • +
  • Expressions: Used within stages to compute values, access fields, and perform operations on data.
  • +
+

More information can be found on MongoDB site.

+
+ +

Reasons why MongoDB Aggregation JSON is not ideal for human writing

+ +
    +
  • Verbosity and Nesting: Aggregation pipelines often involve deeply nested JSON structures, making them lengthy and difficult to parse.
  • +
  • Operator-Centric Approach: The syntax relies heavily on operators (e.g., $match, $group, $avg), which can be less intuitive than declarative SQL.
  • +
  • Error-Prone: The strict JSON syntax means minor errors (missing commas, incorrect brackets) can lead to invalid queries that are hard to debug.
  • +
  • Lack of Readability: JSON isn't naturally conducive to human understanding of complex logic; the data flow can be obscured.
  • +
  • Difficult to Visualize: It's hard to mentally visualize data transformations at each stage from raw JSON.
  • +
  • Repetitive Patterns: Similar structures might be repeated, making queries tedious to write and maintain.
  • +
  • Limited Code Reuse: No built-in mechanism for easily reusing pipeline parts or defining functions within the query.
  • +
+ +

+ While powerful, MongoDB aggregation's JSON syntax can be challenging for humans due to its verbosity and operator-centric nature. + The provided grammar (MongoAggregationForHumans) aims to address these issues with a more human-friendly syntax. +

+ +
+ + +

Here is an example of the pretty complex AFH pipeline and its equivalent using MongoDB aggregation Json. We hope you'll understand why we created AFH :)

+ +
+ + + +

MongoAggregationForHumans Language Syntax

+ +

The MongoAggregationForHumans grammar defines a language for expressing MongoDB aggregation pipelines in a more human-readable format. It aims to simplify the creation and understanding of these pipelines compared to the standard JSON syntax.

+ +

Overall Structure

+ +

A program in this language consists of a single statement that defines an aggregation pipeline:

+ +
+            
+                file
+                : 'FROM' STRING pipeline_def
+                ;
+            
+        
+ +

This indicates that a pipeline operates on a collection specified by STRING (the collection name) and is defined by a pipeline_def.

+ +

Pipeline Definition

+ +

A pipeline is a sequence of stages enclosed in curly braces:

+ +
+            
+                pipeline_def
+                : 'PIPELINE' '{'  stages_list '}'
+                ;
+            
+        
+ +

Stages

+ +

The stages_list allows for one or more stage_def, which represent the individual operations in the pipeline:

+ +
+            
+                stages_list
+                : stage_def
+                | stages_list stage_def
+                ;
+            
+        
+ +

The grammar supports the following stage types:

+ +
+            
+                stage_def
+                : match_def       // WHERE clause for filtering
+                | addfields_def   // ADD new fields
+                | project_def     // PROJECT fields (include or exclude)
+                | group_by_def    // GROUP BY a key and calculate aggregates
+                | sort_def        // SORT BY specified fields
+                | join_def        // JOIN with another collection
+                | unwind_def      // UNWIND an array field
+                | replace_def     // REPLACE the root document
+                | do_def          // DO (likely for custom operations or embedding JSON)
+                ;
+            
+        
+ +

Stage Details

+ +

Each stage has its own syntax. Here are some examples:

+ +
    +
  • + match_def (WHERE): +
    +                    
    +                        match_def: 'WHERE' expression ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Filters documents based on an expression. An optional OPTIONS clause allows for specifying JSON options.

    +
  • + +
  • + addfields_def (ADD): +
    +                    
    +                        addfields_def
    +                        : 'ADD' let_list ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Adds new fields defined in a let_list. Optional OPTIONS.

    +
  • + +
  • + project_def (PROJECT): +
    +                    
    +                        project_def
    +                        : 'PROJECT' ('ID' '{' id_list=let_list '}')? data_list=let_list ('OPTIONS' json)?     # ProjectInclude
    +                        | 'PROJECT' 'EXCLUDE' var_list ('OPTIONS' json)?                    # ProjectExclude
    +                        ;
    +                    
    +                
    +

    Includes or excludes fields. ProjectInclude allows specifying an ID and a data_list of fields to include. ProjectExclude uses a var_list to specify fields to exclude. Optional OPTIONS.

    +
  • + +
  • + group_by_def (GROUP BY): +
    +                    
    +                        group_by_def: 'GROUP' 'BY' id_list=let_list ('LET' data_list=let_list)? ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Groups documents by fields specified in id_list and calculates aggregates defined in the optional data_list (using LET). Optional OPTIONS.

    +
  • + +
  • + sort_def (SORT BY): +
    +                    
    +                        sort_def: 'SORT' 'BY' sort_var_list ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Sorts documents based on fields in sort_var_list, which can include ASC or DESC order. Optional OPTIONS.

    +
  • + +
  • + join_def (JOIN): +
    +                    
    +                        join_def: 'JOIN' STRING 'AS' (VARIABLE | STRING) 'ON' equivalence_list ('LET' let_list )? ('PIPELINE' '{'  stages_list '}')? ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Performs a join with another collection (specified by STRING). The joined collection is aliased using AS (either a VARIABLE or STRING). The join condition is defined by equivalence_list. An optional LET clause allows defining new fields based on the joined data. A sub-pipeline can be applied to the joined collection. Optional OPTIONS.

    +
  • + +
  • + unwind_def (UNWIND): +
    +                    
    +                        unwind_def: 'UNWIND' VARIABLE ('INDEX' VARIABLE)? ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Unwinds an array field (specified by VARIABLE). An optional INDEX clause specifies a variable to store the index of the array element. Optional OPTIONS.

    +
  • + +
  • + replace_def (REPLACE): +
    +                    
    +                        replace_def
    +                        : 'REPLACE' 'ID' '{' id_list=let_list '}' data_list=let_list ('OPTIONS' json)?
    +                        ;
    +                    
    +                
    +

    Replaces the root document. It uses id_list and data_list to define the replacement document. Optional OPTIONS.

    +
  • + +
  • + do_def (DO): +
    +                    
    +                        do_def: 'DO' json
    +                        ;
    +                    
    +                
    +

    This stage seems to allow embedding arbitrary JSON (json) within the pipeline. Its exact behavior would depend on the implementation.

    +
  • +
+ +

Expressions

+ +

The language uses a hierarchy of expressions to define conditions, calculations, and field manipulations. The grammar includes rules for:

+ +
    +
  • expression: Combines comparison_expression with logical operators (AND, OR).
  • +
  • comparison_expression: Combines additive_expression with comparison operators (==, !=, >, >=, <, <=).
  • +
  • additive_expression: Combines multiplicative_expression with addition and subtraction (+, -).
  • +
  • multiplicative_expression: Combines unary_expression with multiplication and division (*, /).
  • +
  • unary_expression: Handles unary operators (+, -, NOT) and various primary expressions.
  • +
  • brackets_expression: Includes atoms, function calls, "IN" expressions, "IS" (projection) expressions, "EXISTS" expressions, and bracketed expressions.
  • +
  • atom: Represents basic values like strings, numbers, booleans, null, and variables.
  • +
+ +

The grammar also defines rules for let_list (for defining fields and expressions), var_list (for lists of variables), sort_var_list (for sorting specifications), and equivalence_list (for join conditions).

+ +

JSON Integration

+ +

The language integrates with JSON for specifying options and potentially within the do_def stage. The JsonGrammar.g4 file (also provided) defines the JSON syntax used.

+ +

Key Features and Improvements

+ +

Compared to standard MongoDB JSON, this language offers:

+ +
    +
  • More Readable Keywords: Uses keywords like FROM, PIPELINE, WHERE, ADD, PROJECT, GROUP BY, SORT BY, JOIN, UNWIND, REPLACE, making the structure clearer.
  • +
  • Simplified Syntax: Aims to reduce nesting and verbosity, especially for common operations.
  • +
  • More Natural Expression Syntax: Uses familiar operators (==, >, <, AND, OR) and function call syntax.
  • +
+ +

Overall, MongoAggregationForHumans provides a more user-friendly way to express MongoDB aggregation pipelines, potentially reducing errors and improving developer productivity.

+
+ +

expression

+
+

comparizon_expression

+
+

additive_expression

+
+

multiplicative_expression

+
+

unary_expression

+
+

brackets_expression

+
+

atom

+
+

named_args_list

+
+

unnamed_args_list

+
+

expression_array

+
+

expression_array_item

+
+

Variables

+
+

Strings

+
+

Operators

+

+                AND: 'AND' | '&&';
+                OR: 'OR' | '||';
+                NOT: 'NOT' | '!';
+                EQ: '==';
+                NEQ: '<>' | '!=';
+                GT: '>';
+                GTE: '>=';
+                LT: '<';
+                LTE: '>=';
+                ASC: 'ASC';
+                DESC: 'DESC';
+                MUL: '*';
+                DIV: '/';
+                PLUS: '+';
+                MINUS: '-';
+            
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+ +
+ + +

pipeline_def

+ + +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + +
+ +@code +{ + private string StageWhere = +@" +WHERE + ( cob == ""2025-04-22"" && Department == ""FX Options"" ) + && Book NOT IN (""CHFX"", ""JPYPJPY"") + +"; + + private string StageAdd = +@" +ADD // special syntax + pv + premiumPV AS TotalPV, + abs( pv + pvMove ) AS MyPnl, // function call + dateToString( format: ""%Y-%m-%d"", date: field1 ) AS ""TodayStr"", // named args + { + ""data"" AS Key, + ""value"" AS Value + } AS Nested, // nested object + [{ + ""data1"" AS Key, + ""v1"" AS Value + }, + { + ""data2"" AS Key, + ""v2"" AS Value + }] AS NestedArray // nested array +"; + + private string StageGroupBy = +@" +GROUP BY + CurveKey + LET + max( Order ) AS Order, + sum( Value ) AS Value +"; + private string StageBucket = + @" +BUCKET + Field1 / 100.1 + BOUNDARIES 1, 10, 100, 1000 + DEFAULT ""Ignored"" + LET + Field1 / 100.1 AS Gain +"; + private string StageJoin = +@" +JOIN ""PnL-Market"" AS Market ON + $_id.CurveKey == _id +"; + private string StageProject = +@" +PROJECT + HedgeLVSVInstruments, + CurvePrefix, + objectToArray( VegaDetails ) AS VegaDetails +"; + private string StageReplace = +@" +REPLACE ID { + $_id.CcyPair AS CcyPair, + $_id.Tenor AS Tenor + } + Order, + 'DN OpeningCurve', + '10RR OpeningCurve' +"; + private string StageSort = +@" +SORT BY + $_id.CcyPair, + Order +"; + private string StageUnwind = +@" +UNWIND Data INDEX Order +"; + private string StageDo = +@" +DO +{ + ""$replaceRoot"": { + ""newRoot"": ""$_id"" + } +} +"; + + private string StageFacet = + @" +FACET + categorizedByTags PIPELINE { + UNWIND tags + DO + { + ""$sortByCount"": ""$tags"" + } + }, + categorizedByPrice PIPELINE { + WHERE + price == exists( 1 ) + BUCKET + price + BOUNDARIES 0, 150, 200, 300, 400 + DEFAULT ""Other"" + LET + sum( 1 ) AS count, + push( title ) AS titles + }, + 'categorizedByYears(Auto)' PIPELINE { + BUCKET AUTO + year + BUCKETS 4 + } +"; + + private string StageComplexExample = +@" + WHERE + COB == date( ""2025-05-15T00:00:00Z"" ) + AND (Department == ""FX Options Controlling"" ) + WHERE + $VegaDetails.VegaImpact == exists( true ) + PROJECT + HedgeLVSVInstruments, + $VegaDetails.OpeningVega, + $VegaDetails.ClosingVega, + $VegaDetails.VegaImpact, + $VegaDetails.Vega2ndOrderImpact, + concat( dateToString( + format: ""%Y%m%d"", + date: COB + ), ""-"", toString( OpeningVolRateSetId ), ""-"", toString( ClosingVolRateSetId ), ""-PnL-Vol-"" ) AS CurvePrefix + PROJECT + HedgeLVSVInstruments, + CurvePrefix, + objectToArray( VegaDetails ) AS VegaDetails + UNWIND VegaDetails + PROJECT + HedgeLVSVInstruments, + CurvePrefix, + $VegaDetails.k AS Type, + objectToArray( $VegaDetails.v ) AS Data + UNWIND Data + WHERE + (Type == ""OpeningVega"" + OR Type == ""VegaImpact"" + OR Type == ""Vega2ndOrderImpact"") + PROJECT + concat( CurvePrefix, $Data.v.CcyPair, ""-"", HedgeLVSVInstruments, ""-"" ) AS CurvePrefix, + Type, + $Data.v.CcyPair AS CcyPair, + objectToArray( $Data.v.Data ) AS Data + UNWIND Data INDEX Order + PROJECT + CurvePrefix, + CcyPair, + $Data.k AS Tenor, + Order, + Type, + objectToArray( $Data.v ) AS Data + UNWIND Data + PROJECT + CurvePrefix, + concat( CurvePrefix, Tenor, ""-"", $Data.k ) AS CurveKey, + CcyPair, + Type, + Tenor, + Order, + $Data.k AS Delta, + $Data.v AS Value + WHERE + (Delta == ""10RR"" + OR Delta == ""10FLY"" + OR Delta == ""25RR"" + OR Delta == ""25FLY"" + OR Delta == ""10C"" + OR Delta == ""10P"" + OR Delta == ""25C"" + OR Delta == ""25P"" + OR Delta == ""DN"") + GROUP BY + CurvePrefix, + CcyPair, + Type, + Tenor, + Delta, + CurveKey + LET + max( Order ) AS Order, + sum( Value ) AS Value + JOIN ""PnL-Market"" AS Market ON + $_id.CurveKey == _id + UNWIND Market + ADD + Order AS ""_id.Order"", + [ + { + ""OpeningCurve"" AS ""k"", + $Market.Opening AS ""v"" + }, + { + ""CurveMove"" AS ""k"", + $Market.Move AS ""v"" + }, + { + ""Value"" AS ""k"", + Value AS ""v"" + } + ] AS ""_id.Data"" + DO + { + ""$replaceRoot"": { + ""newRoot"": ""$_id"" + } + } + UNWIND Data + PROJECT + CurvePrefix, + CcyPair, + Tenor, + Order, + Delta, + cond( + if: $Data.k == ""OpeningCurve"" + OR $Data.k == ""CurveMove"", + then: $Data.k, + else: Type + ) AS Type, + $Data.v AS Value + GROUP BY + CurvePrefix, + CcyPair, + Tenor + LET + max( Order ) AS Order, + addToSet( + Name: concat( Delta, "" "", Type ), + Value: Value + ) AS Items + PROJECT + _id, + Order, + arrayToObject( zip( + inputs: [ + $Items.Name, + $Items.Value + ] + ) ) AS tmp + ADD + _id AS ""tmp._id"", + Order AS ""tmp.Order"" + DO + { + ""$replaceRoot"": { + ""newRoot"": ""$tmp"" + } + } + WHERE + 'DN OpeningVega' != NULL + AND 'DN OpeningVega' != NULL + REPLACE ID { + $_id.CcyPair AS CcyPair, + $_id.Tenor AS Tenor + } + Order, + 'DN OpeningCurve', + '10RR OpeningCurve', + '25RR OpeningCurve', + '10FLY OpeningCurve', + '25FLY OpeningCurve', + '25P OpeningCurve', + '10P OpeningCurve', + '10C OpeningCurve', + '25C OpeningCurve', + 'DN CurveMove', + '10RR CurveMove', + '25RR CurveMove', + '10FLY CurveMove', + '25FLY CurveMove', + '25P CurveMove', + '10P CurveMove', + '10C CurveMove', + '25C CurveMove', + 'DN OpeningVega', + '10RR OpeningVega', + '25RR OpeningVega', + '10FLY OpeningVega', + '25FLY OpeningVega', + '25P OpeningVega', + '10P OpeningVega', + '10C OpeningVega', + '25C OpeningVega', + 'DN VegaImpact', + '10RR VegaImpact', + '25RR VegaImpact', + '10FLY VegaImpact', + '25FLY VegaImpact', + '25P VegaImpact', + '10P VegaImpact', + '10C VegaImpact', + '25C VegaImpact', + 'DN Vega2ndOrderImpact', + '10RR Vega2ndOrderImpact', + '25RR Vega2ndOrderImpact', + '10FLY Vega2ndOrderImpact', + '25FLY Vega2ndOrderImpact', + '25P Vega2ndOrderImpact', + '10P Vega2ndOrderImpact', + '10C Vega2ndOrderImpact', + '25C Vega2ndOrderImpact' + SORT BY + $_id.CcyPair, + Order + PROJECT EXCLUDE + Order +"; + + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Sync the URL with the current tab page + NavigationManager.GetQueryParameters().TryGetValue("tab", out var tabPage); + ActivePage = tabPage ?? "Overview"; + SyncUrl(); + StateHasChanged(); + } + + return Task.CompletedTask; + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + "doc/afh"; + if (!string.IsNullOrWhiteSpace(ActivePage)) + url += $"?tab={Uri.EscapeDataString(ActivePage)}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private string ActivePage + { + get; + set + { + if (field == value) + return; + field = value; + SyncUrl(); + } + } = "Overview"; +} diff --git a/Rms.Risk.Mango/Pages/Doc/AllIcons.razor b/Rms.Risk.Mango/Pages/Doc/AllIcons.razor new file mode 100644 index 0000000..5902bf9 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Doc/AllIcons.razor @@ -0,0 +1,352 @@ +@page "/doc/all-icons" + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +

All Icons

+ + +@* *@ + +
+ @foreach (var icon in FilteredIcons.OrderBy(x => x.DisplayName)) + { +
+ + @icon.DisplayName +
+ } +
+ +@code { + // List of icon class names and their display names + private readonly List<(string ClassName, string DisplayName)> _icons = + [ + ("icon-alarm-clock-sm", "alarm-clock-sm"), + ("icon-cog-outline-sm", "cog-outline-sm"), + ("icon-ellipsis-sm", "ellipsis-sm"), + ("icon-image-sm", "image-sm"), + ("icon-mouse-left-sm", "mouse-left-sm"), + ("icon-single-chevron-down-sm", "single-chevron-down-sm"), + ("icon-single-chevron-right-sm", "single-chevron-right-sm"), + ("icon-single-chevron-up-sm", "single-chevron-up-sm"), + ("icon-star-half-sm", "star-half-sm"), + ("icon-undo-sm", "undo-sm"), + ("icon-unlock-outline-sm", "unlock-outline-sm"), + ("icon-unlock-sm", "unlock-sm"), + ("icon-unlock-wide-sm", "unlock-wide-sm"), + ("icon-wrench-outline-sm", "wrench-outline-sm"), + ("icon-ab-logo-sm", "ab-logo-sm"), + ("icon-calendar-sm", "calendar-sm"), + ("icon-caret-down-sm", "caret-down-sm"), + ("icon-caret-left-sm", "caret-left-sm"), + ("icon-caret-right-sm", "caret-right-sm"), + ("icon-caret-up-sm", "caret-up-sm"), + ("icon-cascade-sm", "cascade-sm"), + ("icon-chat-outline-sm", "chat-outline-sm"), + ("icon-chat-sm", "chat-sm"), + ("icon-checkmark-sm", "checkmark-sm"), + ("icon-clone-sm", "clone-sm"), + ("icon-close-circle-sm", "close-circle-sm"), + ("icon-close-sm", "close-sm"), + ("icon-cog-sm", "cog-sm"), + ("icon-document-blank-sm", "document-blank-sm"), + ("icon-document-download-sm", "document-download-sm"), + ("icon-document-sm", "document-sm"), + ("icon-document-upload-sm", "document-upload-sm"), + ("icon-download-selected-sm", "download-selected-sm"), + ("icon-download-sm", "download-sm"), + ("icon-flip-sm", "flip-sm"), + ("icon-folder-outline-sm", "folder-outline-sm"), + ("icon-folder-sm", "folder-sm"), + ("icon-layers-sm", "layers-sm"), + ("icon-mobile-sm", "mobile-sm"), + ("icon-pencil-sm", "pencil-sm"), + ("icon-play-sm", "play-sm"), + ("icon-print-sm", "print-sm"), + ("icon-save-outline-sm", "save-outline-sm"), + ("icon-save-sm", "save-sm"), + ("icon-search-sm", "search-sm"), + ("icon-single-chevron-left-sm", "single-chevron-left-sm"), + ("icon-trash-sm", "trash-sm"), + ("icon-user-db-outline-sm", "user-db-outline-sm"), + ("icon-user-db-sm", "user-db-sm"), + ("icon-user-details-outline-sm", "user-details-outline-sm"), + ("icon-user-details-sm", "user-details-sm"), + ("icon-user-group-outline-sm", "user-group-outline-sm"), + ("icon-user-group-sm", "user-group-sm"), + ("icon-wrench-sm", "wrench-sm"), + ("icon-arrow-down-sm", "arrow-down-sm"), + ("icon-arrow-left-sm", "arrow-left-sm"), + ("icon-arrow-right-sm", "arrow-right-sm"), + ("icon-arrow-up-sm", "arrow-up-sm"), + ("icon-bell-outline-sm", "bell-outline-sm"), + ("icon-bell-sm", "bell-sm"), + ("icon-chain-sm", "chain-sm"), + ("icon-chart-area-sm", "chart-area-sm"), + ("icon-chart-bar-sm", "chart-bar-sm"), + ("icon-chart-complex-line-sm", "chart-complex-line-sm"), + ("icon-chart-curve-sm", "chart-curve-sm"), + ("icon-chart-simple-line-sm", "chart-simple-line-sm"), + ("icon-clock-sm", "clock-sm"), + ("icon-funnel-clear-sm", "funnel-clear-sm"), + ("icon-funnel-sm", "funnel-sm"), + ("icon-globe-sm", "globe-sm"), + ("icon-grid-sm", "grid-sm"), + ("icon-join-sm", "join-sm"), + ("icon-list-sm", "list-sm"), + ("icon-lock-sm", "lock-sm"), + ("icon-merge-sm", "merge-sm"), + ("icon-message-outline-sm", "message-outline-sm"), + ("icon-message-sm", "message-sm"), + ("icon-moon-sm", "moon-sm"), + ("icon-pause-sm", "pause-sm"), + ("icon-phone-directory-sm", "phone-directory-sm"), + ("icon-phone-sm", "phone-sm"), + ("icon-plug-disconnected-sm", "plug-disconnected-sm"), + ("icon-print-outline-sm", "print-outline-sm"), + ("icon-reply-outline-sm", "reply-outline-sm"), + ("icon-reply-sm", "reply-sm"), + ("icon-revert-sm", "revert-sm"), + ("icon-star-outline-sm", "star-outline-sm"), + ("icon-star-sm", "star-sm"), + ("icon-tos-sm", "tos-sm"), + ("icon-window-add-sm", "window-add-sm"), + ("icon-zoom-in-sm", "zoom-in-sm"), + ("icon-zoom-out-sm", "zoom-out-sm"), + ("icon-annotate-sm", "annotate-sm"), + ("icon-attach-sm", "attach-sm"), + ("icon-bezir-curve-sm", "bezir-curve-sm"), + ("icon-contacts-cog-sm", "contacts-cog-sm"), + ("icon-context-help-outline-sm", "context-help-outline-sm"), + ("icon-context-help-sm", "context-help-sm"), + ("icon-context-info-outline-sm", "context-info-outline-sm"), + ("icon-context-info-sm", "context-info-sm"), + ("icon-duplicate-document-sm", "duplicate-document-sm"), + ("icon-excel-sm", "excel-sm"), + ("icon-exit-sm", "exit-sm"), + ("icon-first-aid-sm", "first-aid-sm"), + ("icon-forward-document-sm", "forward-document-sm"), + ("icon-hide-sm", "hide-sm"), + ("icon-link-sm", "link-sm"), + ("icon-mail-outline-sm", "mail-outline-sm"), + ("icon-mail-sm", "mail-sm"), + ("icon-menu-sm", "menu-sm"), + ("icon-pc-screen-sm", "pc-screen-sm"), + ("icon-plus-sm", "plus-sm"), + ("icon-reload-sm", "reload-sm"), + ("icon-running-man-sm", "running-man-sm"), + ("icon-upload-selected-sm", "upload-selected-sm"), + ("icon-upload-sm", "upload-sm"), + ("icon-user-outline-sm", "user-outline-sm"), + ("icon-user-sm", "user-sm"), + ("icon-view-sm", "view-sm"), + ("icon-warning-circle-sm", "warning-circle-sm"), + ("icon-warning-triangle-sm", "warning-triangle-sm"), + ("icon-weight-equal-sm", "weight-equal-sm"), + ("icon-weight-not-equal-sm", "weight-not-equal-sm"), + // Large icons + ("icon-ab-logo", "ab-logo"), + ("icon-adobe-reader", "adobe-reader"), + ("icon-alarm-clock", "alarm-clock"), + ("icon-annotate", "annotate"), + ("icon-arrow-down", "arrow-down"), + ("icon-arrow-enter", "arrow-enter"), + ("icon-arrow-left", "arrow-left"), + ("icon-arrow-right", "arrow-right"), + ("icon-arrow-up", "arrow-up"), + ("icon-attach", "attach"), + ("icon-bell", "bell"), + ("icon-bell-outline", "bell-outline"), + ("icon-bezir-curve", "bezir-curve"), + ("icon-boxing-glove", "boxing-glove"), + ("icon-calendar", "calendar"), + ("icon-caret-down", "caret-down"), + ("icon-caret-left", "caret-left"), + ("icon-caret-right", "caret-right"), + ("icon-caret-up", "caret-up"), + ("icon-caret-up-down", "caret-up-down"), + ("icon-cascade", "cascade"), + ("icon-chain", "chain"), + ("icon-chart-area", "chart-area"), + ("icon-chart-bar", "chart-bar"), + ("icon-chart-complex-line", "chart-complex-line"), + ("icon-chart-curve", "chart-curve"), + ("icon-chart-simple-line", "chart-simple-line"), + ("icon-chat", "chat"), + ("icon-chat-outline", "chat-outline"), + ("icon-checkmark", "checkmark"), + ("icon-clock", "clock"), + ("icon-clone", "clone"), + ("icon-close", "close"), + ("icon-close-circle", "close-circle"), + ("icon-cog", "cog"), + ("icon-cog-outline", "cog-outline"), + ("icon-contacts-cog", "contacts-cog"), + ("icon-context-help", "context-help"), + ("icon-context-help-outline", "context-help-outline"), + ("icon-context-info", "context-info"), + ("icon-context-info-outline", "context-info-outline"), + ("icon-db-logo", "db-logo"), + ("icon-document", "document"), + ("icon-document-add", "document-add"), + ("icon-document-blank", "document-blank"), + ("icon-document-code", "document-code"), + ("icon-document-download", "document-download"), + ("icon-document-forward", "document-forward"), + ("icon-document-upload", "document-upload"), + ("icon-double-chevron-down", "double-chevron-down"), + ("icon-double-chevron-left", "double-chevron-left"), + ("icon-double-chevron-right", "double-chevron-right"), + ("icon-double-chevron-up", "double-chevron-up"), + ("icon-download", "download"), + ("icon-download-selected", "download-selected"), + ("icon-drawer", "drawer"), + ("icon-duplicate-document", "duplicate-document"), + ("icon-ellipsis", "ellipsis"), + ("icon-envelope", "envelope"), + ("icon-envelope-outline", "envelope-outline"), + ("icon-excel", "excel"), + ("icon-exit", "exit"), + ("icon-first-aid", "first-aid"), + ("icon-flag", "flag"), + ("icon-flag-outline", "flag-outline"), + ("icon-flame", "flame"), + ("icon-flip", "flip"), + ("icon-folder", "folder"), + ("icon-folder-add", "folder-add"), + ("icon-folder-add-outline", "folder-add-outline"), + ("icon-folder-open", "folder-open"), + ("icon-folder-open-outline", "folder-open-outline"), + ("icon-folder-outline", "folder-outline"), + ("icon-folder-remove", "folder-remove"), + ("icon-folder-remove-outline", "folder-remove-outline"), + ("icon-funnel", "funnel"), + ("icon-funnel-clear", "funnel-clear"), + ("icon-gauge", "gauge"), + ("icon-globe", "globe"), + ("icon-grid", "grid"), + ("icon-help", "help"), + ("icon-hide", "hide"), + ("icon-history", "history"), + ("icon-image", "image"), + ("icon-info", "info"), + ("icon-join", "join"), + ("icon-ladder-down", "ladder-down"), + ("icon-ladder-up", "ladder-up"), + ("icon-layers", "layers"), + ("icon-link", "link"), + ("icon-list", "list"), + ("icon-list-items", "list-items"), + ("icon-lock", "lock"), + ("icon-menu", "menu"), + ("icon-merge", "merge"), + ("icon-message", "message"), + ("icon-message-outline", "message-outline"), + ("icon-mobile", "mobile"), + ("icon-moon", "moon"), + ("icon-mouse-left", "mouse-left"), + ("icon-number-outline", "number-outline"), + ("icon-paste", "paste"), + ("icon-pause", "pause"), + ("icon-pc-screen", "pc-screen"), + ("icon-pencil", "pencil"), + ("icon-phone", "phone"), + ("icon-phone-directory", "phone-directory"), + ("icon-phone-directory-outline", "phone-directory-outline"), + ("icon-phone-outline", "phone-outline"), + ("icon-pin-down", "pin-down"), + ("icon-pin-down-outline", "pin-down-outline"), + ("icon-pin-left", "pin-left"), + ("icon-pin-left-bottom", "pin-left-bottom"), + ("icon-pin-left-bottom-outline", "pin-left-bottom-outline"), + ("icon-pin-left-outline", "pin-left-outline"), + ("icon-play", "play"), + ("icon-plug", "plug"), + ("icon-plug-disconnected", "plug-disconnected"), + ("icon-plug-disconnected-outline", "plug-disconnected-outline"), + ("icon-plus", "plus"), + ("icon-printer", "printer"), + ("icon-printer-outline", "printer-outline"), + ("icon-reject", "reject"), + ("icon-reload", "reload"), + ("icon-reply", "reply"), + ("icon-reply-outline", "reply-outline"), + ("icon-revert", "revert"), + ("icon-rss", "rss"), + ("icon-running-man", "running-man"), + ("icon-save", "save"), + ("icon-save-outline", "save-outline"), + ("icon-search", "search"), + ("icon-single-chevron-down", "single-chevron-down"), + ("icon-single-chevron-left", "single-chevron-left"), + ("icon-single-chevron-right", "single-chevron-right"), + ("icon-single-chevron-up", "single-chevron-up"), + ("icon-sliders-horizontal", "sliders-horizontal"), + ("icon-star", "star"), + ("icon-star-half", "star-half"), + ("icon-star-outline", "star-outline"), + ("icon-text-bold", "text-bold"), + ("icon-text-italic", "text-italic"), + ("icon-thumbs-up", "thumbs-up"), + ("icon-tiles", "tiles"), + ("icon-tos", "tos"), + ("icon-trash", "trash"), + ("icon-txt", "txt"), + ("icon-undo", "undo"), + ("icon-unlock", "unlock"), + ("icon-unlock-outline", "unlock-outline"), + ("icon-unlock-wide", "unlock-wide"), + ("icon-upload", "upload"), + ("icon-upload-selected", "upload-selected"), + ("icon-user", "user"), + ("icon-user-add", "user-add"), + ("icon-user-add-outline", "user-add-outline"), + ("icon-user-db", "user-db"), + ("icon-user-db-outline", "user-db-outline"), + ("icon-user-details", "user-details"), + ("icon-user-group", "user-group"), + ("icon-user-group-outline", "user-group-outline"), + ("icon-user-outline", "user-outline"), + ("icon-user-remove", "user-remove"), + ("icon-user-remove-outline", "user-remove-outline"), + ("icon-user-star-outline", "user-star-outline"), + ("icon-view", "view"), + ("icon-warning-circle", "warning-circle"), + ("icon-warning-triangle", "warning-triangle"), + ("icon-weight-equal", "weight-equal"), + ("icon-weight-not-equal", "weight-not-equal"), + ("icon-window-add", "window-add"), + ("icon-window-group", "window-group"), + ("icon-wrench", "wrench"), + ("icon-wrench-double-head-outline", "wrench-double-head-outline"), + ("icon-wrench-outline", "wrench-outline"), + ("icon-xml", "xml"), + ("icon-zoom-in", "zoom-in"), + ("icon-zoom-out", "zoom-out") + ]; + + private string _searchText = string.Empty; + + private IEnumerable<(string ClassName, string DisplayName)> FilteredIcons => + string.IsNullOrWhiteSpace(_searchText) + ? _icons + : _icons.Where(x => x.DisplayName.Contains(_searchText, StringComparison.OrdinalIgnoreCase)); +} diff --git a/Rms.Risk.Mango/Pages/Doc/Doc.razor b/Rms.Risk.Mango/Pages/Doc/Doc.razor new file mode 100644 index 0000000..ff20280 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Doc/Doc.razor @@ -0,0 +1,153 @@ +@page "/doc" +@page "/doc/{FileName}" + +@using Markdig +@using Markdown.ColorCode + +@inject NavigationManager NavigationManager +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +
+
+ @((MarkupString)MarkdownIndex) +
+
+ @((MarkupString)MarkdownContent) +
+
+ +@code { + [Parameter] + public string FileName { get; set; } = string.Empty; + + private string MarkdownContent { get; set; } = ""; + private string MarkdownIndex { get; set; } = ""; + + protected override async Task OnParametersSetAsync() + { + if (string.IsNullOrWhiteSpace(FileName)) + FileName = "overview"; + + if (string.IsNullOrWhiteSpace(FileName)) + { + throw new ArgumentException("FileName parameter must be provided."); + } + + var filePath = Path.Combine("wwwroot", "docs", FileName+".md"); + if (!File.Exists(filePath)) + { + MarkdownContent = $"# Error\n\nFile `{FileName}.md` not found."; + return; + } + + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseLocalUrlHandler() + .UseColorCode() + .Build() + ; + + var markdown = await File.ReadAllTextAsync(filePath); + MarkdownContent = Markdown.ToHtml( + markdown, + pipeline + ); + + if (string.IsNullOrWhiteSpace(MarkdownIndex)) + { + filePath = Path.Combine("wwwroot", "docs", "index.md"); + markdown = await File.ReadAllTextAsync(filePath); + MarkdownIndex = Markdown.ToHtml( + markdown, + pipeline + ); + + } + + SyncUrl(); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"doc/{FileName}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + +} diff --git a/Rms.Risk.Mango/Pages/Doc/TextEditorDoc.razor b/Rms.Risk.Mango/Pages/Doc/TextEditorDoc.razor new file mode 100644 index 0000000..7862f7f --- /dev/null +++ b/Rms.Risk.Mango/Pages/Doc/TextEditorDoc.razor @@ -0,0 +1,71 @@ +@page "/doc/editor" + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Text Editor

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyMeaning
F11Full screen
EscExit full screen or close search dialog
Ctrl-SpaceAutocomplete
Ctrl-FFind
Ctrl-HFind and replace
Ctrl-QFold/unfold the code
Alt-GGoto line
+ +@code { + +} diff --git a/Rms.Risk.Mango/Pages/Error.cshtml b/Rms.Risk.Mango/Pages/Error.cshtml new file mode 100644 index 0000000..ea879a2 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Error.cshtml @@ -0,0 +1,42 @@ +@page +@model Rms.Risk.Mango.Pages.ErrorModel + + + + + + + + Error + + + + + +
+
+

Error.

+

An error occurred while processing your request.

+ + @if (Model.ShowRequestId) + { +

+ Request ID: @Model.RequestId +

+ } + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+
+
+ + + diff --git a/Rms.Risk.Mango/Pages/Error.cshtml.cs b/Rms.Risk.Mango/Pages/Error.cshtml.cs new file mode 100644 index 0000000..9a3b9bc --- /dev/null +++ b/Rms.Risk.Mango/Pages/Error.cshtml.cs @@ -0,0 +1,40 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; + +namespace Rms.Risk.Mango.Pages; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorModel(ILogger logger) : PageModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + // ReSharper disable once UnusedMember.Local + private readonly ILogger _logger = logger; + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/Index.razor b/Rms.Risk.Mango/Pages/Index.razor new file mode 100644 index 0000000..ff26ed3 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Index.razor @@ -0,0 +1,247 @@ +@page "/" +@* @page "/{DatabaseStr}" +@page "/{DatabaseStr}/{DatabaseInstanceStr}" *@ +@using Microsoft.Extensions.Options +@using Rms.Risk.Mango.Services.Context +@using Rms.Service.Bootstrap +@using Rms.Risk.Mango.Pivot.Core.MongoDb; + +@inject NavigationManager NavigationManager +@inject IDatabaseConfigurationService DatabaseConfig +@inject IUserSession Session +@inject IJSRuntime JsRuntime +@inject IAuthorizationService Auth +@inject IOptions DbMangoSettings + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + +Index + + + +
+
+
+

dbMango

+
Version: @(VersionsHelper.Version)
+
+
+
+
+ @if (DatabasesNames.Count == 0) + { +
It looks like you don't have access to any databases...
+ } + else + { +
+ +
+ @if (Session.IsDatabaseInstanceSelectionAllowed) + { +
+ +
+ } + @if (Error != null) + { + + } +
+ +
+ } +
+
+
Databases you don't have access to:
+ + + + +
+
+
+ + + + + + + + +
+ @if (!string.IsNullOrWhiteSpace(DbMangoSettings.Value.SupportLinkLabel)) + { + + } + +
+
+
+ + +
+

dbMango

+
Version: @(VersionsHelper.Version)
+ +
 
+
Welcome to dbMango - MongoDB administrative tool! Please, consider logging in.
+ @if (!string.IsNullOrWhiteSpace(DbMangoSettings.Value.SupportLinkLabel)) + { + + } +
+
+ +@code{ + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private List DatabasesNames { get; } = []; + private List DatabaseInstances { get; set; } = []; + private Exception? Error { get; set; } + + private record NoAccessToRec(string Name, string Contacts); + + private List NoAccessTo { get; } = []; + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + DatabasesNames.Clear(); + foreach (var name in DatabaseConfig.Databases.Keys.OrderBy(x => x)) + { + if (name.Contains("-placeholder>")) + continue; + + if (!await IsUserAuthorized(name)) + NoAccessTo.Add( new (name, DatabaseConfig.Databases[name].Contacts)); + else + DatabasesNames.Add(name); + } + + if (string.IsNullOrWhiteSpace(DatabaseStr) || !await IsUserAuthorized(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + await InvokeAsync(StateHasChanged); + } + + private string Database + { + get => Session.Database; + set + { + if (Session.Database == value) + return; + + Session.Database = value; + SyncUrl(); + + _ = Task.Run(DatabaseChanged); + } + } + + private string DatabaseInstance + { + get => Session.DatabaseInstance; + set + { + if (Session.DatabaseInstance == value) + return; + + Session.DatabaseInstance = value; + SyncUrl(); + InvokeAsync(StateHasChanged); + } + } + + private async Task DatabaseChanged() + { + Error = null; + await InvokeAsync(StateHasChanged); + try + { + DatabaseInstances = await GetDatabasesInstanceNames(); + + if (DatabaseInstances.All(x=> x != DatabaseInstance) ) + DatabaseInstance = DatabaseInstances.First(); + + SyncUrl(); + } + catch (Exception ex) + { + Error = ex; + } + await InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + // var url = NavigationManager.BaseUri + $"{Database}"; + // if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + // url += $"/{DatabaseInstance}"; + // JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task> GetDatabasesInstanceNames() + { + var admin = Session.MongoDbAdminForAdminDatabase; + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var databases = await admin.ListDatabases(cts.Token); + return databases.Select(x => x.Name).ToList(); + } + + private async Task IsUserAuthorized(string database) + { + var readAccess = await Auth.AuthorizeAsync( + Session.User.GetUser(), + database, + [new ReadAccessRequirement()]); + return readAccess.Succeeded; + } + + private void NavigateToBrowse() => NavigationManager.NavigateTo("/user/browse"); + private void NavigateToPivot() => NavigationManager.NavigateTo("/user/pivot"); + private void NavigateToAfh() => NavigationManager.NavigateTo("/user/afh"); + private void NavigateToShell() => NavigationManager.NavigateTo("/admin/shell"); + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/Login.cshtml b/Rms.Risk.Mango/Pages/Login.cshtml new file mode 100644 index 0000000..77bd788 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Login.cshtml @@ -0,0 +1,8 @@ +@page "{ReturnUrl?}" + +@using Microsoft.AspNetCore.Authorization + +@attribute [Authorize] +@model Rms.Risk.Mango.Pages.LoginModel +@{ +} diff --git a/Rms.Risk.Mango/Pages/Login.cshtml.cs b/Rms.Risk.Mango/Pages/Login.cshtml.cs new file mode 100644 index 0000000..e1a79d8 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Login.cshtml.cs @@ -0,0 +1,35 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#nullable disable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Rms.Risk.Mango.Pages; + +public class LoginModel : PageModel +{ + public IActionResult OnGet([FromRoute] string returnUrl = null) => + LocalRedirect("/" + (string.IsNullOrEmpty(returnUrl) ? "" : Base64Decode(returnUrl))); + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/LoginFailed.razor b/Rms.Risk.Mango/Pages/LoginFailed.razor new file mode 100644 index 0000000..9fe5ed0 --- /dev/null +++ b/Rms.Risk.Mango/Pages/LoginFailed.razor @@ -0,0 +1,47 @@ +@page "/login-failed" + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Login failed

+ +@if ( !string.IsNullOrWhiteSpace( Message ) ) +{ +
+ @Message +
+} + +

+ Login failed. dbMango uses OAuth2/OIDC for user authentication. + Make sure yu are using authorized email address and your password is correct. +

+ + +@code { + [Parameter] + [SupplyParameterFromQuery(Name = "m")] + public string Message { get; set; } = ""; +} diff --git a/Rms.Risk.Mango/Pages/Logout.cshtml b/Rms.Risk.Mango/Pages/Logout.cshtml new file mode 100644 index 0000000..e40505b --- /dev/null +++ b/Rms.Risk.Mango/Pages/Logout.cshtml @@ -0,0 +1,4 @@ +@page +@model Rms.Risk.Mango.Pages.LogoutModel +@{ +} diff --git a/Rms.Risk.Mango/Pages/Logout.cshtml.cs b/Rms.Risk.Mango/Pages/Logout.cshtml.cs new file mode 100644 index 0000000..0988b62 --- /dev/null +++ b/Rms.Risk.Mango/Pages/Logout.cshtml.cs @@ -0,0 +1,35 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Rms.Risk.Mango.Pages; + +public class LogoutModel : PageModel +{ + public async Task OnGet() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + return LocalRedirect("/"); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/User/Aggregate.razor b/Rms.Risk.Mango/Pages/User/Aggregate.razor new file mode 100644 index 0000000..8cebb14 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Aggregate.razor @@ -0,0 +1,546 @@ +@page "/user/aggregate" +@page "/user/aggregate/{DatabaseStr}/{CollectionStr}" +@page "/user/aggregate/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] +@using MongoDB.Bson.Serialization + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Aggregate: @SelectedCollection

+ + + + +
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+ @for (var i = 0; i < Stages.Count; i++) + { + var index = i; + +
+ + + @if (index > 0) + { + + } + @if (index < Stages.Count - 1) + { + + } + @if (index > 0) + { + + } + +
+ +
+ +
+ } +
+ +
+ @if (ShowAsJson) + { + + } + else + { + + } +
+
+
+ +@code { + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private class StageRec + { + public string Text { get; set; } = @"{ + ""_id"": { + ""$ne"": """" + } +}"; + public bool Use { get; set; } = true; + public string Type { get; set; } = "match"; + } + + private List Stages { get; set; } = [new ()]; + + private static readonly string[] _availableStages = + new[]{ + "addFields", + "bucket", + "bucketAuto", + "changeStream", + "changeStreamSplitLargeEvent", + "collStats", + "count", + "currentOp", + "densify", + "documents", + "facet", + "fill", + "geoNear", + "graphLookup", + "group", + "indexStats", + "limit", + "listLocalSessions", + "listSampledQueries", + "listSearchIndexes", + "listSessions", + "lookup", + "match", + "merge", + "out", + "planCacheStats", + "project", + "querySettings", + "queryStats", + "redact", + "replaceRoot", + "replaceWith", + "sample", + "search", + "searchMeta", + "set", + "setWindowFields", + "shardedDataDistribution", + "skip", + "sort", + "sortByCount", + "unionWith", + "unset", + "unwind", + "vectorSearch", + }.OrderBy(x => x).ToArray(); + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private string Timeout { get; set; } = "20"; + private string FetchSize { get; set; } = "10"; + private bool IsReady { get; set; } + private bool ShowAsJson { get; set; } + + private string Result { get; set; } = "{}"; + private List ResultBson { get; set; } = []; + public ArrayBasedPivotData ResultPivot { get; set; } = new([]); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + protected override void OnInitialized() + { + _editContext = new (this); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + SyncUrl(); + + var text = await JsRuntime.LoadFromLocalStorage("Aggregate"); + if ( !string.IsNullOrWhiteSpace(text)) + UpdateStages(text); + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + } + + private async Task Format() + { + foreach (var stage in Stages) + { + if (!BsonDocument.TryParse(stage.Text, out var doc)) + continue; + stage.Text = doc.ToJson(new() { Indent = true }); + } + + await InvokeAsync(StateHasChanged); + } + + private Task Run() => Run(false); + private Task Explain() => Run(true); + + private async Task Run(bool explain) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var text = GetCombinedPipelineText(); + + await JsRuntime.SaveToLocalStorage("Aggregate", text, cts.Token); + + var res = explain + ? await RunExplain(text, cts.Token) + : await RunAggregate(text, cts.Token) + ; + ResultPivot = res.Item1; + ResultBson = res.Item2; + + await Display(ResultBson); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private string GetCombinedPipelineText() + { + var text = "[" + string.Join(",", Stages.Where(x => x.Use && !string.IsNullOrWhiteSpace(x.Text)).Select(CombineStage)) + "]"; + var arr = BsonSerializer.Deserialize(text); + return arr.ToJson(new() { Indent = true }); + } + + private string CombineStage(StageRec arg) => $"{{ ${arg.Type} : {arg.Text} }}"; + + private Task Display(Exception e) + { + ShowAsJson = true; + Result = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + Result = res.ToJson(new() { Indent = true }); + return InvokeAsync(StateHasChanged); + } + + private async Task<(ArrayBasedPivotData, List)> RunExplain(string text, CancellationToken token) + { + var service = UserSession.MongoDb; + var command = $@"{{ + ""aggregate"" : ""{SelectedCollection}"", + ""pipeline"" : {text}, + ""cursor"" : {{}} +}}"; + var res = await service.ExplainAsync(command, token); + ShowAsJson = true; + return (ArrayBasedPivotData.NoData, [res]); + } + + private async Task<(ArrayBasedPivotData, List)> RunAggregate(string text, CancellationToken token) + { + var service = UserSession.MongoDb; + var results = service.AggregateAsyncRaw(text, int.Parse(FetchSize), token: token); + + var fieldMap = new ConcurrentDictionary + { + ["find"] = new(false) + }; + + var (pd, list) = await MongoDbDataSource.FetchPivotData( + "find", + "find", + fieldMap, + results, + null, + true, + int.Parse(FetchSize), + false, + token); + + if (list.Count == 0) + list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {SelectedCollection}""}}")); + + return (pd, list); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/aggregate/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task AddStageAfter(int i) + { + Stages.Insert(i+1,new()); + await InvokeAsync(StateHasChanged); + } + + private async Task RemoveStage(int i) + { + if (i == 0) + return; + + Stages.RemoveAt(i); + await InvokeAsync(StateHasChanged); + } + + private async Task MoveStageUp(int i) + { + if (i == 0) + return; + + var item = Stages[i]; + Stages.RemoveAt(i); + Stages.Insert(i - 1, item); + await InvokeAsync(StateHasChanged); + } + + private async Task MoveStageDown(int i) + { + if (i >= Stages.Count-1) + return; + + var item = Stages[i]; + Stages.RemoveAt(i); + Stages.Insert(i+1, item); + await InvokeAsync(StateHasChanged); + } + + private async Task Copy() + { + var text = GetCombinedPipelineText(); + + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", text); + } + + private async Task Paste() + { + try + { + Result = ""; + await InvokeAsync(StateHasChanged); + + var text = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + UpdateStages(text); + + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + } + + private void UpdateStages(string text) + { + try + { + text = string.IsNullOrWhiteSpace(text) + ? "" + : text.Trim() + ; + + if (!text.StartsWith("[") || text.StartsWith("]")) + return; + + var arr = BsonSerializer.Deserialize(text); + if (arr == null || arr.Count == 0) + return; + + var newStages = new List(); + foreach (var stage in arr.OfType().Where(x => x.ElementCount == 1)) + { + var type = stage.ElementAt(0).Name; + var txt = stage.ElementAt(0).Value.ToJson(new(){Indent = true}); + newStages.Add(new() { Type = type[1..], Text = txt, Use = true }); + } + + Stages.Clear(); + Stages.AddRange(newStages); + } + catch (Exception) + { + // ignore + } + + if ( Stages.Count == 0 ) + Stages.Add(new()); + } + + private string GetStageType(int index) => Stages[index].Type; + + private void SetStageType(int index, string value) + { + Stages[index].Type = value; + } + +} diff --git a/Rms.Risk.Mango/Pages/User/AggregateScript.razor b/Rms.Risk.Mango/Pages/User/AggregateScript.razor new file mode 100644 index 0000000..269aace --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/AggregateScript.razor @@ -0,0 +1,435 @@ +@page "/user/afh" +@page "/user/afh/{DatabaseStr}" +@page "/user/afh/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] +@using Rms.Risk.Mango.Language +@using Rms.Risk.Mango.Language.Parsers + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Aggregation for humans

+ + + + +
+ + + @* *@ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ See: + Aggregation pipeline + , + Stages + , + Script help + , + Keys +
+
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ +
+ + + + + +
+ +
+
+
+
+
+
+ +@code { + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string Timeout { get; set; } = "20"; + private string FetchSize { get; set; } = "10"; + private bool IsReady { get; set; } = true; + // private bool ShowAsJson { get; set; } + private string ResultActivePage + { + get; + set + { + if (field == value) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } = "Table"; + + + private string Text { get; set; } = ""; + private string TextJson { get; set; } = ""; + private string Result { get; set; } = "{}"; + private List ResultBson { get; set; } = []; + public ArrayBasedPivotData ResultPivot { get; set; } = new([]); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + protected override void OnInitialized() + { + _editContext = new (this); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + + var text = await JsRuntime.LoadFromLocalStorage("AggregationForHumans"); + if (!string.IsNullOrWhiteSpace(text)) + { + Text = text; + await InvokeAsync(StateHasChanged); + } + } + + private async Task Format() + { + + await InvokeAsync(StateHasChanged); + } + + private Task Run() => Run(false); + private Task Explain() => Run(true); + + private async Task Run(bool explain) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var pipelineJson = GetCombinedPipelineJson(); + await UpdatePipelineJson(); + + await JsRuntime.SaveToLocalStorage("AggregationForHumans", Text, cts.Token); + + UserSession.Collection = GetSelectedCollection(); + + var res = explain + ? await RunExplain(pipelineJson, cts.Token) + : await RunAggregate(pipelineJson, cts.Token) + ; + ResultPivot = res.Item1; + ResultBson = res.Item2; + + await Display(ResultBson); + } + catch (SyntaxErrorException ex) + { + var row = ex.Line; + var col = ex.Position; + + try + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CodeEditor_SetCaret", CancellationToken.None, row, col); + } + catch (Exception) + { + // exception + } + + await Display(ex); + + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private static readonly System.Text.Json.JsonSerializerOptions _prettyPrint = new () + { + WriteIndented = true + }; + + + private string GetCombinedPipelineJson() + { + var ast = LanguageParser.ParseScriptToAST(Text); + var json = ast.AsJson(); + + var result = json?.ToJsonString(_prettyPrint) ?? ""; + return result; + } + + private Task Display(Exception e) + { + ResultActivePage = "Json"; + Result = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + Result = res.ToJson(new() { Indent = true }); + return InvokeAsync(StateHasChanged); + } + + private string GetSelectedCollection() + { + try + { + var ast = LanguageParser.ParseScriptToAST(Text); + return ast.Collection; + } + catch (Exception) + { + return ""; + } + } + + private async Task<(ArrayBasedPivotData, List)> RunExplain(string json, CancellationToken token) + { + var service = UserSession.MongoDb; + var command = $@"{{ + ""aggregate"" : ""{GetSelectedCollection()}"", + ""pipeline"" : {json}, + ""cursor"" : {{}} +}}"; + var res = await service.ExplainAsync(command, token); + ResultActivePage = "Json"; + return (ArrayBasedPivotData.NoData, [res]); + } + + private async Task<(ArrayBasedPivotData, List)> RunAggregate(string text, CancellationToken token) + { + var service = UserSession.MongoDb; + var results = service.AggregateAsyncRaw(text, token: token); + + var fieldMap = new ConcurrentDictionary + { + ["find"] = new(false) + }; + + var (pd, list) = await MongoDbDataSource.FetchPivotData( + "find", + "find", + fieldMap, + results, + null, + true, + int.Parse(FetchSize), + false, + token); + + if (list.Count == 0) + list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {GetSelectedCollection()}""}}")); + + return (pd, list); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/afh/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + + private async Task Copy() + { + await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", Text); + } + + private async Task Paste() + { + try + { + Result = ""; + await InvokeAsync(StateHasChanged); + + var text = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); + + if (string.IsNullOrWhiteSpace(text)) + return; + + if (text[..Math.Min(text.Length, 100)].Trim().StartsWith("[{")) + { + try + { + var ast = LanguageParser.ParseAggregationJsonToAST("", text); + text = ast.AsText(); + } + catch (Exception) + { + // ignore + } + } + + Text = text; + + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + } + + private async Task OnActivePageChanged(string arg) + { + if (arg != "Json pipeline") + return; + + await UpdatePipelineJson(); + } + + private async Task UpdatePipelineJson() + { + try + { + var json = GetCombinedPipelineJson(); + TextJson = json; + } + catch (Exception e) + { + await Display(e); + } + + await InvokeAsync(StateHasChanged); + } + +} diff --git a/Rms.Risk.Mango/Pages/User/Audit.razor b/Rms.Risk.Mango/Pages/User/Audit.razor new file mode 100644 index 0000000..0b7b9bf --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Audit.razor @@ -0,0 +1,312 @@ +@page "/user/audit" +@page "/user/audit/{DatabaseStr}" +@page "/user/audit/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] +@using Rms.Risk.Mango.Services.Audit + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Audit

+ + + + +
+ + +
+ + +
+
+ +
+ + @if (ShowAsJson) + { + + } + else + { + + } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string FetchSize { get; set; } = "100"; + private bool IsReady { get; set; } = true; + private bool ShowAsJson { get; set; } + private DateOnly? StartDate { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(7)); + private DateOnly? EndDate { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow); + + private string Result { get; set; } = "{}"; + private List ResultBson { get; set; } = []; + private ArrayBasedPivotData ResultPivot { get; set; } = new([]); + + + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + protected override void OnInitialized() + { + _editContext = new (this); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + UserSession.Collection = AuditService.AuditCollection; + + SyncUrl(); + } + + private async Task Run() + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var res = await RunAudit(cts.Token); + // var res = await RunAuditFind(cts.Token); + + ResultPivot = res.Item1; + ResultBson = res.Item2; + + await Display(ResultBson); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private Task Display(Exception e) + { + ShowAsJson = true; + Result = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + Result = res.ToJson(new() { Indent = true }); + return InvokeAsync(StateHasChanged); + } + + private async Task<(ArrayBasedPivotData, List)> RunAudit(CancellationToken token) + { + if (StartDate == null || EndDate == null) + return (ArrayBasedPivotData.NoData,[]); + + var audit = UserSession.Audit; + var res = await audit.Audit(StartDate.Value.ToDateTime(TimeOnly.MinValue), EndDate.Value.ToDateTime(TimeOnly.MaxValue), token); + + var pd = new ArrayBasedPivotData([ + "Timestamp UTC", + "Collection" , + "Command" , + "Success" , + "Ticket" , + "Email" , + "Comment" , + "Error" , + "Json" , + "_id" + ]); + + var list = new List(); + var id = 0; + foreach (var auditRecord in res) + { + var row = new object?[pd.Headers.Count]; + var i = 0; + + row[i++] = auditRecord.Timestamp; + row[i++] = auditRecord.Command.ElementAt(0).Value.ToString() ?? ""; + row[i++] = auditRecord.Command.ElementAt(0).Name; + row[i++] = auditRecord.Success; + row[i++] = auditRecord.Ticket; + row[i++] = auditRecord.Email; + row[i++] = auditRecord.Command.GetValue("comment", "").ToString(); + row[i++] = auditRecord.Error; + row[i++] = auditRecord.Command.ToJson(new(){ Indent = true }); + row[i ] = ++id; + + var json = auditRecord.ToJson(new() { Indent = true }); + var d = BsonDocument.Parse(json); + d["_id"] = id; + + pd.Add(row); + list.Add(d); + } + + if (list.Count == 0) + list.Add(BsonDocument.Parse("{\"Result\": \"No data fetched\"}")); + else + { + var newPd = (ArrayBasedPivotData)pd.FilterColumns( + [ + "Timestamp UTC" , + "Collection" , + "Command" , + "Success" , + "Ticket" , + "Email" , + "Comment" , + "Error" , + "_id" + ]); + pd = newPd; + } + + return (pd, list); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/audit/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task OnCellClick(DynamicObject row, string columnName) + { + try + { + var id = TableControl.GetDynamicMember(row, "_id"); + if (id == null) + return true; + + var bson = ResultBson.FirstOrDefault(x => x["_id"].ToString() == id.ToString()); + if (bson == null) + return true; + +// var oldId = bson["_id"]; + + // _ = await Auth.AuthorizeAsync( + // UserSession.User.GetUser(), + // UserSession.Database, + // [new ReadAccessRequirement()]); + + _ = await Find.ShowBsonDialog(Modal, id.ToString()!, bson, false); + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e); + } + return true; + } + + + private PivotColumnDescriptor? GetColumnDescriptor(string column) + { + if (column != "Timestamp UTC") + return null; + + return new() + { + Format = "yyyy-MM-dd HH:mm:ss" + }; + } + +} diff --git a/Rms.Risk.Mango/Pages/User/Browse.razor b/Rms.Risk.Mango/Pages/User/Browse.razor new file mode 100644 index 0000000..cfb5632 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Browse.razor @@ -0,0 +1,678 @@ +@page "/user/browse" +@page "/user/browse/{DatabaseStr}/{CollectionStr}" +@page "/user/browse/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@using Rms.Risk.Mango.Language +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@implements IDisposable + +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IAuthorizationService Auth + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Browse: @SelectedCollection

+ + + + + +
+ + + + +
+
+
+ +
+
+ +
+
+ +
+ + + +
+ + +
+
@context
+ @if (Stats.TryGetValue(context, out var stat)) + { + if (stat.Sharded) + { +
Sharded
+ } +
@NumbersUtils.ToHumanReadable(stat.TotalSize)
+ } +
+
+
+
+
+ +
+ @* *@ + + + @if ( !IsReady ) + { + + } + else if (ShowAsJson) + { + + } + else + { + + } +
+
+
+ + +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + + + [Parameter] + [SupplyParameterFromQuery(Name = "q")] + public string Text { get; set; } = ""; + + private List Collections { get; set; } = []; + private ConcurrentDictionary Stats { get; set; } = new(StringComparer.OrdinalIgnoreCase); + private static readonly List _rowValues = Enumerable.Range(0, 8).Select(x => x * 5 + 25).ToList(); + private Dictionary> AllHistory { get; set; } = []; + + private int Rows + { + get; + set + { + if (field == value) + return; + field = value; + InvokeAsync(StateHasChanged); + } + } = 35; + + private List History + { + get + { + var key = $"{Database}|{DatabaseInstance}|{SelectedCollection}"; + if ( AllHistory.TryGetValue(key, out var hist) ) + { + hist.Sort(StringComparer.OrdinalIgnoreCase); + return hist; + } + hist = []; + AllHistory[key] = hist; + return hist; + } + } + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + ShowDropdown = false; + Text = ""; + SyncUrl(); + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + InvokeAsync(StateHasChanged); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string Timeout { get; set; } = "20"; + private string FetchSize { get; set; } = "100"; + private bool IsReady { get; set; } + private bool ShowAsJson { get; set; } + private bool ShowDropdown { get; set; } + + private string Result { get; set; } = "{}"; + private List ResultBson { get; set; } = []; + public ArrayBasedPivotData ResultPivot { get; set; } = new([]); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + private readonly CancellationTokenSource _globalCancellation = new(); + + private static string GetCollectionClass(string collectionName) => + collectionName.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) + ? "meta" + : ""; + + protected override void OnInitialized() + { + _editContext = new (this); + } + + public void Dispose() + { + _globalCancellation.Cancel(); + _globalCancellation.Dispose(); + } + + + + private class BrowseState + { + public string Text { get; set; } = ""; + public int Rows { get; set; } = 25; + public Dictionary> History { get; set; } = []; + public string? Collection { get; set; } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); + var state = await GetState(cts.Token); + + if ( state != null ) + { + if ( state.History.Any() ) + AllHistory = state.History; + Rows = state.Rows; + if ( string.IsNullOrWhiteSpace(Text) && !string.IsNullOrWhiteSpace(state.Text)) + Text = state.Text; + if (string.IsNullOrWhiteSpace(CollectionStr) && !string.IsNullOrWhiteSpace(state.Collection)) + CollectionStr = state.Collection; + } + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + SyncUrl(); + + + if (Text is "null" or null) + Text = ""; + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + var cts2 = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); + var grouped = (await admin.ListCollections(cts2.Token)) + .GroupBy(x => + x.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) + ? " Meta" + : x.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) + ? " Cache" + : " Data" + ); + + Collections.Clear(); + foreach( var group in grouped) + { + Collections.Add(group.Key); + Collections.AddRange(group.OrderBy(x => x).Select(x => x)); + } + + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault( x => !x.StartsWith(" ")) ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + + _ = Task.Run(LoadCollStats, _globalCancellation.Token); + } + catch (Exception e) + { + await Display(e); + } + }, _globalCancellation.Token); + } + + private async Task LoadCollStats() + { + var admin = UserSession.MongoDbAdmin; + var collections = Collections.ToList(); // make a copy to avoid modifying the original list during iteration + + foreach (var name in collections) + { + try + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); + + var doc = await admin.CollStats(name, cts.Token); + doc.Details = null; // Remove details to avoid large output + + Stats[name] = doc; + await InvokeAsync(StateHasChanged); + } + catch (Exception) + { + // ignore + } + } + } + + private async Task GetState(CancellationToken token) + { + try + { + var json = await JsRuntime.LoadFromLocalStorage("Browse", token); + + if (string.IsNullOrWhiteSpace(json)) + return null; + + return JsonUtils.FromJson(json); + } + catch (Exception) + { + return null; + } + } + + enum ActionType + { + Find + } + + private async Task Run(ActionType actionType) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + ShowDropdown = false; + await InvokeAsync(StateHasChanged); + + await SaveState(cts.Token); + + if (int.TryParse(FetchSize, out var limit) == false) + limit = 0; + + var json = ""; + if (!string.IsNullOrWhiteSpace(Text)) + { + var afh = $$"""FROM "{{SelectedCollection}}" PIPELINE { WHERE {{Text}} }"""; + try + { + var ast = LanguageParser.ParseScriptToAST(afh); + json = ast.AsJson()!.ToJsonString(new() { WriteIndented = true }); + } + catch (Exception e) + { + await Display(e); + return; + } + } + + var res = actionType switch + { + ActionType.Find => await RunFind(json, limit, cts.Token), + _ => throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null) + }; + + ResultPivot = res.Item1; + ResultBson = res.Item2; + + if (!History.Contains(Text)) + { + History.Insert(0, Text); + await SaveState(cts.Token); + } + + SyncUrl(); + + await Display(ResultBson); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private async Task SaveState(CancellationToken token) + { + try + { + foreach( var key in AllHistory.Keys.ToList()) + { + var hist = AllHistory[key]; + AllHistory[key] = hist.Distinct(StringComparer.OrdinalIgnoreCase).Take(100).ToList(); + } + + var state = new BrowseState + { + Text = Text, + Collection = SelectedCollection, + Rows = Rows, + History = AllHistory + }; + + var json = JsonUtils.ToJson(state); + await JsRuntime.SaveToLocalStorage("Browse", json, token); + } + catch (Exception) + { + // Ignore errors during saving state + // This is not critical, and we can continue without saving + } + } + private Task Run() => Run(ActionType.Find); + + private Task Display(Exception e) + { + ShowAsJson = true; + Result = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + Result = res.ToJson(new() { Indent = true }); + return InvokeAsync(StateHasChanged); + } + + private async Task<(ArrayBasedPivotData, List)> RunFind(string text, int limit, CancellationToken token) + { + var service = UserSession.MongoDb; + var results = + string.IsNullOrWhiteSpace(text) || text.Trim() == "{}" + ? service.FindAsync("{}", limit: limit <= 0 ? -1 : limit, token: token) + : service.AggregateAsyncRaw(text, limit <= 0 ? -1 : limit, token: token); + + + var fieldMap = new ConcurrentDictionary + { + ["find"] = new(false) + }; + + var (pd, list) = await MongoDbDataSource.FetchPivotData( + "find", + "find", + fieldMap, + results, + null, + true, + int.Parse(FetchSize), + true, + token); + + if (list.Count == 0) + list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {SelectedCollection}""}}")); + + return (pd, list); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/browse/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + if (!string.IsNullOrWhiteSpace(Text)) + url += $"?q={Uri.EscapeDataString(Text)}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task OnCellClick(DynamicObject row, string columnName) + { + try + { + var id = TableControl.GetDynamicMember(row, "_id"); + if (id == null) + return true; + + var bson = ResultBson.FirstOrDefault(x => x["_id"].ToString() == id.ToString()); + if (bson == null) + return true; + + var oldId = bson["_id"]; + + var enableWrite = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + UserSession.Database, + [new WriteAccessRequirement()]); + + var res = await Find.ShowBsonDialog(Modal, id.ToString()!, bson, enableWrite.Succeeded); + if (res == null) + return true; + + if (enableWrite.Succeeded) + { + if (res.Value.ShouldUpdate) + { + bson = BsonDocument.Parse(res.Value.Json); + await OnUpdate(oldId, bson); + } + else + await OnDelete(oldId); + } + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e); + } + return true; + } + + private async Task OnDelete(BsonValue id) + { + var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Delete {id}", "Are you sure to delete document?"); + if (r.Cancelled) + return; + + var command = BsonDocument.Parse($@"{{ + ""delete"": ""{SelectedCollection}"", + ""deletes"": [{{ + ""q"": {{ }}, + ""limit"": 1 + }}] +}}"); + + var idDoc = new BsonDocument + { + ["_id"] = id + }; + + command["deletes"][0]["q"] = idDoc; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); + + var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); + var deleted = res["n"].ToInt32(); + if (deleted != 1) + { + Result = + res.ToJson(new() { Indent = true }) + + "\n------------------------------------------------------------------\n"+ + command.ToJson(new() { Indent = true }); + ShowAsJson = true; + await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Failure"); + await InvokeAsync(StateHasChanged); + } + else + await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Success"); + } + + private async Task OnUpdate(BsonValue id, BsonDocument newBson) + { + var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Update {id}", "Are you sure to update document?"); + if (r.Cancelled) + return; + + var command = BsonDocument.Parse($@"{{ + update: ""{SelectedCollection}"", + updates: [ + {{ + q: {{ }}, + u: {{ }}, + upsert: false, + multi: false, + }} + ], + ordered: false, + bypassDocumentValidation: false +}}"); + + var idDoc = new BsonDocument + { + ["_id"] = id + }; + + command["updates"][0]["q"] = idDoc; + command["updates"][0]["u"] = newBson; + + Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); + + var modified = res["nModified"].ToInt32(); + if (modified != 1) + { + Result = + res.ToJson(new() { Indent = true }) + + "\n------------------------------------------------------------------\n"+ + command.ToJson(new() { Indent = true }); + ShowAsJson = true; + await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Failure"); + await InvokeAsync(StateHasChanged); + } + else + await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Success"); + } + + private bool IsSelectable(string arg) => !arg.StartsWith(" "); + +} diff --git a/Rms.Risk.Mango/Pages/User/ConnectedUsers.razor b/Rms.Risk.Mango/Pages/User/ConnectedUsers.razor new file mode 100644 index 0000000..be49bd6 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/ConnectedUsers.razor @@ -0,0 +1,34 @@ +@page "/user/connected-users" +@using System.Diagnostics +@attribute [Authorize] + +@inject IConnectedUserList ConnectedUserList + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Active users

+ + + + + + + +

Server uptime: @(DateTime.Now - Process.GetCurrentProcess().StartTime).

\ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/User/Delete.razor b/Rms.Risk.Mango/Pages/User/Delete.razor new file mode 100644 index 0000000..6e5bec0 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Delete.razor @@ -0,0 +1,282 @@ +@page "/user/delete" +@page "/user/delete/{DatabaseStr}/{CollectionStr}" +@page "/user/delete/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] +@using Rms.Risk.Mango.Components.Commands + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Delete

+ + + + +
+ + +
+ + +
+
+ +
+ + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + Error = ""; + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private CmdBase.CommandParams _commandParams = null!; + + private string Timeout { get; set; } = "20"; + private string Error { get; set; } = ""; + + private bool IsReady { get; set; } + private bool IsReadyToRun => IsReady && CanExecute && !string.IsNullOrWhiteSpace(CommandJson); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + private string CommandName => _commandParams.Name; + private string CommandJson => _commandParams.CommandJson; + private bool CanExecute => _commandParams.CanExecute; + private bool NeedConfirmation => _commandParams.NeedConfirmation; + + private List Result + { + get => _commandParams.Result; + set => _commandParams.Result = value; + } + + protected override void OnInitialized() + { + _editContext = new(this); + _commandParams = new("Delete Documents", OnChanged); + } + + private void OnChanged(CmdBase.CommandParams arg) + { + InvokeAsync(StateHasChanged); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + + SyncUrl(); + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + SyncUrl(); + StateHasChanged(); + } + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(List res) + { + Result = res; + return InvokeAsync(StateHasChanged); + } + + private async Task> RunCommand(BsonDocument doc, CancellationToken token) + => [await UserSession.MongoDbAdmin.RunCommand(doc, token)]; + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/delete/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + + private async Task Execute() + { + Error = ""; + if (string.IsNullOrWhiteSpace(CommandJson)) + return; + + if (NeedConfirmation) + { + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Confirmation", $"Are you sure executing command \"{CommandName}\"? Some command's effects are irreversible!"); + if (!res.Confirmed) + return; + } + + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var doc = BsonDocument.Parse(CommandJson); + Shell.UpdateComment(doc, ticket, UserSession.User.GetEmail()); + + var res = await RunCommand(doc, cts.Token); + await Display(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + + } + + +} diff --git a/Rms.Risk.Mango/Pages/User/Find.razor b/Rms.Risk.Mango/Pages/User/Find.razor new file mode 100644 index 0000000..a4d05a4 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Find.razor @@ -0,0 +1,529 @@ +@page "/user/find" +@page "/user/find/{DatabaseStr}/{CollectionStr}" +@page "/user/find/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject IAuthorizationService Auth + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Find: @SelectedCollection

+ + + + +
+ + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ See documentation... +
+
+ +
+ +
+ + +
+
Filter:
+ +
+
+ +
+
Projection: (use {} to see everything, _id MUST be present in the result)
+ +
+
+
+
+ + @if (!IsReady) + { + + } + else if (ShowAsJson) + { + + } + else + { + + } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private string Text { get; set; } = +@"{ + ""_id"": { + ""$ne"" : """" + } +}"; + + private string Project { get; set; } = + @"{ + +}"; + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + InvokeAsync(StateHasChanged); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string Timeout { get; set; } = "20"; + private string FetchSize { get; set; } = "100"; + private bool IsReady { get; set; } + private bool ShowAsJson { get; set; } + + private string Result { get; set; } = "{}"; + private List ResultBson { get; set; } = []; + public ArrayBasedPivotData ResultPivot { get; set; } = new([]); + + + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + protected override void OnInitialized() + { + _editContext = new (this); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + SyncUrl(); + + var text = await JsRuntime.LoadFromLocalStorage("Find"); + if (!string.IsNullOrWhiteSpace(text)) + Text = text; + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + } + + enum ActionType + { + Find, Count, Explain + } + + private async Task Run(ActionType actionType) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + await JsRuntime.SaveToLocalStorage("Find", Text, cts.Token); + + if ( int.TryParse(FetchSize, out var limit) == false ) + limit = 0; + + var res = actionType switch + { + ActionType.Find => await RunFind(Text, Project, limit, cts.Token), + ActionType.Count => await RunCount (Text, cts.Token), + ActionType.Explain => await RunExplain(Text, cts.Token), + _ => throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null) + }; + + ResultPivot = res.Item1; + ResultBson = res.Item2; + + await Display(ResultBson); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + } + + private Task Run() => Run(ActionType.Find); + private Task Count() => Run(ActionType.Count); + private Task Explain() => Run(ActionType.Explain); + + private Task Display(Exception e) + { + ShowAsJson = true; + Result = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(IEnumerable res) + { + Result = res.ToJson(new() { Indent = true }); + return InvokeAsync(StateHasChanged); + } + + private async Task<(ArrayBasedPivotData, List)> RunExplain(string text, CancellationToken token) + { + var service = UserSession.MongoDb; + var command = $@"{{ + ""find"" : ""{SelectedCollection}"", + ""filter"" : {text} +}}"; + var res = await service.ExplainAsync(command, token); + ShowAsJson = true; + return (ArrayBasedPivotData.NoData, [res]); + } + + private async Task<(ArrayBasedPivotData, List)> RunFind(string text, string project , int limit, CancellationToken token) + { + var service = UserSession.MongoDb; + var results = service.FindAsync(text, false, project, limit: limit <= 0 ? null : limit, token: token); + + var fieldMap = new ConcurrentDictionary + { + ["find"] = new(false) + }; + + var (pd, list) = await MongoDbDataSource.FetchPivotData( + "find", + "find", + fieldMap, + results, + null, + true, + int.Parse(FetchSize), + true, + token); + + if (list.Count == 0) + list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {SelectedCollection}""}}")); + + return (pd, list); + } + + private async Task<(ArrayBasedPivotData, List)> RunCount(string text, CancellationToken token) + { + var service = UserSession.MongoDb; + var result = await service.CountAsync(text, token); + + var pd = new ArrayBasedPivotData(["Count"]); + pd.Add([result]); + + return (pd, [BsonDocument.Parse($"{{ Count: {result} }}"), ]); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/find/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private async Task Format() + { + Text = JsonUtils.FormatJson(Text); + await InvokeAsync(StateHasChanged); + } + + private async Task OnCellClick(DynamicObject row, string columnName) + { + try + { + var id = TableControl.GetDynamicMember(row, "_id"); + if (id == null) + return true; + + var bson = ResultBson.FirstOrDefault(x => x["_id"].ToString() == id.ToString()); + if (bson == null) + return true; + + var oldId = bson["_id"]; + + var enableWrite = await Auth.AuthorizeAsync( + UserSession.User.GetUser(), + UserSession.Database, + [new WriteAccessRequirement()]); + + var res = await ShowBsonDialog(Modal, id.ToString()!, bson, enableWrite.Succeeded); + if (res == null) + return true; + + if (enableWrite.Succeeded) + { + if (res.Value.ShouldUpdate) + { + bson = BsonDocument.Parse(res.Value.Json); + await OnUpdate(oldId, bson); + } + else + await OnDelete(oldId); + } + } + catch (Exception e) + { + await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e); + } + return true; + } + + private async Task OnDelete(BsonValue id) + { + var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Delete {id}", "Are you sure to delete document?"); + if (r.Cancelled) + return; + + var command = BsonDocument.Parse($@"{{ + ""delete"": ""{SelectedCollection}"", + ""deletes"": [{{ + ""q"": {{ }}, + ""limit"": 1 + }}] +}}"); + + var idDoc = new BsonDocument + { + ["_id"] = id + }; + + command["deletes"][0]["q"] = idDoc; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); + + var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); + var deleted = res["n"].ToInt32(); + if (deleted != 1) + { + Result = + res.ToJson(new() { Indent = true }) + + "\n------------------------------------------------------------------\n"+ + command.ToJson(new() { Indent = true }); + ShowAsJson = true; + await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Failure"); + await InvokeAsync(StateHasChanged); + } + else + await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Success"); + } + + private async Task OnUpdate(BsonValue id, BsonDocument newBson) + { + var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Update {id}", "Are you sure to update document?"); + if (r.Cancelled) + return; + + var command = BsonDocument.Parse($@"{{ + update: ""{SelectedCollection}"", + updates: [ + {{ + q: {{ }}, + u: {{ }}, + upsert: false, + multi: false, + }} + ], + ordered: false, + bypassDocumentValidation: false +}}"); + + var idDoc = new BsonDocument + { + ["_id"] = id + }; + + command["updates"][0]["q"] = idDoc; + command["updates"][0]["u"] = newBson; + + Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); + + var modified = res["nModified"].ToInt32(); + if (modified != 1) + { + Result = + res.ToJson(new() { Indent = true }) + + "\n------------------------------------------------------------------\n"+ + command.ToJson(new() { Indent = true }); + ShowAsJson = true; + await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Failure"); + await InvokeAsync(StateHasChanged); + } + else + await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Success"); + } + + + public static async Task<(bool ShouldUpdate, string Json)?> ShowBsonDialog(IModalService service, string header, BsonDocument bson, bool enableWrite) + { + var parameters = new ModalParameters + { + { "Text", bson.ToJson(new() { Indent = true }) }, + { "EnableWrite", enableWrite } + }; + + var options = new ModalOptions + { + HideCloseButton = true, + DisableBackgroundCancel = true + }; + + var form = service.Show(header, parameters, options); + var res = await form.Result; + if (res.Cancelled) + return null; + + var v = (MessageBoxJsonComponent.ResultType?)res.Data; + if (v == null) + return null; + + return (v.ShouldUpdate, v.Json); + } + + +} diff --git a/Rms.Risk.Mango/Pages/User/Insert.razor b/Rms.Risk.Mango/Pages/User/Insert.razor new file mode 100644 index 0000000..138c987 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Insert.razor @@ -0,0 +1,315 @@ +@page "/user/insert" +@page "/user/insert/{DatabaseStr}/{CollectionStr}" +@page "/user/insert/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] +@using MongoDB.Bson.Serialization +@using Rms.Risk.Mango.Components.Commands + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Insert: @SelectedCollection

+ + + + +
+ + +
+ +
You can insert either a single document (Json object) or multiple documents wrapped in Json array.
+
+
+ +
+ +
+ +
+ + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + Error = ""; + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private string ToInsert { get; set; } = "{}"; + + private string Timeout { get; set; } = "20"; + private string Error { get; set; } = ""; + + private bool IsReady { get; set; } + private bool IsReadyToRun => IsReady && IsValid; + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + private List Result { get; set; } = []; + + private bool IsValid + { + get + { + if ( BsonDocument.TryParse(ToInsert, out var _) ) + return true; + + try + { + var docs = BsonSerializer.Deserialize(ToInsert); + return docs.Count > 0; + } + catch + { + // ignore + } + return false; + } + } + + private List ParseToInsert() + { + if ( BsonDocument.TryParse(ToInsert, out var doc) ) + return [doc]; + + try + { + var docs = BsonSerializer.Deserialize(ToInsert).Select(x => x.AsBsonDocument).ToList(); + return docs; + } + catch + { + // ignore + } + return []; + } + + protected override void OnInitialized() + { + _editContext = new(this); + } + + private void OnChanged(CmdBase.CommandParams arg) + { + //InvokeAsync(StateHasChanged); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + + SyncUrl(); + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + SyncUrl(); + StateHasChanged(); + } + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(List res) + { + Result = res; + return InvokeAsync(StateHasChanged); + } + + private async Task> RunCommand(BsonDocument doc, CancellationToken token) + => [await UserSession.MongoDbAdmin.RunCommand(doc, token)]; + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/insert/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + + private async Task Execute() + { + Error = ""; + var docs = ParseToInsert(); + if (docs.Count == 0) + { + Error = "No documents to insert!"; + await InvokeAsync(StateHasChanged); + return; + } + + var mres = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Confirmation", $"Are you sure inserting {docs.Count} document(s)?"); + if (!mres.Confirmed) + return; + + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var doc = new BsonDocument + { + ["insert"] = SelectedCollection, + ["documents"] = new BsonArray(docs), + ["ordered"] = false + }; + + Shell.UpdateComment(doc, ticket, UserSession.User.GetEmail()); + + var res = await RunCommand(doc, cts.Token); + await Display(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + + } + + +} diff --git a/Rms.Risk.Mango/Pages/User/Logs.razor b/Rms.Risk.Mango/Pages/User/Logs.razor new file mode 100644 index 0000000..5172bfe --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Logs.razor @@ -0,0 +1,293 @@ +@page "/user/logs" +@page "/user/logs/{DatabaseStr}" +@page "/user/logs/{DatabaseStr}/{DatabaseInstanceStr}" +@using Rms.Risk.Mango.Pivot.Core.MongoDb +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Logs

+ + + +
+ + +
+ +
+
+ + @if (Error != null) + { + + } + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + + private Exception? Error { get; set; } + private List Result { get; set; } = new(); + private List AvailableSeverity = []; + private List AvailableCategories = []; + + private string RowsPerPageStr + { + get => _state.RowsPerPage.ToString(); + set + { + if (int.TryParse(value, out var newValue)) + _state.RowsPerPage = newValue; + } + } + + private string[] LogCategories { get; set; } = ["global"]; + private bool IsReady { get; set; } = false; + + private class LogsPageState + { + public string LogCategory { get; set; } = "global"; + public int RowsPerPage { get; set; } = 28; + } + + private LogsPageState _state = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + SyncUrl(); + + var state = await JsRuntime.LoadFromLocalStorage("Logs"); + if (state != null) + { + _state = JsonUtils.FromJson(state) ?? new(); + } + + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + LogCategories = await UserSession.MongoDbAdminForAdminDatabase.GetLogCategories(cts.Token); + } + catch (Exception) + { + // ignore + } + + StateHasChanged(); + _ = Task.Run(Run); + } + + private async Task Run() + { + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + IsReady = false; // Indicate that the process is starting + await InvokeAsync(() => JsRuntime.SaveToLocalStorage("Logs", JsonUtils.ToJson(_state), cts.Token)); + await InvokeAsync(StateHasChanged); + + var res = await UserSession.MongoDbAdminForAdminDatabase.GetLogs(token:cts.Token); + await Display(res); + } + catch (Exception e) + { + IsReady = true; // Indicate that the process has finished + await Display(e); + } + finally + { + IsReady = true; // Indicate that the process has finished + await InvokeAsync(StateHasChanged); + } + } + + + private Task Display(List model) + { + Error = null; + + model.Reverse(); + Result = model; + + AvailableCategories = model + .Select(x => x.Category) + .Distinct() + .OrderBy(x => x) + .ToList(); + + AvailableSeverity = model + .Select(x => x.Severity) + .Distinct() + .OrderBy(x => x) + .ToList(); + + return InvokeAsync(StateHasChanged); + } + + + private Task Display(Exception e) + { + Error = e; + Result.Clear(); + return InvokeAsync(StateHasChanged); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/logs/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private RenderFragment GetAttributes((dynamic Row, TableColumnControl Column) value) + { + LogRecordModel doc = value.Row; + + var json = doc.Attr?.ToJson() ?? ""; + if (json.Length > 100) + json = json[..100] + "..."; + + return @@json; + } + + private async Task OnCellClick(LogRecordModel doc) + { + var json = doc.ToJson(new () { Indent = true }); + + await ModalDialogUtils.ShowTextDialog( + Modal, + "Log event", + json, // Pass the JSON representation instead of the doc + "Press F11 to enter fullscreen mode. Use ESC to exit it." + ); + } + +} diff --git a/Rms.Risk.Mango/Pages/User/Pivot.razor b/Rms.Risk.Mango/Pages/User/Pivot.razor new file mode 100644 index 0000000..d26f9de --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Pivot.razor @@ -0,0 +1,286 @@ +@page "/user/pivot" +@page "/user/pivot/{DatabaseStr}/{DatabaseInstanceStr}" +@page "/user/pivot/{DatabaseStr}/{DatabaseInstanceStr}/{Collection}/{PivotName}" +@* @page "/user/pivot/{DatabaseStr}/{DatabaseInstanceStr}/{Collection}/{PivotName}/{Parameters}" *@ + +@attribute [Authorize] + +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime +@inject NavigationManager NavigationManager + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Pivot

+ + + + + + + + + + + @* Share="Share" *@ + + +@code { + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + [Parameter] + public string? Collection + { + get; + set + { + if (field == value) + return; + field = value; + if (field != null && _state.SelectedPivotForCollection.TryGetValue(field, out var pivot)) + { + PivotName = pivot; + InvokeAsync(StateHasChanged); + } + SyncUrl(); + } + } + + [Parameter] + public string? PivotName + { + get; + set + { + if (field == value) + return; + field = value; + if (!string.IsNullOrWhiteSpace(field) && !string.IsNullOrWhiteSpace(Collection)) + _state.SelectedPivotForCollection[Collection] = field; + SyncUrl(); + } + } + + private string? _currentUrl; + + private class PivotState + { + public int Rows { get; set; } = 40; + public Dictionary SelectedPivotForCollection { get; set; } = new(); + } + + private PivotState _state = new(); + private FilterExpressionTree.ExpressionGroup _extraFilter = new(); + private FilterExpressionTree.ExpressionGroup GetExtraFilter() => _extraFilter; + + private readonly List _collections = []; + + private const string DefaultCollectionName = "Forge: PnL"; + private const string DefaultPivotName = "Summary"; + + private ExtraFilterDefinition _extraFilterDef = new(); + + protected override async Task OnParametersSetAsync() + { + if (UserSession == null || _extraFilterDef.Filter.Count > 0 ) + return; + + if (DatabaseStr != UserSession.Database) + { + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = UserSession.Database; + else + UserSession.Database = DatabaseStr; + } + + if (DatabaseInstanceStr != UserSession.DatabaseInstance) + { + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = UserSession.DatabaseInstance; + else + UserSession.DatabaseInstance = DatabaseInstanceStr; + } + + if (_collections.Count == 0) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var collections = await UserSession.PivotDataSource.GetAllMeta(token: cts.Token); + _collections.Clear(); + _collections.AddRange(collections); + } + + + + if (string.IsNullOrWhiteSpace(Collection)) + { + var collections = _collections + .Where(x => !x.IsGroup) + .Select(x => x.CollectionNameWithPrefix) + .OrderBy(x => x) + .ToList() + ; + + Collection = collections.Contains(DefaultCollectionName) + ? DefaultCollectionName + : collections.FirstOrDefault(); + } + + if (string.IsNullOrWhiteSpace(PivotName) && !string.IsNullOrWhiteSpace(Collection)) + { + var pivots = (_collections.FirstOrDefault( x => x.CollectionNameWithPrefix == Collection)?.Pivots ?? []) + .Select(x => x.Pivot) + .Select(x => x.Name) + .OrderBy(x => x) + .ToList() + ; + + PivotName = pivots.Contains(DefaultPivotName) + ? DefaultPivotName + : pivots.FirstOrDefault(); + } + + _extraFilterDef = new() + { + Filter = + [ + new() + { + ControlType = ExtraFilterDefinition.ControlTypeDropDown, + AllowMultiselect = true, + DisplayName = "Department", + FieldName = "Department", +// Values = [.. DepartmentsList ], + SelectorCollection = ExtraFilterDefinition.CurrentCollectionSignature, + SelectorQuery = @" +[{ + ""$group"" : { + ""_id"" : null, + ""Department"" : { + ""$addToSet"" : ""$Department"" + } + } + }, + { ""$unwind"" : ""$Department""} +] +" + }, + + new() + { + ControlType = ExtraFilterDefinition.ControlTypeDatePicker, + AllowMultiselect = true, + DisplayName = "COB", + FieldName = "COB", +// Values = [.. await UserSession.PivotDataSource.GetCobDatesAsync(Collection!) ], + DefaultValue = CobHelper.GetLatestCob().ToString(CobHelper.CobFormat), + SelectorCollection = ExtraFilterDefinition.CurrentCollectionSignature, + SelectorQuery = @" +[ + { + ""$group"" : { + ""_id"" : null, + ""COB"" : { + ""$addToSet"" : ""$COB"" + } + } + }, + { ""$unwind"" : ""$COB"" } +] +" + } + ] + }; + + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var data = await JsRuntime.LoadFromLocalStorage("pivot", cts.Token); + if ( data != null ) + _state = data; + + await InvokeAsync(StateHasChanged); + + SyncUrl(); + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + "user/pivot"; + + url += $"/{Uri.EscapeDataString(UserSession.Database)}"; + if (!string.IsNullOrWhiteSpace(UserSession.DatabaseInstance)) + url += $"/{UserSession.DatabaseInstance}"; + else + url += "/default"; + + if (!string.IsNullOrWhiteSpace(Collection) && !string.IsNullOrWhiteSpace(PivotName)) + url += $"/{Uri.EscapeDataString(Collection)}/{Uri.EscapeDataString(PivotName)}"; + + var values = _extraFilterDef.ParseExtraFilter(_extraFilter); + var qp = _extraFilterDef.CreateQueryParameters(values); + + var paramStr = string.Join("&", qp.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); + if ( !string.IsNullOrWhiteSpace(paramStr)) + url += $"?{paramStr}"; + + if (_currentUrl == url) + return; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + _currentUrl = url; + } + + private void SetExtraFilter(FilterExpressionTree.ExpressionGroup expressionGroup) + { + _extraFilter = expressionGroup; + SyncUrl(); + } + + private async Task OnCurrentPivotChanged(PivotDefinition arg) + { + if (PivotName != null && Collection != null) + _state.SelectedPivotForCollection[Collection] = PivotName; + + await InvokeAsync(async () => await JsRuntime.SaveToLocalStorage("pivot", _state)); + } + +} diff --git a/Rms.Risk.Mango/Pages/User/SavedQueries.razor b/Rms.Risk.Mango/Pages/User/SavedQueries.razor new file mode 100644 index 0000000..ffb3829 --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/SavedQueries.razor @@ -0,0 +1,351 @@ +@page "/user/saved-queries" +@page "/user/saved-queries/{DatabaseStr}" +@page "/user/saved-queries/{DatabaseStr}/{DatabaseInstanceStr}" +@attribute [Authorize] + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +

Saved queries

+ + + + + + + + + + +
+ + +
+ +
+ +
+ +
+
+ + + +
+
+
+
+ +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + + private readonly List> _rootNodes = []; + private Navigation _navigation = null!; + private Dictionary _groupedCollections = new(); + + private bool IsRefreshEnabled => SelectedPivotNode != null && SelectedPivotNode.Data != null; + + private bool IsExportEnabled { get; set; } = true; + private DateTime LastRefresh { get; set; } + private TimeSpan LastRefreshElapsed { get; set; } + + private PivotTableComponent PivotTable { get; set; } = null!; + private bool UseCache { get; set; } = true; + private IPivotedData? PivotData { get; set; } + private PivotDefinition? CurrentPivot { get; set; } + private int Rows { get; set; } = 35; + + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + public TreeNode? SelectedPivotNode + { + get; + set + { + if (field == value) + return; + field = value; + //if (field?.Data != null) + //{ + // // If a pivot is selected, navigate to the pivot page + // var url = NavigationManager.BaseUri + $"user/pivot/{Database}/{field.Data.Collection}/{field.Data.Name}/{UserSession.Department}"; + // JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + //} + //else + //{ + // // If no pivot is selected, just update the state + // StateHasChanged(); + //} + } + } + + // Extracts the name of the direct descendant of the root parent (Base) + private TreeNode? BaseNode + { + get + { + var node = SelectedPivotNode; + if (node == null) return null; + // Traverse up to the root + while (node.Parent != null && node.Parent.Parent != null) + { + node = node.Parent; + } + // node is now the direct child of root (i.e., Base) + return node.Parent != null ? node : null; + } + } + + private string? Base => BaseNode?.Label.Split(":").FirstOrDefault(); + private string? Collection => BaseNode?.Label.Split(":").LastOrDefault(); + private List _collections = []; + + // Concatenation of names of nodes between Collection and SelectedPivotNode, excluding both + private string Path + { + get + { + + // Now, walk from SelectedPivotNode up to Collection, excluding both + var names = new List(); + var temp = SelectedPivotNode; + var baseNode = BaseNode; + while (temp != null && temp != baseNode) + { + if (temp != SelectedPivotNode) // Exclude SelectedPivotNode itself + names.Insert(0, temp.Label); + temp = temp.Parent; + } + + return string.Join("/", names); + } + } + + private GroupedCollection? SelectedCollectionNode + { + get + { + if (SelectedPivotNode == null || BaseNode == null) + return null; + // Find the grouped collection that matches the current collection name + _groupedCollections.TryGetValue(BaseNode.Label, out var groupedCollection); + return groupedCollection; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + _navigation = new(PivotTable.Navigate); + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = UserSession.Database; + else + UserSession.Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = UserSession.DatabaseInstance; + else + UserSession.DatabaseInstance = DatabaseInstanceStr; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var ds = UserSession.PivotDataSource; + _collections = await ds.GetAllMeta(token: cts.Token); + + if (_collections.Count == 0) + { + await ModalDialogUtils.ShowInfoDialog(Modal, "Problem", "No collections found that have corresponding -Meta collections."); + return; + } + + _groupedCollections = _collections + .Where(x => !x.IsGroup) + .ToDictionary(x => x.CollectionNameWithPrefix, x => x); + + var root = new TreeNode() + { + Label = "Saved queries", + Data = null, + Parent = null, + IsExpanded = true + }; + + PopulateRootNodes(ds, root, _collections, cts.Token); + _rootNodes.Add(root); + + SyncUrl(); + StateHasChanged(); + } + + private static void PopulateRootNodes(IPivotTableDataSource ds, TreeNode root, List collections, CancellationToken token) + { + // Clear any existing children + root.Children.Clear(); + + foreach (var collection in collections.Where( x => !x.IsGroup)) + { + // Each collection becomes a child node under the root + var node = new TreeNode + { + Label = collection.CollectionNameWithoutPrefix, + Data = null, + Parent = root, + IsExpanded = false + }; + root.Children.Add(node); + + // Group pivots by GroupName (null or empty group names go under "Ungrouped") + var groupedPivots = collection.Pivots + .Where(x => !x.IsGroup) + .GroupBy(p => string.IsNullOrWhiteSpace(p.Pivot.Group) ? "Ungrouped" : p.Pivot.Group) + .OrderBy(g => g.Key); + + foreach (var group in groupedPivots) + { + // Create a group node under the collection node + var groupNode = new TreeNode + { + Label = group.Key, + Data = null, + Parent = root.Children.Last(), // The collection node just added + IsExpanded = false + }; + root.Children.Last().Children.Add(groupNode); + + // Add all pivots in this group as children of the group node + foreach (var pivot in group.OrderBy(x => x.Pivot.Name)) + { + var childNode = new TreeNode + { + Label = pivot.Pivot.Name, + Data = pivot, + Parent = groupNode, + IsExpanded = false + }; + groupNode.Children.Add(childNode); + } + } + } + } + + private async Task HandleNodeChanged(TreeNode node) + { + if (node.Data == null) + { + // If the node has no data, it means it's a collection or group node, so we don't do anything + return; + } + node.Data.Pivot.Name = node.Label; // Update the pivot name to match the node label + await InvokeAsync(StateHasChanged); // Refresh the UI + } + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/saved-queries/{Database}"; + if (!string.IsNullOrWhiteSpace(UserSession.DatabaseInstance)) + url += $"/{UserSession.DatabaseInstance}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private readonly FilterExpressionTree.ExpressionGroup _noFilter = new (); + + private FilterExpressionTree.ExpressionGroup GetExtraFilter() => _noFilter; + + + private Task OnRefreshPivot() => PivotTable.RunPivot(); + private Task OnCopyCsv() => PivotTable.CopyCsv(); + + private Task OnExportCsv() => Task.CompletedTask; //PivotTable.ExportCsv(Uri.EscapeDataString($"{CobStr}-{Department}-{Pivot?.Pivot.Name}.csv")); + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Pages/User/Update.razor b/Rms.Risk.Mango/Pages/User/Update.razor new file mode 100644 index 0000000..ba8639e --- /dev/null +++ b/Rms.Risk.Mango/Pages/User/Update.razor @@ -0,0 +1,282 @@ +@page "/user/update" +@page "/user/update/{DatabaseStr}/{CollectionStr}" +@page "/user/update/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" +@attribute [Authorize] +@using Rms.Risk.Mango.Components.Commands + +@inject NavigationManager NavigationManager +@inject IUserSession UserSession +@inject IJSRuntime JsRuntime + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +

Update: @SelectedCollection

+ + + + +
+ + +
+ + +
+
+ +
+ + + + @if (!string.IsNullOrWhiteSpace(Error)) + { +
@Error
+ } +
+ +@code { + [CascadingParameter] public IModalService Modal { get; set; } = null!; + + [Parameter] public string? DatabaseStr { get; set; } + [Parameter] public string? DatabaseInstanceStr { get; set; } + [Parameter] public string? CollectionStr { get; set; } + + private IReadOnlyCollection Collections { get; set; } = []; + + private string SelectedCollection + { + get => UserSession.Collection; + set + { + if (UserSession.Collection == value) + return; + UserSession.Collection = value; + SyncUrl(); + Error = ""; + InvokeAsync(StateHasChanged); + } + } + + private string Database + { + get => UserSession.Database; + set + { + if (UserSession.Database == value) + return; + UserSession.Database = value; + SyncUrl(); + } + } + + private string DatabaseInstance + { + get => UserSession.DatabaseInstance; + set + { + if (UserSession.DatabaseInstance == value) + return; + UserSession.DatabaseInstance = value; + SyncUrl(); + } + } + + private CmdBase.CommandParams _commandParams = null!; + + private string Timeout { get; set; } = "20"; + private string Error { get; set; } = ""; + + private bool IsReady { get; set; } + private bool IsReadyToRun => IsReady && CanExecute && !string.IsNullOrWhiteSpace(CommandJson); + + private EditContext? _editContext; + private EditContext EditContext => _editContext!; + + private string CommandName => _commandParams.Name; + private string CommandJson => _commandParams.CommandJson; + private bool CanExecute => _commandParams.CanExecute; + private bool NeedConfirmation => _commandParams.NeedConfirmation; + + private List Result + { + get => _commandParams.Result; + set => _commandParams.Result = value; + } + + protected override void OnInitialized() + { + _editContext = new(this); + _commandParams = new("Update Documents", OnChanged); + } + + private void OnChanged(CmdBase.CommandParams arg) + { + //InvokeAsync(StateHasChanged); + } + + protected override void OnAfterRender(bool firstRender) + { + if (!firstRender) + return; + + if (string.IsNullOrWhiteSpace(DatabaseStr)) + DatabaseStr = Database; + else + Database = DatabaseStr; + + if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) + DatabaseInstanceStr = DatabaseInstance; + else + DatabaseInstance = DatabaseInstanceStr; + + if (string.IsNullOrWhiteSpace(CollectionStr)) + CollectionStr = SelectedCollection; + else + SelectedCollection = CollectionStr; + + SyncUrl(); + + _ = Task.Run(async () => + { + try + { + var admin = UserSession.MongoDbAdmin; + + Collections = await admin.ListCollections(); + if (!Collections.Contains(SelectedCollection)) + SelectedCollection = Collections.FirstOrDefault() ?? ""; + + IsReady = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + await Display(e); + } + }); + SyncUrl(); + StateHasChanged(); + } + + private Task Display(Exception e) + { + Error = e.ToString(); + return InvokeAsync(StateHasChanged); + } + + private Task Display(List res) + { + Result = res; + return InvokeAsync(StateHasChanged); + } + + private async Task> RunCommand(BsonDocument doc, CancellationToken token) + => [await UserSession.MongoDbAdmin.RunCommand(doc, token)]; + + private void SyncUrl() + { + var url = NavigationManager.BaseUri + $"user/update/{Database}"; + if (!string.IsNullOrWhiteSpace(DatabaseInstance)) + url += $"/{DatabaseInstance}"; + url += $"/{SelectedCollection}"; + + JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); + } + + private Task CanExecuteCommand() + => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); + + + private async Task Execute() + { + Error = ""; + if (string.IsNullOrWhiteSpace(CommandJson)) + return; + + if (NeedConfirmation) + { + var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Confirmation", $"Are you sure executing command \"{CommandName}\"? Some command's effects are irreversible!"); + if (!res.Confirmed) + return; + } + + var ticket = await CanExecuteCommand(); + if (string.IsNullOrWhiteSpace(ticket)) + return; + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); + + try + { + IsReady = false; + await InvokeAsync(StateHasChanged); + + var doc = BsonDocument.Parse(CommandJson); + Shell.UpdateComment(doc, ticket, UserSession.User.GetEmail()); + + var res = await RunCommand(doc, cts.Token); + await Display(res); + } + catch (Exception e) + { + await Display(e); + } + finally + { + IsReady = true; + await InvokeAsync(StateHasChanged); + } + + } + + +} diff --git a/Rms.Risk.Mango/Pages/_Host.cshtml b/Rms.Risk.Mango/Pages/_Host.cshtml new file mode 100644 index 0000000..bd8265c --- /dev/null +++ b/Rms.Risk.Mango/Pages/_Host.cshtml @@ -0,0 +1,102 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace Rms.Risk.Mango.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + @* DB styles - Web.ADK *@ + + + @* External components *@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @* CodeMirror https://codemirror.net/*@ + + + + + + + + + + + + + + + + + + + + + + + + + + @* *@ + @* *@ + + + + + + + + + + + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/Rms.Risk.Mango/PluginSupport.cs b/Rms.Risk.Mango/PluginSupport.cs new file mode 100644 index 0000000..4a3519b --- /dev/null +++ b/Rms.Risk.Mango/PluginSupport.cs @@ -0,0 +1,122 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Interfaces; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Rms.Risk.Mango; + +public static partial class PluginSupport +{ + [GeneratedRegex(@"^.*Mango.*Plugin\.dll$")] + private static partial Regex PluginNameMask(); + + private static readonly List _loaded = []; + private static IDbMangoPlugin? _plugin; + + public static List GetPluginAssemblies(ILogger? logger) + { + if ( _loaded.Count > 0 ) + return _loaded; + + var basePath = AppContext.BaseDirectory; + + if (!Directory.Exists(basePath)) + return _loaded; + + var pluginFiles = Directory.GetFiles(basePath, "*.dll", SearchOption.TopDirectoryOnly) + .Where(file => PluginNameMask().IsMatch(file)); + + _loaded.AddRange(pluginFiles.Select(LoadPlugin).OfType()); + + if ( _loaded.Count > 0 ) + logger?.LogInformation("Plugin assemblies loaded:\n{Join}", string.Join("\n\t", _loaded.Select(x => x.FullName))); + else + logger?.LogInformation("No plugin assemblies loaded"); + + return _loaded; + } + + public static IDbMangoPlugin? GetPlugin(ILogger? logger = null) + { + if ( _plugin != null ) + return _plugin; + string? pluginClassName = GetPluginClassName(); + var assemblies = GetPluginAssemblies(logger); + var type = assemblies + .SelectMany(x => x.GetTypes().Where(t => ( string.IsNullOrWhiteSpace(pluginClassName) || t.Name == pluginClassName) && t.GetInterface(nameof(IDbMangoPlugin)) != null )) + .FirstOrDefault() + ; + + if ( type == null ) + return null; + + _plugin = (IDbMangoPlugin?)Activator.CreateInstance(type); + + if ( _loaded.Count > 0 ) + logger?.LogInformation("Plugin selected:\n{type}", type); + else + logger?.LogInformation("No plugin selected"); + + return _plugin; + } + + private static string? GetPluginClassName() + { + var args = Environment.GetCommandLineArgs(); + // Check for "--plugin" argument and extract the plugin class name + string? pluginClassName = null; + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "--plugin-class-name" && i + 1 < args.Length) + { + pluginClassName = args[i + 1]; + break; + } + } + if (!string.IsNullOrWhiteSpace(pluginClassName)) + { + return pluginClassName.Trim(); + } + + pluginClassName = Environment.GetEnvironmentVariable("DBMANGO_PLUGIN_CLASS_NAME"); + if (!string.IsNullOrWhiteSpace(pluginClassName)) + { + return pluginClassName.Trim(); + } + + return null; + } + + private static Assembly? LoadPlugin(string pluginPath) + { + if (!File.Exists(pluginPath)) + return null; + + try + { + var assembly = Assembly.LoadFrom(pluginPath); + return assembly; + } + catch (Exception) + { + return null; + } + } +} diff --git a/Rms.Risk.Mango/Program.cs b/Rms.Risk.Mango/Program.cs new file mode 100644 index 0000000..3d6b5d5 --- /dev/null +++ b/Rms.Risk.Mango/Program.cs @@ -0,0 +1,172 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Blazored.Modal; +using Microsoft.Extensions.Configuration.UserSecrets; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Rms.Risk.Mango.Pivot.UI.Services; +using Rms.Risk.Mango.Services; +using Rms.Risk.Mango.Services.Context; +using Rms.Risk.Mango.Services.Logging; +using Rms.Risk.Mango.Services.Security; +using Rms.Service.Bootstrap; +using Rms.Service.Bootstrap.Health; +using Rms.Service.Bootstrap.Security; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; + +[assembly: UserSecretsId("BF0D1460-2D1E-40CC-8F10-127369F684FE")] +[assembly: InternalsVisibleTo("Tests.Rms.Risk.Mango")] + +namespace Rms.Risk.Mango; + +public class Program +{ + private static ILogger? _logger; + + public static void Main(string[] args) + { + TaskScheduler.UnobservedTaskException += ( _, a ) => + { + //HACK: Because of a BUG we ignore all unhandled task exceptions + a.SetObserved(); + var m = $"Unobserved task exception ignored: {a.Exception.InnerException?.Message ?? a.Exception.Message}"; + _logger?.LogError(a.Exception, m); + }; + + AppDomain.CurrentDomain.UnhandledException += ( _, a ) => + { + var m = $"Unexpected execution error IsTerminating={a.IsTerminating} ExceptionObject=\"{a.ExceptionObject}\""; + _logger?.LogError(a.ExceptionObject as Exception, m); + }; + + // Load assembly dynamically if it exists + var plugin = PluginSupport.GetPlugin(_logger); + + var builder = WebApplication.CreateBuilder(args); + + var options = new ServiceBootstrapOptions + { + EnableGrpc = false, + EnableOidc = false, + EnableMTLS = false, + AuthorizeBy = AuthorizationType.Skip, + EnableOpenIdConnect = true, + }; + + builder.ConfigureStandardEndpoint(options); + builder.Services + .AddLog4NetRedirection(); + + MongoDbHelper.RemoteCertificateCheck = (_, s,c,e) => CertificateHelper.CheckClientCertificateChain((X509Certificate2?)s,c,e); + + IdentityModelEventSource.ShowPII = true; // show strings in error messages + + builder.Services + .ConfigureProtected(builder.Configuration.GetSection("DatabasesConfig")) + .ConfigureProtected(builder.Configuration.GetSection("DbMangoSettings")) + ; + + // Add services to the container. + + builder.Services + .AddServerSideBlazor() + .AddHubOptions(x=> x.MaximumReceiveMessageSize = 100_000_000) + ; + + builder.Services + .TryAddSingleton(); + builder.Services.AddBlazoredModal(); + + if ( plugin != null ) + { + builder.Services.AddSingleton(plugin); + } + else + { + builder.Services + .AddSingleton() + ; + } + + builder.Services + .AddMongoDbAccess() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddScoped() + .AddScoped() + .AddSingleton() + ; + + plugin?.ConfigureServices(builder); + + builder.WebHost + .UseStaticWebAssets() + .UseKestrel((_, kestrelServerOptions) => { kestrelServerOptions.ConfigureStandardKestrel(builder, options); }); + + AfhHelpers.Init(); + + var app = builder.Build(); + + _logger = app.Services.GetService>(); + var settings = app.Services.GetService>(); + if (settings?.Value.DefaultTimeout != null ) + DatabaseConfigurationService.DefaultTimeout = settings.Value.DefaultTimeout.Value; + + // Configure the HTTP request pipeline. + + if (!app.Environment.IsDevelopment()) + { + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + else + { + app.UseDeveloperExceptionPage(); + } + + if ( builder.IsHttps() ) + app.UseHttpsRedirection(); + + app.UseStaticFiles(); + + app.UseStandardEndpoint(options); + + app.MapBlazorHub(); + app.MapFallbackToPage("/_Host"); + + // ----------------------------------------------- run the server --------------------------------------------- + + StartupHealthCheck.StartupCompleted = true; + + app.Run(); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/README.md b/Rms.Risk.Mango/README.md new file mode 100644 index 0000000..5a50556 --- /dev/null +++ b/Rms.Risk.Mango/README.md @@ -0,0 +1,8 @@ +# dbMango +![Template](wwwroot/images/mango.svg){height=170} + +dbMango is a secure, audited MongoDB management tool designed for database administrators, developers, and support teams. It provides a user-friendly interface for managing multiple MongoDB databases with features like data browsing, querying, aggregation, structure synchronization, and data migration. Access is strictly controlled through LDAP-based roles (Admin, ReadOnly, ReadWrite), ensuring compliance and security. dbMango simplifies complex MongoDB operations with tools like Aggregation for Humans (AFH), which offers a human-readable syntax for creating aggregation pipelines. All privileged actions are logged for auditing, and the platform supports tasks like backups, restores, and shell access, making it an essential tool for efficient and compliant database management. + +# Documentation + +See [documentation](wwwroot/docs/index.md) here. diff --git a/Rms.Risk.Mango/Rms.Risk.Mango.Settings.json b/Rms.Risk.Mango/Rms.Risk.Mango.Settings.json new file mode 100644 index 0000000..15dc369 --- /dev/null +++ b/Rms.Risk.Mango/Rms.Risk.Mango.Settings.json @@ -0,0 +1,126 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.Extensions.Http.DefaultHttpClientFactory": "Information", + "Microsoft.AspNetCore.Authentication": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Extensions.Diagnostics.HealthChecks": "Information", + "Rms": "Trace" + } + }, + "DetailedErrors": true, + "AllowedHosts": "*", + "HealthCheckPublisher": { + "Delay": "00:00:10", + "Period": "00:15:00" + }, + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Https" + } + }, + "SecuritySettings": { + "MasterKeyFileName": ".p12", + // dotnet user-secrets set "SecuritySettings:MasterKeyPassword" "actual value" + "MasterKeyPassword": "", + + // for Development environment generate (and trust to) dev https keys using these commands: + // dotnet dev-certs https + // dotnet dev-certs https --trust + // Leave the following 2 parameters blank + "CertificateFileName": "", + "CertificatePassword": "", + + "LogoffIdlePeriod": "05:00:00", + + "Oidc": { + "ClientId": "", + "Secret": "", + "ValidClientIds": [], + // dotnet user-secrets set "SecuritySettings:Oidc:Secret" "actual value" + "ConfigUrl": "https:///oauth2/global", + "ConfigCacheFile": "%TEMP%/.oidc.json" + }, + "Ldap": { + "Url": "ldaps://", + "UserName": "", + "Password": "", + "EntryPoint": "", + + "RoleGroupMapping": { + } + }, + "CASubjectToAccept": [ + "CN=" + ], + "ClientCertWhiteList": [] + }, + "DbMangoSettings": { + "InstanceName": "dbMango", + "EnableDatabaseOverrides": false, + "Initial": "", + "AuditExpireDays": 7, + "DefaultTimeout": "00:00:15", + //"MongoDbDocUrl": "", + "MongoDbDocUrl": "https://raw.githubusercontent.com/mongodb/docs/5084326383812cc65297bc8151760b9250a723b6/content/manual/v7.0/source/reference/command/", + "MongoDbDocProxyUrl": "", + "RequestAccessURL": "", + "RequestAccessLabel": "", + "SupportLinkLabel": "", + "SupportLinkUrl": "", + "Settings": { + "MongoDbSocketTimeout": "00:02:00", + "MongoDbServerSelectionTimeout": "00:00:10", + "MongoDbConnectTimeout": "00:00:10", + "MongoDbMinConnectionPoolSize": "0", + "MongoDbMaxConnectionPoolSize": "100", + "MaxConnectionIdleTime": "00:10:00", + "MaxConnectionLifeTime": "00:30:00", + "MongoDbQueryTimeout": "60", + "MongoDbPingTimeoutSec": "5", + "MongoDbQueryBatchSize": "5000", + "MongoDbConnectionRetries": "1", + "MongoDbRetryTimeoutSec": "5" + }, + // only used within DB + "AuditLogsInOracle": false, + "OracleConnectionSettings": { + "ConnectionString": "", + "Password": "", + "AuditConnectionString": "", + "AuditPassword": "" + } + }, + // only used within DB + "ChangeNumberCheckerSettings": { + "CheckChangeNumber": false, + "ServerGridUrl": "", + "ServerGridUser": "", + "ServerGridPassword": "" + }, + "DatabasesConfig": { + "Databases": { + "": { + "Groups": { + "Admin": "", + "ReadOnly": "", + "ReadWrite": "" + }, + "Config": { + "MongoDbUrl": "mongodb://:27017", + "MongoDbDatabase": "", + "DirectConnection": false, + "UseTls": true, + "AllowShardAccess": false, + "Auth": { + "User": "", + "Password": "", + "AuthDatabase": "admin", + "Method": "SCRAM-SHA-256" + } + } + } + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Rms.Risk.Mango.csproj b/Rms.Risk.Mango/Rms.Risk.Mango.csproj new file mode 100644 index 0000000..accab09 --- /dev/null +++ b/Rms.Risk.Mango/Rms.Risk.Mango.csproj @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + data\%(FileName)%(Extension) + + + + + + diff --git a/Rms.Risk.Mango/Services/AfhHelpers.cs b/Rms.Risk.Mango/Services/AfhHelpers.cs new file mode 100644 index 0000000..46549e5 --- /dev/null +++ b/Rms.Risk.Mango/Services/AfhHelpers.cs @@ -0,0 +1,183 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text.Json.Serialization.Metadata; +using Rms.Risk.Mango.Language; +using Rms.Risk.Mango.Language.Ast; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.Models; + +namespace Rms.Risk.Mango.Services; + +public static class AfhHelpers +{ + private static readonly System.Text.Json.JsonSerializerOptions _prettyPrint = new() + { + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + + + private static Func /*fieldTypes*/, Func /*getAggregationOperator*/, string>? _prevConvertToJson; + + public static void Init() + { + _prevConvertToJson = PivotDefinition.ConvertToJson; + PivotDefinition.ConvertToJson = ConvertToJson; + } + + public static string ConvertToJson( + PivotDefinition pivot, + FilterExpressionTree.ExpressionGroup? extraFilter, + Dictionary fieldTypes, + Func getAggregationOperator) + { + if (_prevConvertToJson == null) + throw new ApplicationException("Call AfhHelpers.Init() first"); + + if ( pivot.PivotType != PivotTypeEnum.AggregationForHumans ) + return _prevConvertToJson(pivot, extraFilter, fieldTypes, getAggregationOperator ); + + var filterText = ""; + if ( extraFilter?.IsEmpty == false ) + { + var afhFilter = ConvertToExpression(extraFilter!, fieldTypes); + filterText = $" AND ( {afhFilter.AsText()} )"; + } + + var keys = ConvertKeys(pivot.KeyFields); + var preserveKeys = ConvertKeysPreserve(pivot.KeyFields); + var idToRootKeys = ConvertKeysFromIdToRoot(pivot.KeyFields); + + var query = pivot.CustomQuery + .Replace("/*[EXTRA_FILTER]*/", filterText) + .Replace("/*[KEYS]*/", keys) + .Replace("/*[KEYS_PRESERVE]*/", preserveKeys) + .Replace("/*[KEYS_FROM_ID_TO_ROOT]*/", idToRootKeys) + ; + + var ast = LanguageParser.ParseScriptToAST(query); + var json = ast.AsJson() ?? throw new ApplicationException("Failed to convert AST to JSON"); + + return json.ToJsonString(_prettyPrint); + } + + private static string? ConvertKeys(string[] keys) => string.Join(", ", keys.Select(x => $"'${x}'")); + private static string? ConvertKeysPreserve(string[] keys) => string.Join(", ", keys.Select(x => $"'${x}' AS '${x}'")); + private static string? ConvertKeysFromIdToRoot(string[] keys) => string.Join(", ", keys.Select(x => $"'$_id.{x}' AS '${x}'")); + + private static AstExpressionOperation ConvertToExpression(FilterExpressionTree.ExpressionGroup filter, Dictionary fieldTypes) + { + var afhFilter = new AstExpressionOperation( + filter.Condition == FilterExpressionTree.ExpressionGroup.ConditionType.And + ? AstExpressionOperation.OperationType.AND + : AstExpressionOperation.OperationType.OR + , filter.Children.Select( x => + x switch + { + FilterExpressionTree.FieldExpression fe => ConvertToExpression(fe, fieldTypes), + FilterExpressionTree.ExpressionGroup eg => ConvertToExpression(eg, fieldTypes), + _ => throw new NotSupportedException() + }) + ); + + return afhFilter; + } + + private static AstExpression ConvertToExpression(FilterExpressionTree.FieldExpression filter, Dictionary fieldTypes) + { + switch (filter.Condition) + { + case FilterExpressionTree.FieldConditionType.EqualTo: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.EQ, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.NotEqualTo: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.NEQ, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.GreaterThan: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.GT, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.LessThan: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.LT, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.GreaterThanOrEqualTo: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.GTE, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.LessThanOrEqualTo: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.LTE, + new AstExpressionVariable(filter.Field), + ConvertConstant(filter.Argument, fieldTypes)); + + case FilterExpressionTree.FieldConditionType.IsEmpty: + return new AstExpressionExists(filter.Field, false); + + case FilterExpressionTree.FieldConditionType.NotIsEmpty: + return new AstExpressionExists(filter.Field, true); + + case FilterExpressionTree.FieldConditionType.IsNull: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.EQ, + new AstExpressionVariable(filter.Field), + new AstExpressionNull()); + + case FilterExpressionTree.FieldConditionType.NotIsNull: + return new AstExpressionOperation( + AstExpressionOperation.OperationType.NEQ, + new AstExpressionVariable(filter.Field), + new AstExpressionNull()); + + case FilterExpressionTree.FieldConditionType.Contains: + case FilterExpressionTree.FieldConditionType.StartsWith: + case FilterExpressionTree.FieldConditionType.EndsWith: + case FilterExpressionTree.FieldConditionType.Matches: + case FilterExpressionTree.FieldConditionType.DoesNotMatch: + case FilterExpressionTree.FieldConditionType.DoesNotContain: + case FilterExpressionTree.FieldConditionType.DoesNotStartWith: + case FilterExpressionTree.FieldConditionType.DoesNotEndWith: + default: + throw new NotImplementedException(); + } + } + + private static AstExpression ConvertConstant(string? filterArgument, Dictionary fieldTypes) + { + if (filterArgument == null) + return new AstExpressionNull(); + if ( long.TryParse(filterArgument, out var l)) + return new AstExpressionNumber(l); + if ( double.TryParse(filterArgument, out var d)) + return new AstExpressionNumber(d); + return new AstExpressionString(filterArgument); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Audit/AuditService.cs b/Rms.Risk.Mango/Services/Audit/AuditService.cs new file mode 100644 index 0000000..69af839 --- /dev/null +++ b/Rms.Risk.Mango/Services/Audit/AuditService.cs @@ -0,0 +1,133 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using MongoDB.Driver; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Rms.Risk.Mango.Pivot.UI.Services; + +namespace Rms.Risk.Mango.Services.Audit; + +// ReSharper disable InconsistentNaming +public class AuditService(MongoDbConfigRecord _config, MongoDbSettings _settings, int _auditExpireDays, string? _databaseInstance = null) : IAuditService +// ReSharper restore InconsistentNaming +{ + public const string AuditCollection = DatabaseStructureLoader.AuditCollection; + + private readonly IMongoDatabase _database = MongoDbHelper.GetDatabase(_config, _settings, _databaseInstance); + + public void PreCheck(BsonDocument command) + { + if (command.ToJson().Contains(AuditCollection)) + throw new ApplicationException("Forbidden command"); + } + + public async Task Record(AuditRecord rec, CancellationToken token = default) + { + var commandType = rec.Command.ElementAt(0).Name ?? ""; + if (MongoDbCommandHelper.IsReadOnlyCommand(commandType)) + return; + + string? shortError = null; + if (rec.Error != null) + { + var pos = rec.Error.IndexOf('\n'); + if (pos >= 0) + { + shortError = rec.Error![..pos].Replace("\r", ""); + shortError = shortError[..Math.Min(shortError.Length, 128)]; + } + } + + var doc = new BsonDocument(new Dictionary() + { + ["_id"] = Guid.NewGuid().ToString("N"), + ["expire-at"] = DateTime.UtcNow + TimeSpan.FromDays(_auditExpireDays), + ["ts"] = rec.Timestamp, + ["collection"] = rec.Command.ElementAt(0).Value, + ["database"] = rec.DatabaseName, + ["email"] = rec.Email, + ["ticket"] = rec.Ticket, + ["ok"] = rec.Success, + ["error"] = shortError, + ["commandType"] = commandType, + ["command"] = rec.Command + }); + + await _database.GetCollection(AuditCollection).InsertOneAsync(doc, new (), token); + } + + public async Task> Audit(DateTime startDate, DateTime endDate, CancellationToken token = default) + { + var filter = $@"{{ + ""$and"" : [ + {{ ts : {{ ""$gte"" : ISODate(""{startDate:yyyy-MM-dd}T00:00:00"") }} }}, + {{ ts : {{ ""$lte"" : ISODate(""{endDate:yyyy-MM-dd}T23:59:59"") }} }} + ] +}}"; + + var findOptions = new FindOptions() + { + BatchSize = 1000 + }; + + var coll = _database.GetCollection(AuditCollection); + + var cursor = await coll.FindAsync(filter, findOptions, token); + + var list = new List(); + + while (await cursor.MoveNextAsync(token)) + { + foreach (var doc in cursor.Current) + { + if (doc.IsBsonNull) + continue; + + var rec = new AuditRecord + ( + DatabaseName: GetStr(doc,"database"), + Timestamp : doc.GetValue("ts" , DateTime.MinValue).AsBsonDateTime.AsUniversalTime, + Email : GetStr(doc,"email" ), + Ticket : GetStr(doc,"ticket"), + Success : doc.GetValue("ok" , false).ToBoolean(), + Error : GetStr(doc,"error" ), + Command : BsonDocument.Parse(doc.GetValue("command", "").ToString() ?? "") + ); + + list.Add(rec); + } + } + + return list; + } + + private static string GetStr(BsonDocument doc, string name) + { + if (!doc.Contains(name) || doc[name].IsBsonNull ) + return ""; + return doc[name].ToString() ?? ""; + } + //private static bool GetBool(BsonDocument doc, string name) + //{ + // if (!doc.Contains(name) || doc[name].IsBsonNull ) + // return false; + // return doc[name].ToBoolean(); + //} +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Audit/AuditedMongoDbDatabaseAdminService.cs b/Rms.Risk.Mango/Services/Audit/AuditedMongoDbDatabaseAdminService.cs new file mode 100644 index 0000000..3ed0fe0 --- /dev/null +++ b/Rms.Risk.Mango/Services/Audit/AuditedMongoDbDatabaseAdminService.cs @@ -0,0 +1,105 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango.Services.Audit; + +public class AuditedMongoDbDatabaseAdminService( + MongoDbConfigRecord _config, + MongoDbSettings _settings, + IUserSession _session, + IAuditService _audit, + string? databaseInstance = null ) : IMongoDbDatabaseAdminService +{ + private MongoDbDatabaseAdminService _mongo = new (_config, _settings, databaseInstance ?? _session.DatabaseInstance); + + public string Database => _mongo.Database; + + public Task> ListCollections(CancellationToken token = default) => _mongo.ListCollections(token); + + public async Task RunCommand(BsonDocument doc, string? originalCommand, CancellationToken token = default) + { + _audit.PreCheck(doc); + + if ( !MongoDbCommandHelper.IsReadOnlyCommand(doc) && !await _session.HasValidTask()) + { + + var origCommand = string.IsNullOrWhiteSpace(originalCommand) + ? doc + : BsonDocument.Parse(originalCommand) + ; + + var message = $"Ticket check failed: {_session.TaskCheckError}"; + var rec = new AuditRecord( + _session.Database, + DateTime.UtcNow, + _session.User.GetEmail(), + _session.TaskNumber ?? "", + false, + origCommand, + message + ); + await _audit.Record(rec, token); + throw new ApplicationException(message); + } + + try + { + var ret = await _mongo.RunCommand(doc, originalCommand, token); + + AuditRecord rec = ret.TryGetValue("ok", out var value) && !value.ToBoolean() + ? new( + _session.Database, + DateTime.UtcNow, + _session.User.GetEmail(), + _session.TaskNumber ?? "", + false, + doc, + ret["errmsg"]?.ToString() + ) + : new ( + _session.Database, + DateTime.UtcNow, + _session.User.GetEmail(), + _session.TaskNumber ?? "", + true, + doc + ); + + await _audit.Record(rec, token); + return ret; + } + catch (Exception ex) + { + var rec = new AuditRecord( + _session.Database, + DateTime.UtcNow, + _session.User.GetEmail(), + _session.TaskNumber ?? "", + false, + doc, + ex.ToString() + ); + await _audit.Record(rec, token); + throw; + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Audit/ChainedAuditService.cs b/Rms.Risk.Mango/Services/Audit/ChainedAuditService.cs new file mode 100644 index 0000000..74a3ff5 --- /dev/null +++ b/Rms.Risk.Mango/Services/Audit/ChainedAuditService.cs @@ -0,0 +1,47 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Rms.Risk.Mango.Interfaces; + +namespace Rms.Risk.Mango.Services.Audit; + +public class ChainedAuditService(IReadOnlyCollection _audit) : IAuditService +{ + public void PreCheck(BsonDocument command) + { + foreach (var auditService in _audit) + { + auditService.PreCheck(command); + } + } + + public async Task Record(AuditRecord rec, CancellationToken token = default) + { + if (MongoDbCommandHelper.IsReadOnlyCommand(rec.Command)) + return; + + foreach (var auditService in _audit) + { + await auditService.Record(rec, token); + } + } + + public Task> Audit(DateTime startDate, DateTime endDate, CancellationToken token = default) + => _audit.First().Audit(startDate, endDate, token); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/CommandListService.cs b/Rms.Risk.Mango/Services/CommandListService.cs new file mode 100644 index 0000000..5d5a8fd --- /dev/null +++ b/Rms.Risk.Mango/Services/CommandListService.cs @@ -0,0 +1,62 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Components; +using MongoDB.Bson; +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango.Services; + +public record CommandDef(string Name, MarkupString Description, bool AdminOnly); + +public interface ICommandListService +{ + Task> GetCommands(IMongoDbDatabaseAdminService admin, CancellationToken token); +} + +internal class CommandListService : ICommandListService +{ + private static readonly List _commands = []; + private static readonly Lock _commandsLock = new(); + + public async Task> GetCommands(IMongoDbDatabaseAdminService admin, CancellationToken token) + { + lock (_commandsLock) + { + if (_commands.Count > 0 ) + return _commands; + } + + var listCommands = BsonDocument.Parse("{ listCommands: 1}"); + var doc = await admin.RunCommand(listCommands, token); + + lock (_commands) + { + _commands.Clear(); + foreach (var cmd in doc["commands"]?.AsBsonDocument.Elements ?? []) + { + var name = cmd.Name; + var help = cmd.Value["help"].AsString.Replace("\n", "
"); + var isAdmin = cmd.Value["adminOnly"].AsBoolean; + + _commands.Add(new(name, new (help), isAdmin)); + } + return _commands; + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/ConnectedUser.cs b/Rms.Risk.Mango/Services/ConnectedUser.cs new file mode 100644 index 0000000..790ed2e --- /dev/null +++ b/Rms.Risk.Mango/Services/ConnectedUser.cs @@ -0,0 +1,30 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +public class ConnectedUser : IConnectedUser +{ + public Guid Id { get; } = Guid.NewGuid(); + public DateTime ConnectedAtUtc { get; } = DateTime.UtcNow; + + + public string Name { get; set; } = "anonymous"; + + public override string ToString() => $"{ConnectedAtUtc:yyyy-MM-dd HH:mm:ss} {Name}"; +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/ConnectedUserList.cs b/Rms.Risk.Mango/Services/ConnectedUserList.cs new file mode 100644 index 0000000..3203629 --- /dev/null +++ b/Rms.Risk.Mango/Services/ConnectedUserList.cs @@ -0,0 +1,79 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Reflection; +using log4net; + +namespace Rms.Risk.Mango.Services; + +public class ConnectedUserList : IConnectedUserList +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public static int UsersReportingIntervalMinutes = 20; + + private readonly List _users = []; + + public ConnectedUserList() + { + Task.Run(ReportingLoop); + } + + private async Task ReportingLoop() + { + while (true) + { + try + { + await Task.Delay(TimeSpan.FromMinutes(UsersReportingIntervalMinutes)); + + var users = string.Join("\n\t", Users); + _log.Debug($"Connected users:\n\t{users}"); + } + catch (Exception) + { + // ignore + } + } + // ReSharper disable once FunctionNeverReturns + } + + public IReadOnlyCollection Users + { + get + { + lock (_users) + { + var copy = new List(_users); + return copy; + } + } + } + + public void Add(IConnectedUser user) + { + lock (_users) + _users.Add(user); + } + + public void Remove(IConnectedUser user) + { + lock (_users) + _users.Remove(user); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Context/DatabaseConfigurationService.cs b/Rms.Risk.Mango/Services/Context/DatabaseConfigurationService.cs new file mode 100644 index 0000000..466a88b --- /dev/null +++ b/Rms.Risk.Mango/Services/Context/DatabaseConfigurationService.cs @@ -0,0 +1,234 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; +using Microsoft.Extensions.Options; +using Rms.Risk.Mango.Interfaces; +using Rms.Service.Bootstrap.Security; + +namespace Rms.Risk.Mango.Services.Context; + +// ReSharper disable InconsistentNaming +public class DatabaseConfigurationService : IDatabaseConfigurationService +// ReSharper restore InconsistentNaming +{ + public static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); + + private readonly IDatabaseConfigurationStorage? _storage; + private readonly IOptions _config; + private readonly IPasswordManager _passwordManager; + + public const string ContextTypeNameDbMangoDatabase = "dbMangoDatabase"; + + public DatabaseConfigurationService( + IOptions config, + IPasswordManager passwordManager, + IDatabaseConfigurationStorage? storage = null + ) + { + _storage = storage; + _config = config; + _passwordManager = passwordManager; + + Databases = _config.Value.Databases.ToDictionary(x => x.Key, x => x.Value.Clone()); + + var cts = new CancellationTokenSource(DefaultTimeout); + _ = Task.Run(Reload, cts.Token); + } + + private ConcurrentDictionary RawDatabases { get; set; } = []; + public Dictionary Databases { get; set; } + + public async Task Reload() + { + if ( _storage == null) + return; + + var cts = new CancellationTokenSource(DefaultTimeout); + var raw = await _storage.List("dbMango", cts.Token); + RawDatabases = new(raw.ToDictionary(x => x.Name, x => x)); + + var newDb = _config.Value.Databases.ToDictionary(x => x.Key, x => x.Value.Clone()); + + foreach (var databaseConfig in RawDatabases.Select( x => (x.Key, Config : ConvertTo(x.Value)))) + { + newDb[databaseConfig.Key] = databaseConfig.Config; + } + + Databases = newDb; + } + + public async Task Update(string name, DatabasesConfig.DatabaseConfig c, string email) + { + await Reload(); + + if (RawDatabases.ContainsKey(name)) + await UpdateInternal(name, c, email); + else + await CreateInternal(name, c, email); + } + + private async Task CreateInternal(string name, DatabasesConfig.DatabaseConfig c, string email) + { + var ctx = ConvertFrom(name, c, true); + + if ( _storage != null ) + { + var cts = new CancellationTokenSource(DefaultTimeout); + await _storage.Create(ctx, email, cts.Token); + } + + RawDatabases[name] = ctx; + } + + private async Task UpdateInternal(string name, DatabasesConfig.DatabaseConfig c, string email) + { + + var ctx = ConvertFrom(name, c, false); + + if ( _storage != null ) + { + var cts = new CancellationTokenSource(DefaultTimeout); + await _storage.Update(ctx, email, cts.Token); + } + + RawDatabases[name] = ctx; + } + + public async Task Delete(string name, string email) + { + await Reload(); + + if (!RawDatabases.TryGetValue(name, out var raw)) + throw new ApplicationException($"Configuration for {name} is not loaded"); + + if ( _storage != null ) + { + var cts = new CancellationTokenSource(DefaultTimeout); + await _storage.Delete(raw.ID, email, cts.Token); + } + + RawDatabases.Remove(name, out _); + } + + + private DatabasesConfig.DatabaseConfig ConvertTo(DbMangoDatabaseConfigContext ctx) + { + var userPass = DecryptPassword(ctx.DatabaseParams.UserAuthPassword); + var adminPass = DecryptPassword(ctx.DatabaseParams.AdminAuthPassword); + + var c = new DatabasesConfig.DatabaseConfig + { + Contacts = ctx.DatabaseParams.Contacts, + Config = new() + { + MongoDbUrl = ctx.DatabaseParams.MongoDbUrl, + MongoDbDatabase = ctx.DatabaseParams.MongoDbDatabase, + DirectConnection = ctx.DatabaseParams.DirectConnection, + UseTls = ctx.DatabaseParams.UseTls, + AllowShardAccess = ctx.DatabaseParams.AllowShardAccess, + Auth = new() + { + User = ctx.DatabaseParams.UserAuthUser, + Password = userPass, + AuthDatabase = ctx.DatabaseParams.UserAuthAuthDatabase, + Method = ctx.DatabaseParams.UserAuthMethod + }, + AdminAuth = new() + { + User = ctx.DatabaseParams.AdminAuthUser, + Password = adminPass, + AuthDatabase = ctx.DatabaseParams.AdminAuthAuthDatabase, + Method = ctx.DatabaseParams.AdminAuthMethod + } + }, + Groups = ctx.LdapParams + }; + + if (string.IsNullOrWhiteSpace(c.Config.Auth.User) || + string.IsNullOrWhiteSpace(c.Config.Auth.Password)) + { + c.Config.Auth = null; + } + + if (string.IsNullOrWhiteSpace(c.Config.AdminAuth.User) || + string.IsNullOrWhiteSpace(c.Config.AdminAuth.Password)) + { + c.Config.AdminAuth = null; + } + + return c; + } + + private string DecryptPassword(string? p) => + string.IsNullOrWhiteSpace(p) + ? "" + : p.StartsWith("*") + ? _passwordManager.DecryptPassword(p) + : p; + + private string EncryptPassword(string? p) => + string.IsNullOrWhiteSpace(p) + ? "" + : p.StartsWith("*") + ? p + : _passwordManager.EncryptPassword(p); + + private DbMangoDatabaseConfigContext ConvertFrom( string name, DatabasesConfig.DatabaseConfig c, bool isNew ) + { + DbMangoDatabaseConfigContext? raw = null; + if (!isNew && !RawDatabases.TryGetValue(name, out raw)) + throw new ApplicationException($"Configuration for {name} is not loaded"); + + var userPass = EncryptPassword(c.Config.Auth?.Password); + var adminPass = EncryptPassword(c.Config.AdminAuth?.Password); + + var ctx = new DbMangoDatabaseConfigContext + { + Type = ContextTypeNameDbMangoDatabase , + Name = isNew ? name : raw!.Name, + ID = isNew ? 0 : raw!.ID, + IsTemplate = !isNew && raw!.IsTemplate, + ProposedForDeletion = !isNew && raw!.ProposedForDeletion, + + DatabaseParams = new() + { + Contacts = c.Contacts, + MongoDbUrl = c.Config.MongoDbUrl, + MongoDbDatabase = c.Config.MongoDbDatabase, + DirectConnection = c.Config.DirectConnection, + UseTls = c.Config.UseTls, + AllowShardAccess = c.Config.AllowShardAccess, + + UserAuthUser = c.Config.Auth?.User ?? "", + UserAuthPassword = userPass, + UserAuthAuthDatabase = c.Config.Auth?.AuthDatabase, + UserAuthMethod = c.Config.Auth?.Method, + + AdminAuthUser = c.Config.AdminAuth?.User ?? "", + AdminAuthPassword = adminPass, + AdminAuthAuthDatabase = c.Config.AdminAuth?.AuthDatabase, + AdminAuthMethod = c.Config.AdminAuth?.Method, + }, + LdapParams = c.Groups + }; + + return ctx; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Context/IDatabaseConfigurationService.cs b/Rms.Risk.Mango/Services/Context/IDatabaseConfigurationService.cs new file mode 100644 index 0000000..592bb3e --- /dev/null +++ b/Rms.Risk.Mango/Services/Context/IDatabaseConfigurationService.cs @@ -0,0 +1,29 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Interfaces; + +namespace Rms.Risk.Mango.Services.Context; + +public interface IDatabaseConfigurationService +{ + Dictionary Databases { get; } + Task Reload(); + Task Update(string name, DatabasesConfig.DatabaseConfig c, string email); + Task Delete(string name, string email); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/DocumentationService.cs b/Rms.Risk.Mango/Services/DocumentationService.cs new file mode 100644 index 0000000..64822b2 --- /dev/null +++ b/Rms.Risk.Mango/Services/DocumentationService.cs @@ -0,0 +1,269 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using Microsoft.Extensions.Options; +using Rms.Service.Bootstrap.Security; +using System.Collections.Concurrent; +using System.IO.Compression; +using System.Net; +using System.Reflection; +using System.Text; + +namespace Rms.Risk.Mango.Services; + +/// +/// Provides functionality for retrieving and caching documentation for MongoDB commands. +/// +/// +/// This service fetches documentation for MongoDB commands from a remote source, caches the results, +/// and handles gzip-compressed files. It ensures that documentation is retrieved efficiently and avoids redundant +/// network calls by caching both existing and non-existing documentation. +/// +public class DocumentationService : IDocumentationService +{ + private readonly IHttpClientFactory _factory; + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + private readonly DbMangoSettings _dbMangoSettings; + private readonly string _tempFolderPath; + + private readonly ConcurrentDictionary _documentationCache = new(StringComparer.OrdinalIgnoreCase); + + public DocumentationService( + IOptions dbMangoSettings, + IHttpClientFactory factory + ) + { + _factory = factory; + _dbMangoSettings = dbMangoSettings.Value; + + _tempFolderPath = Path.Combine(Path.GetTempPath(), "MongoDbDocumentation"); + if (!Directory.Exists(_tempFolderPath)) + { + Directory.CreateDirectory(_tempFolderPath); + } + } + + public async Task TryGetHint(string commandName, CancellationToken token = default) + { + try + { + if ( string.IsNullOrWhiteSpace(_dbMangoSettings.MongoDbDocUrl)) + return null; + + commandName = commandName.Trim(); + if ( commandName.Length < 4) + return null; + + if ( _documentationCache.TryGetValue(commandName, out var documentation)) + return documentation; + + documentation = await LoadCommandDocumentation(commandName, token); + _log.Info( !string.IsNullOrWhiteSpace(documentation) + ? $"Loaded documentation for command '{commandName}'" + : $"No documentation found for command '{commandName}'" + ); + + _documentationCache[commandName] = documentation; // even if empty. this prevents re-fetching of non-existing documentation + return documentation; + } + catch (Exception ex) + { + _log.Warn($"Failed to load documentation for command '{commandName}': {ex.Message}", ex); + } + return null; + } + + public async Task TryGetMarkdown(string commandName, CancellationToken token = default) + { + try + { + if ( string.IsNullOrWhiteSpace(_dbMangoSettings.MongoDbDocUrl)) + return null; + + commandName = commandName.Trim(); + if ( commandName.Length < 4) + return null; + + var lines = await DownloadDocumentation(commandName, token); + if ( lines.Length == 0 ) + return null; + + var converter = new RstToMarkdownConverter(); + var markdown = converter.Convert(lines); + return markdown; + } + catch (Exception ex) + { + _log.Warn($"Failed to load documentation for command '{commandName}': {ex.Message}", ex); + } + + return null; + } + + private string GetMongoDbDocUrl(string commandName) => $"{_dbMangoSettings.MongoDbDocUrl}{commandName}.txt"; + + private async Task LoadCommandDocumentation(string commandName, CancellationToken token) + { + var lines = await DownloadDocumentation(commandName, token); + if ( lines.Length == 0 ) + return null; + + var syntaxIndex = Array.FindIndex(lines, line => line.Trim() == "Syntax"); + if (syntaxIndex == -1) + { + throw new InvalidDataException("Syntax section not found in documentation."); + } + + var codeBlockIndex = Array.FindIndex(lines, syntaxIndex, line => line.Trim() == ".. code-block:: javascript"); + if (codeBlockIndex == -1) + { + throw new InvalidDataException("Code block section not found in documentation."); + } + + var codeBlock = new List(); + for (var i = codeBlockIndex + 1; i < lines.Length; i++) + { + if (!lines[i].StartsWith(" ") && !String.IsNullOrWhiteSpace(lines[i])) + break; + + codeBlock.Add(lines[i]); + } + + return string.Join(Environment.NewLine, codeBlock); + } + + private async Task DownloadDocumentation(string commandName, CancellationToken token) + { + var mongoDbDocUrl = _dbMangoSettings.MongoDbDocUrl; + if ( string.IsNullOrWhiteSpace(mongoDbDocUrl)) + return []; + + if ( mongoDbDocUrl.EndsWith(".zip")) + return LoadFromZip(commandName, token); + + return await LoadFromWeb(commandName, token); + } + + private string[] LoadFromZip(string commandName, CancellationToken token) + { + if ( string.IsNullOrWhiteSpace(_dbMangoSettings.MongoDbDocUrl) || + !_dbMangoSettings.MongoDbDocUrl.EndsWith(".zip")) + { + return []; + } + + var zipFilePath = _dbMangoSettings.MongoDbDocUrl; + if (!File.Exists(zipFilePath)) + zipFilePath = Path.Combine(AppContext.BaseDirectory, _dbMangoSettings.MongoDbDocUrl); + + if (!File.Exists(zipFilePath)) + return []; + + using var zipArchive = ZipFile.OpenRead(zipFilePath); + var entry = zipArchive.GetEntry($"{commandName}.txt"); + if (entry == null) + return []; + + using var stream = entry.Open(); + using var reader = new StreamReader(stream); + var content = reader.ReadToEnd(); + + return content.Replace("\r", "").Split('\n', StringSplitOptions.RemoveEmptyEntries); + } + + private async Task LoadFromWeb(string commandName, CancellationToken token) + { + var url = GetMongoDbDocUrl(commandName); + var filePath = Path.Combine(_tempFolderPath, $"{commandName}.txt.gz"); + + if (!File.Exists(filePath)) + { + using var httpClient = CreateClient(); + await DownloadFileFromUrlAsync( httpClient, url, filePath, token); + } + else + _log.Debug($"Documentation file already exists: {filePath}"); + + var lines = await ReadGzipCompressedFileAsync(filePath, token); + return lines; + } + + private static async Task DownloadFileFromUrlAsync(HttpClient httpClient, string url, string destinationFilePath, CancellationToken token) + { + _log.Debug($"Downloading file from URL: {url}"); + + var response = await httpClient.GetAsync(url, token); + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"Failed to download file from URL: {url}. Status code: {response.StatusCode}"); + + token.ThrowIfCancellationRequested(); + + var fileContent = await response.Content.ReadAsStringAsync(token); + if (File.Exists(destinationFilePath)) + return; + + await WriteFileGzipCompressedAsync(destinationFilePath, fileContent, token); + } + + private HttpClient CreateClient() + { + if (string.IsNullOrWhiteSpace(_dbMangoSettings.MongoDbDocProxyUrl)) + return _factory.CreateClient(); + + var handler = CertificateHelper.ConfigureHttpsHandler("MongoDBDoc"); + handler.Proxy = new WebProxy(_dbMangoSettings.MongoDbDocProxyUrl); + + return new (handler); + } + + + private static async Task WriteFileGzipCompressedAsync(string destinationFilePath, string fileContent, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + await using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + await using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal); + var bytes = Encoding.UTF8.GetBytes(fileContent); + await gzipStream.WriteAsync(bytes, token); + } + private static async Task ReadGzipCompressedFileAsync(string filePath, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gzipStream); + + var lines = new List(); + while (!streamReader.EndOfStream) + { + token.ThrowIfCancellationRequested(); + var line = await streamReader.ReadLineAsync(token); + if (line != null) + { + lines.Add(line); + } + } + + return lines.ToArray(); + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/FileUtils.cs b/Rms.Risk.Mango/Services/FileUtils.cs new file mode 100644 index 0000000..e5900c5 --- /dev/null +++ b/Rms.Risk.Mango/Services/FileUtils.cs @@ -0,0 +1,374 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Diagnostics; +using System.Reflection; +using log4net; + +namespace Rms.Risk.Mango.Services; + +public static class FileUtils +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public static string ExecutionEnvironment = ""; + + /// + /// Use this call BEFORE creating a file. + /// Deletion done synchronously. + /// + /// File to delete + /// Throw an exception on failure (default true) + /// Log error on failure (default true) + /// + public static void ForceDelete(string fileName, bool throwOnFailure = true, bool logErrorOnFailure = true, TimeSpan? timeout = null) + { + if (fileName == null || !File.Exists(fileName)) + return; + + timeout ??= TimeSpan.FromSeconds(1); + DeletionCycle( fileName, timeout.Value, out var firstException); + if ( !File.Exists( fileName ) ) + return; + + var msg = $"Unable to delete File=\"{fileName}\""; + + if (firstException != null) + { + msg += $" : exception \"{firstException.Message}\""; + } + if (logErrorOnFailure) + _log.Error( msg ); + if (throwOnFailure) + throw new ApplicationException(msg); + } + + /// + /// Use this method AFTER you finished working with a file. + /// Deletion may be delayed and executed asynchronously. + /// + /// File to delete + public static void SafeDelete(string fileName) + { + if (fileName == null || !File.Exists(fileName)) + return; + + try + { + // try to delete synchronously + File.Delete(fileName); + } + catch + { + // start a delayed task + + Task.Factory.StartNew( () => + { + DeletionCycle( fileName, TimeSpan.FromSeconds( 5 ) , out var firstException); + if (firstException != null) + { + _log.Debug($"Unable to delete file {fileName} : {firstException.Message}"); + } + }); + } + } + + private static void DeletionCycle( string fileName, TimeSpan timeout , out Exception? firstException) + { + var sw = new Stopwatch(); + sw.Start(); + firstException = null; + try + { + do + { + try + { + if (File.Exists(fileName)) + File.Delete( fileName ); + return; + } + catch ( Exception ex ) + { + firstException ??= ex; + } + + Task.Delay(TimeSpan.FromMilliseconds(333)); // 1/3 second + + } while ( sw.Elapsed < timeout ); + } + catch ( Exception ex ) + { + _log.Debug( $"Can't delete File=\"{fileName}\"", ex ); + firstException ??= ex; + // swallow + } + } + public static string Shield(string key, string replace = ".") => + //remove any illegal filename chars (:,/,\,<,> etc) and replace with a replacement string + string.Join(replace, key.Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)); + + + /// + /// Copy file with .tmp renaming + /// + /// + /// + /// Log error on failure in ForceDelete (default true) + public static void SafeCopy(string dest, string src, bool logErrorInForceDelete = true) + { + var tempDest = dest + ".tmp"; + try + { + var directory = new FileInfo(dest).Directory; + if (directory is { Exists: false }) + directory.Create(); + + ForceDelete( tempDest, logErrorOnFailure: logErrorInForceDelete ); + File.Copy( src, tempDest, true ); + ForceDelete( dest, logErrorOnFailure: logErrorInForceDelete ); + File.Move( tempDest, dest ); + } + catch ( Exception ) + { + SafeDelete( tempDest ); + throw; + } + } + + public static T DoSafe( Func action, string message ) + { + Exception? firstException = null; + var endAt = DateTime.Now + TimeSpan.FromSeconds( 10 ); + + while( DateTime.Now < endAt ) + { + try + { + return action(); + } + catch ( Exception e ) + { + firstException ??= e; + _log.Warn( $"{message} (retry pending): {e.Message}", e ); + } + + Thread.Sleep( 333 ); + } + + throw new ApplicationException( $"{message}: {firstException?.Message}", firstException ); + } + + public static StreamReader SafeOpenText( string fileName ) + => DoSafe( () => File.OpenText( fileName ), $"Can't open File=\"{fileName}\"" ); + + public static void SafeRename( string srcFileName, string destFileName ) + => DoSafe( () => + { + ForceDelete(destFileName); + File.Move( srcFileName, destFileName ); + return true; + }, $"Can't rename File=\"{srcFileName}\" To=\"{destFileName}\"" ); + + public static bool SafeDeleteFolder( string folder ) + { + if ( string.IsNullOrWhiteSpace( folder ) || !Directory.Exists( folder ) ) + return true; + + try + { + Directory.Delete(folder, true); + if (!Directory.Exists(folder)) + return true; + _log.Warn( $"Switching to slow method as Folder=\"{folder}\" is still exists"); + } + catch (Exception ex) + { + _log.Warn( $"Switching to slow method as we can't remove Folder=\"{folder}\" {ex.Message}", ex); + } + + var files = GetFiles(folder); + foreach (var file in files) + { + ForceDelete(file, false); + } + + try + { + Directory.Delete(folder, true); + if (!Directory.Exists(folder)) + return true; + _log.Error( $"Folder=\"{folder}\" should have been deleted, but it still exists"); + } + catch (Exception ex) + { + _log.Error( $"Can't delete Folder=\"{folder}\": {ex.Message}", ex); + } + + return false; + } + + public static IEnumerable GetFiles(string path, bool throwOnError = false) + { + var queue = new Queue(); + queue.Enqueue(path); + while (queue.Count > 0) + { + path = queue.Dequeue(); + try + { + foreach (var subDir in Directory.GetDirectories(path)) + { + queue.Enqueue(subDir); + } + } + catch (Exception ex) + { + _log.Error( $"Error listing directories for Folder=\"{path}\": {ex.Message}", ex); + if (throwOnError) + throw; + } + } + string[]? files = null; + try + { + files = Directory.GetFiles(path); + } + catch (Exception ex) + { + _log.Error($"Error listing files within Folder=\"{path}\": {ex.Message}", ex); + if (throwOnError) + throw; + } + + if (files == null) + yield break; + + foreach (var t in files) + yield return t; + } + + // If folderName is relative (incl. if it is empty), then it is appended to baseFolder. + // Otherwise, if folderName is an absolute path, then baseFolder is ignored. + // If fileName is relative (incl. if it is empty), then it is appended to the above combination. + // Otherwise if fileName is an absolute path, then it replaces everything. + // (Note these are all just desirable consequences of using Path.Combine().) + // + // The result of the above gets the following substitutions: + // %DATEDIR% => yyyy\MM (e.g. 2021\07) + // %ENV% => PROD or UAT or DEV according to ExecutionEnvironment + // % => "-yyyy-MM-dd" + // Note: Date-dependent substitutions only take place if date argument is not null. + // + // Null folder/file names are converted to "", for which Path.Combine is well defined. + // + // Nothing is saved or created here! Just building path name with (optional) substitutions. + // + public static string? InterpolatePath(string baseFolder, string folderName, string fileName, + DateTime? date = null, bool useFullPath = true) + { + string fullPathName; + try + { + fullPathName = useFullPath + ? Path.GetFullPath(Path.Combine(Path.Combine(baseFolder ?? "", folderName ?? ""), + fileName ?? "")) + : Path.Combine(Path.Combine(baseFolder ?? "", folderName ?? ""), + fileName ?? ""); + + var envName = ExecutionEnvironment.Replace("Cloud-", ""); + + fullPathName = fullPathName.Replace("%ENV%", envName); + if (date != null) + { + fullPathName = fullPathName.Replace("%DATEDIR%", date.Value.Year.ToString() + + Path.DirectorySeparatorChar + date.Value.Month.ToString("D2")); + fullPathName = fullPathName.Replace("%", "-" + date.Value.ToString("yyyy-MM-dd")); + } + } + catch (Exception e) + { + _log.Error($"Failed to build path from '{baseFolder}' + '{folderName}' + '{fileName}' + '{date?.ToString()}': {e.Message}"); + return null; + } + return fullPathName; + } + + /// + /// Delete a directory and its subdirs and files. + /// Fixes the issue were Directory.Delete doesn't work in the hidden .git folder + /// + public static void ObliterateDirectory(string targetDir) + { + if (!Directory.Exists(targetDir)) + return; + + try + { + File.SetAttributes(targetDir, FileAttributes.Normal); + } + catch (Exception e) + { + _log.Warn($"Cant set attributes for {targetDir} directory", e); + } + + var files = Directory.GetFiles(targetDir); + + foreach (var file in files) + { + try + { + File.SetAttributes(file, FileAttributes.Normal); + ForceDelete(file, throwOnFailure:false); + } + catch (Exception e) + { + _log.Warn($"Cant delete {targetDir} directory", e); + } + } + + try + { + foreach (var dir in Directory.EnumerateDirectories(targetDir)) + { + ObliterateDirectory(dir); + } + } + catch (Exception e) + { + _log.Warn($"Cant enumerate {targetDir} directories", e); + } + + Directory.Delete(targetDir, false); + } + + public static void CopyDirectory(string source, string target) + { + if ( !Directory.Exists(target)) + Directory.CreateDirectory(target!); + CopyRecursively(new (source), new (target)); + } + + private static void CopyRecursively(DirectoryInfo source, DirectoryInfo target) + { + foreach (var dir in source.GetDirectories()) + CopyRecursively(dir, target.CreateSubdirectory(dir.Name)); + foreach (var file in source.GetFiles()) + file.CopyTo(Path.Combine(target.FullName, file.Name)); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/IConnectedUserList.cs b/Rms.Risk.Mango/Services/IConnectedUserList.cs new file mode 100644 index 0000000..5722c5f --- /dev/null +++ b/Rms.Risk.Mango/Services/IConnectedUserList.cs @@ -0,0 +1,33 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +public interface IConnectedUser +{ + Guid Id { get; } + string Name { get; set; } + DateTime ConnectedAtUtc { get; } +} + +public interface IConnectedUserList +{ + IReadOnlyCollection Users { get; } + void Add(IConnectedUser user); + void Remove(IConnectedUser user); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/IDocumentationService.cs b/Rms.Risk.Mango/Services/IDocumentationService.cs new file mode 100644 index 0000000..3f360fb --- /dev/null +++ b/Rms.Risk.Mango/Services/IDocumentationService.cs @@ -0,0 +1,43 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +/// +/// Provides methods to retrieve documentation for MongoDB commands. +/// +public interface IDocumentationService +{ + /// + /// Attempts to retrieve the documentation for a specified MongoDB command in plain text format. + /// + /// The name of the MongoDB command. + /// A cancellation token to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains the documentation as a string, or null if the documentation could not be retrieved. + Task TryGetHint(string commandName, CancellationToken token = default); + + /// + /// Attempts to retrieve the documentation for a specified MongoDB command in Markdown format. + /// This method fetches the documentation, processes it to extract relevant Markdown content, + /// and ensures efficient retrieval by utilizing caching mechanisms. + /// + /// The name of the MongoDB command. + /// A cancellation token to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains the documentation in Markdown format as a string, or null if the documentation could not be retrieved. + Task TryGetMarkdown(string commandName, CancellationToken token = default); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/IMigrationEngine.cs b/Rms.Risk.Mango/Services/IMigrationEngine.cs new file mode 100644 index 0000000..ed37875 --- /dev/null +++ b/Rms.Risk.Mango/Services/IMigrationEngine.cs @@ -0,0 +1,171 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Security.Claims; +using MongoDB.Bson; + +namespace Rms.Risk.Mango.Services; + +public class MigrationJob +{ + public enum JobType + { + Copy, + Download, + Upload + } + + public class CollectionJob + { + public DateTime StartedAtUtc { get; set; } + public DateTime FinishedAtUtc { get; set; } + public string SourceCollection { get; init; } = ""; + public string DestinationCollection { get; init; } = ""; + public BsonDocument? Filter { get; set; } + public BsonDocument? Projection { get; set; } + + public long Count { get; set; } + public long? Cleared { get; set; } + public long Copied { get; set; } + public double Progress => Count == 0 ? 0.0 : Copied / (double)Count; + public bool Complete { get; set; } + public Exception? Exception { get; set; } + + public string Error => Exception?.Message ?? ""; + + public override string ToString() + => $"{SourceCollection} -> {DestinationCollection} ({Count} {Cleared} {Progress:P2}) {Error}"; + + public double DocsPerSecond + { + get + { + if (Copied == 0 || Elapsed == TimeSpan.Zero) + return 0.0; + + return Copied / Elapsed.TotalSeconds; + } + } + + public TimeSpan Elapsed + { + get + { + if ( StartedAtUtc == default) + return TimeSpan.Zero; + + var end = Complete ? FinishedAtUtc : DateTime.UtcNow; + var duration = end - StartedAtUtc; + + return duration; + } + } + + public TimeSpan Remaining + { + get + { + var dps = DocsPerSecond; + if (dps == 0) + return TimeSpan.Zero; + + return TimeSpan.FromSeconds((Count - Copied) / dps); + } + } + + + public long TotalReadMSec { get; set;} + public long TotalWriteMSec { get; set;} + + public double ReadDPS + { + get + { + if (Copied == 0 || TotalReadMSec == 0L ) + return 0.0; + + return 1000.0 * Copied / TotalReadMSec; + } + } + + public double WriteDPS + { + get + { + if (Copied == 0 || TotalWriteMSec == 0L) + return 0.0; + + return 1000.0 * Copied / TotalWriteMSec; + } + } + + } + + public int JobId { get; init; } = ++_jobId; + public JobType Type { get; init; } = JobType.Copy; + public string Ticket { get; set; } = ""; + public string Email { get; init; } = ""; + public DateTime StartedAtUtc { get; set; } + public DateTime FinishedAtUtc { get; set; } + public string SourceDatabase { get; init; } = ""; + public string SourceDatabaseInstance { get; init; } = ""; + public string DestinationDatabase { get; init; } = ""; + public string DestinationDatabaseInstance { get; init; } = ""; + public int BatchSize { get; init; } = 1_000; + public bool Upsert { get; init; } + public bool ClearDestinationBefore { get; init; } + public bool DisableIndexes { get; init; } = false; + public List Status { get; init; } = []; + public bool Complete { get; set; } + public Exception? Exception { get; set; } + + private static int _jobId; + + public string Error => Exception?.Message ?? ""; + + /// + /// For Type.Upload, this is the temp file name with the zip just uploaded by user. + /// + public string UploadedFileName { get; init; } = ""; + + /// + /// For Type.Download, this is the result of the download operation, e.g. URL you need to access to download the file. + /// + public string DownloadUrl { get; set; } = ""; + + /// + /// Maximum number of parallel write operations to run for this job. + /// + public int MaxDegreeOfParallelism { get; set; } = 5; + + public override string ToString() + => Type switch + { + JobType.Copy => $"[{JobId:000}] Migrate {SourceDatabase} -> {DestinationDatabase} ({Status.Count}): {Email}", + JobType.Download => $"[{JobId:000}] Download from {SourceDatabase} ({Status.Count}): {Email}", + JobType.Upload => $"[{JobId:000}] Upload to {SourceDatabase} ({Status.Count}): {Email}", + _ => $"[{JobId:000}] {Type} {SourceDatabase} -> {DestinationDatabase} ({Status.Count}): {Email}" + }; +} + +public interface IMigrationEngine +{ + List List(); + Task Add(MigrationJob job, ClaimsPrincipal user); + Task Cancel(MigrationJob job, ClaimsPrincipal user); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/IMongoDbServiceFactory.cs b/Rms.Risk.Mango/Services/IMongoDbServiceFactory.cs new file mode 100644 index 0000000..a87300e --- /dev/null +++ b/Rms.Risk.Mango/Services/IMongoDbServiceFactory.cs @@ -0,0 +1,33 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using MongoDB.Bson; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.MongoDb; + +namespace Rms.Risk.Mango.Services; + +public interface IMongoDbServiceFactory +{ + IPivotTableDataSource CreatePivot(string database, string databaseInstance); + IMongoDbDatabaseAdminService CreateAdmin(string database, IUserSession session, string databaseInstance); + IMongoDbService Create(string database, string collection, string databaseInstance); + IMongoDbDatabaseAdminService CreateAdmin(MongoDbConfigRecord config, string auditDatabase, IUserSession session, string instanceName); + IAuditService CreateAudit(string auditDatabase, string databaseInstance); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/ISingleUseTokenService.cs b/Rms.Risk.Mango/Services/ISingleUseTokenService.cs new file mode 100644 index 0000000..d702618 --- /dev/null +++ b/Rms.Risk.Mango/Services/ISingleUseTokenService.cs @@ -0,0 +1,25 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +public interface ISingleUseTokenService +{ + string GetSingleUseToken(); + bool CheckSingleUseToken( string token ); +} diff --git a/Rms.Risk.Mango/Services/ITempFileStorage.cs b/Rms.Risk.Mango/Services/ITempFileStorage.cs new file mode 100644 index 0000000..8219e96 --- /dev/null +++ b/Rms.Risk.Mango/Services/ITempFileStorage.cs @@ -0,0 +1,53 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +/// +/// Temporary files storage. All files there will be erased when workflow finishes (even if failed). +/// +public interface ITempFileStorage +{ + /// + /// Local folder that persist between workflow runs. However, contents of this folder can be cleared without any warnings. + /// Use for persistent caching only. Do not use for storage. + /// + string LocalPersistentFolder { get; } + /// + /// Temporary files folder path. It's not recommended to use. Better use + /// + string TempFolder { get; } + /// + /// Create a new temp file name. You can't control its name, but name will contain type name of "t" argument. + /// + /// Type that requested the temp file creation + /// Unique temp file name + string GetTempFileName(Type t); + + /// + /// Create a new temp file name. You can't control its name, but name will contain type name of "t" argument. + /// + /// Unique temp file name + string GetTempFileName(); + /// + /// Create a new temp file name. You can't control its name, but name will contain type name of "t" argument. + /// + /// Type name that requested the temp file creation or any other indicative string + /// Unique temp file name + string GetTempFileName(string key); +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Logging/Log4NetHelper.cs b/Rms.Risk.Mango/Services/Logging/Log4NetHelper.cs new file mode 100644 index 0000000..5e13f6e --- /dev/null +++ b/Rms.Risk.Mango/Services/Logging/Log4NetHelper.cs @@ -0,0 +1,30 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services.Logging; + +public static class Log4NetHelper +{ + public static void AddLog4NetRedirection(this IServiceCollection services) + { + var loggerFactory = services.BuildServiceProvider().GetRequiredService(); + + var appender = new Log4NetToMicrosoftLoggerAppender(loggerFactory); + log4net.Config.BasicConfigurator.Configure(appender); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Logging/Log4NetToMicrosoftLoggerAppender.cs b/Rms.Risk.Mango/Services/Logging/Log4NetToMicrosoftLoggerAppender.cs new file mode 100644 index 0000000..9517304 --- /dev/null +++ b/Rms.Risk.Mango/Services/Logging/Log4NetToMicrosoftLoggerAppender.cs @@ -0,0 +1,65 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Concurrent; +using log4net.Appender; +using log4net.Core; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Rms.Risk.Mango.Services.Logging; + +public class Log4NetToMicrosoftLoggerAppender(ILoggerFactory _loggerFactory) : AppenderSkeleton +{ + private readonly ILoggerFactory _loggerFactory = _loggerFactory ?? throw new ArgumentNullException(nameof(_loggerFactory)); + + private readonly ConcurrentDictionary _loggerCache = new(); + + protected override void Append(LoggingEvent loggingEvent) + { + if (loggingEvent.Level == null || loggingEvent.LoggerName == null) + return; + + // Get logger by name from the cache or create a new one + var logger = _loggerCache.GetOrAdd(loggingEvent.LoggerName, name => _loggerFactory.CreateLogger(name)); + + var logLevel = MapLogLevel(loggingEvent.Level); + var message = loggingEvent.RenderedMessage; + + if (message == null) + return; + + if (loggingEvent.ExceptionObject != null) + { + logger.Log(logLevel, loggingEvent.ExceptionObject, message); + } + else + { + logger.Log(logLevel, message); + } + } + + private static LogLevel MapLogLevel(Level log4netLevel) + { + return log4netLevel == Level.Debug ? LogLevel.Debug : + log4netLevel == Level.Info ? LogLevel.Information : + log4netLevel == Level.Warn ? LogLevel.Warning : + log4netLevel == Level.Error ? LogLevel.Error : + log4netLevel == Level.Fatal ? LogLevel.Critical : + LogLevel.None; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/MarkdownPipelineBuilderExtensions.cs b/Rms.Risk.Mango/Services/MarkdownPipelineBuilderExtensions.cs new file mode 100644 index 0000000..f8ccdab --- /dev/null +++ b/Rms.Risk.Mango/Services/MarkdownPipelineBuilderExtensions.cs @@ -0,0 +1,58 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Markdig; +using Markdig.Renderers.Html; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace Rms.Risk.Mango.Services; + +public static class MarkdownPipelineBuilderExtensions +{ + public static bool IsLocalUrl(string url) => !url.StartsWith("http://") && !url.StartsWith("https://"); + + public static MarkdownPipelineBuilder UseLocalUrlHandler(this MarkdownPipelineBuilder pipeline) + { + pipeline.DocumentProcessed += document => + { + foreach (var link in document.Descendants()) + { + if (link.Url == null) + continue; + + + if (link.Url.StartsWith("/")) + { + link.GetAttributes().AddPropertyIfNotExist("target", "_blank"); + } + else if (IsLocalUrl(link.Url)) + { + var name = Path.GetFileNameWithoutExtension(link.Url); + // Modify the URL + link.Url = $"/doc/{name}"; + } + else + { + link.GetAttributes().AddPropertyIfNotExist("target", "_blank"); + } + } + }; + return pipeline; + } +} diff --git a/Rms.Risk.Mango/Services/MenuService.cs b/Rms.Risk.Mango/Services/MenuService.cs new file mode 100644 index 0000000..29816ad --- /dev/null +++ b/Rms.Risk.Mango/Services/MenuService.cs @@ -0,0 +1,43 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Interfaces; + +namespace Rms.Risk.Mango.Services; + +public class MenuService : IMenuService +{ + private static readonly List _menuItems = new(); + + public void AddMenuItem(string menu, string title, string url) + { + _menuItems.Add( new ( menu, title, url ) ); + } + + public List Get(string menu) + => _menuItems.Where( x => x.Menu == menu ).ToList(); + + public List GetMenus() + => _menuItems + .Select(x => x.Menu) + .Distinct() + .OrderBy(x =>x) + .ToList() + ; + +} diff --git a/Rms.Risk.Mango/Services/MigrationEngine.cs b/Rms.Risk.Mango/Services/MigrationEngine.cs new file mode 100644 index 0000000..e84d392 --- /dev/null +++ b/Rms.Risk.Mango/Services/MigrationEngine.cs @@ -0,0 +1,701 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Rms.Risk.Mango.Controllers; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Rms.Risk.Mango.Pivot.UI.Services; +using Rms.Risk.Mango.Services.Context; +using Rms.Risk.Mango.Services.Security; +using Rms.Service.Bootstrap.Security; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO.Compression; +using System.Reflection; +using System.Security.Claims; +using log4net; +using Rms.Risk.Mango.Interfaces; + +namespace Rms.Risk.Mango.Services; + +public class MigrationEngine( + // ReSharper disable InconsistentNaming + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + IDatabaseConfigurationService _databases, + IAuthorizationService _auth, + IMongoDbServiceFactory _factory, + ITempFileStorage _storage, + IPasswordManager _passwordManager, + ISingleUseTokenService _singleUseTokenService, + IOptions _settings + // ReSharper restore InconsistentNaming +) : IMigrationEngine +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + private class InternalMigrationJobStatus(MigrationJob job) + { + public MigrationJob Status { get; } = job; + public CancellationTokenSource Cts { get; } = new(); + } + + private readonly ConcurrentDictionary _jobs = []; + private readonly ConcurrentBag _allJobs = []; + + public List List() => _allJobs.OrderBy(x => x.JobId).ToList(); + + public async Task Add(MigrationJob job, ClaimsPrincipal user) + { + if (_jobs.TryGetValue(job.DestinationDatabase, out var existing )) + throw new($"Only one migration job allowed for destination {job.DestinationDatabase}. Migration already initiated by {existing.Status.Email}."); + + await CheckJobAccess(job, user); + + + var newJob = new InternalMigrationJobStatus(job); + + if ( !_jobs.TryAdd(job.DestinationDatabase, newJob) ) + throw new($"Only one migration job allowed for destination {job.DestinationDatabase}."); + + _allJobs.Add(job); + _ = Task.Run(() => RunJob(job, newJob.Cts.Token)); + } + + private async Task CheckJobAccess(MigrationJob job, ClaimsPrincipal user, bool forCancel = false) + { + if (!_databases.Databases.ContainsKey(job.SourceDatabase)) + throw new($"Source database {job.SourceDatabase} is not defined"); + + if (!_databases.Databases.ContainsKey(job.DestinationDatabase)) + throw new($"Destination database {job.DestinationDatabase} is not defined"); + + var enableSource = await _auth.AuthorizeAsync( + user, + job.SourceDatabase, + [new ReadAccessRequirement()]); + + if (!enableSource.Succeeded) + throw new("Read access to the source database required"); + + if ( job.Type != MigrationJob.JobType.Download ) + { + var enableDest = await _auth.AuthorizeAsync( + user, + job.DestinationDatabase, + [new WriteAccessRequirement()]); + + if (!enableDest.Succeeded) + throw new("Write access to the destination database required"); + } + + if ( forCancel) + { + var enableCancel = await _auth.AuthorizeAsync( + user, + job.DestinationDatabase, + [new AdminAccessRequirement()]); + + if (!enableCancel.Succeeded) + throw new("Admin access to the destination database required"); + } + } + + public async Task Cancel(MigrationJob job, ClaimsPrincipal user) + { + await CheckJobAccess(job, user, true); + + if (!_jobs.TryGetValue(job.DestinationDatabase, out var existing )) + throw new($"No migration job registered for {job.DestinationDatabase}"); + if ( existing.Status.JobId != job.JobId ) + throw new($"Migration job for {job.DestinationDatabase} have JobId=\"{existing.Status.JobId:D}\", but received {job.JobId:D}"); + + await existing.Cts.CancelAsync(); + } + + private async Task RunJob(MigrationJob job, CancellationToken token) + { + var audit = _factory.CreateAudit(job.DestinationDatabase, job.DestinationDatabaseInstance); + try + { + job.StartedAtUtc = DateTime.UtcNow; + var auditRec = CreateAuditRecord(job, $"ticket: {job.Ticket} email: {job.Email} Migration job ({job.Type.ToString()}) starting..."); + await audit.Record(auditRec, token); + + switch ( job.Type) + { + case MigrationJob.JobType.Copy: + foreach (var collStatus in job.Status) + { + try + { + collStatus.StartedAtUtc = DateTime.UtcNow; + await MigrateOneCollection(job, collStatus, token); + collStatus.FinishedAtUtc = DateTime.UtcNow; + } + catch (Exception ex) + { + collStatus.FinishedAtUtc = DateTime.UtcNow; + collStatus.Complete = true; + collStatus.Exception = ex; + job.Exception = ex; + } + } + break; + case MigrationJob.JobType.Download: + await DownloadCollections(job, token); + break; + case MigrationJob.JobType.Upload: + await UploadCollectionsFromZip(job, token); + break; + default: + throw new($"Unsupported migration job type: {job.Type}"); + } + + } + catch (Exception ex) + { + job.Exception = ex; + } + finally + { + job.Complete = true; + job.FinishedAtUtc = DateTime.UtcNow; + + _jobs.TryRemove(job.DestinationDatabase, out _); + + var auditRec = token.IsCancellationRequested + ? CreateAuditRecord(job, $"ticket: {job.Ticket} email: {job.Email} Migration job ({job.Type.ToString()}) cancelled.") + : CreateAuditRecord(job, $"ticket: {job.Ticket} email: {job.Email} Migration job ({job.Type.ToString()}) complete."); + + await audit.Record(auditRec, token); + } + } + + private static AuditRecord CreateAuditRecord(MigrationJob job, string comment) + => new ( + job.DestinationDatabase, + DateTime.UtcNow, + job.Email, + job.Ticket, + job.Exception == null, + CreateAuditCommand(job, comment) + ); + + private static BsonDocument CreateAuditCommand(MigrationJob job, string comment) + { + return new () + { + ["dbMangoMigrate"] = job.ToString(), + ["type"] = job.Type.ToString(), + ["from"] = job.SourceDatabase, + ["to"] = job.DestinationDatabase, + ["upsert"] = job.Upsert, + ["clearDestBefore"] = job.ClearDestinationBefore, + ["disableIndexes"] = job.DisableIndexes, + ["batchSize"] = job.BatchSize, + ["ticket"] = job.Ticket, + ["collections"] = new BsonArray(job.Status.Select(x => new BsonDocument + { + ["sourceCollection"] = x.SourceCollection, + ["destinationCollection"] = x.DestinationCollection, + // ReSharper disable UseCollectionExpression + ["filter"] = x.Filter ?? new BsonDocument(), + ["projection"] = x.Projection ?? new BsonDocument(), + // ReSharper restore UseCollectionExpression + ["count"] = x.Count, + ["copied"] = x.Copied, + ["error"] = x.Error + })), + ["complete"] = job.Complete, + ["error"] = job.Error, + ["comment"] = comment + }; + } + + private async Task UploadCollectionsFromZip(MigrationJob job, CancellationToken token) + { + job.StartedAtUtc = DateTime.UtcNow; + try + { + await using var zipStream = new FileStream(job.UploadedFileName, FileMode.Open, FileAccess.Read); + + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); + var needToUpload = job.Status.Select( x => x.SourceCollection ).ToHashSet(StringComparer.OrdinalIgnoreCase); + + var groupedChunks = GroupAndChunkEntriesByFolder(archive.Entries, job.BatchSize, needToUpload); + + foreach ( var (coll,chunks) in groupedChunks ) + { + var collStatus = job.Status.FirstOrDefault(x => x.SourceCollection.Equals(coll, StringComparison.OrdinalIgnoreCase)); + if ( collStatus == null ) + continue; // no collection status found for this collection + + collStatus.Copied = 0; + collStatus.Count = chunks.Sum(x => x.Count); + collStatus.StartedAtUtc = DateTime.UtcNow; + + try + { + + var filter = collStatus.Filter == null + ? "{ _id : { $ne: \"\" } }" + : collStatus.Filter.ToString()!; + + if ( job.ClearDestinationBefore ) + { + await ClearDestination(job, collStatus, filter, token); + } + + var destination = _factory.Create( + job.DestinationDatabase, + collStatus.SourceCollection, + job.DestinationDatabaseInstance + ); + + + foreach (var chunk in chunks) + { + var documents = new List(); + + foreach (var entryName in chunk) + { + var entry = archive.GetEntry(entryName); + if (entry == null) + continue; + + await using var entryStream = entry.Open(); + using var reader = new StreamReader(entryStream); + var content = await reader.ReadToEndAsync(token); + + var bsonDocument = BsonDocument.Parse(content); + documents.Add(bsonDocument); + } + + if (documents.Count > 0) + { + var insertedCount = await destination.InsertAsync(documents, job.Upsert, true, token); + + // number of the documents inserted can be different if either using Upsert, inserting into non-cleared collection + // or using a filter(?) + lock (_lock) + { + collStatus.Copied += insertedCount; + } + } + } + } + catch (Exception ex) + { + job.Exception = ex; + } + + collStatus.FinishedAtUtc = DateTime.UtcNow; + collStatus.Complete = true; + } + } + catch (Exception ex) + { + job.Exception = ex; + } + job.FinishedAtUtc = DateTime.UtcNow; + job.Complete = true; + } + + private async Task DownloadCollections(MigrationJob job, CancellationToken token) + { + var zipFilePath = _storage.GetTempFileName( $"{job.JobId}_data.zip" ); + + await using var zipStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write); + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create); + + foreach (var collStatus in job.Status) + { + collStatus.Copied = 0; + collStatus.StartedAtUtc = DateTime.UtcNow; + + try + { + token.ThrowIfCancellationRequested(); + + var source = _factory.Create(job.SourceDatabase, collStatus.SourceCollection, job.SourceDatabaseInstance); + var filter = collStatus.Filter?.ToString() ?? "{ _id : { $ne: \"\" } }"; + + archive.CreateEntry($"{collStatus.SourceCollection}/"); + + var count = await source.CountAsync(filter, token); + lock (_lock) + { + collStatus.Count = count; + } + + var copied = 0; + + await foreach (var doc in source.FindAsync(filter, false, collStatus.Projection?.ToString(), limit: null, token)) + { + token.ThrowIfCancellationRequested(); + + var fileName = MakeFileName(doc["_id"]); + var fileEntry = archive.CreateEntry($"{collStatus.SourceCollection}/{fileName}"); + + await using var entryStream = fileEntry.Open(); + await using var writer = new StreamWriter(entryStream); + await writer.WriteAsync(doc.ToJson()); + + copied += 1; + if ( copied % 500 == 0 ) + { + lock( _lock) + { + collStatus.Copied += copied; + copied = 0; + } + } + } + + lock( _lock) + { + collStatus.Copied += copied; + } + } + catch (Exception ex) + { + collStatus.Exception = ex; + job.Exception = ex; + } + + collStatus.FinishedAtUtc = DateTime.UtcNow; + collStatus.Complete = true; + } + + job.FinishedAtUtc = DateTime.UtcNow; + job.Complete = true; + job.DownloadUrl = DownloadController.GetDownloadLink(_passwordManager, _singleUseTokenService, zipFilePath, "dbMango_data.zip"); + } + + private static long _counter; + + private static string MakeFileName(BsonValue bsonValue) + { + var fileName = (bsonValue.ToString() ?? "") + .Replace("..", ".") + .Replace("{", "") + .Replace("}", "") + .Replace("[", "") + .Replace("]", "") + .Replace("\"", "") + .Replace("'", "") + .Replace("?", "") + .Replace("*", "") + .Replace("\\", "") + .Replace("/", "") + .Replace(":", "") + ; + + if ( string.IsNullOrWhiteSpace(fileName) || fileName.Length > 100) + fileName = $"{Interlocked.Increment(ref _counter):D10}"; + + return $"{fileName}.json"; + } + + private async Task MigrateOneCollection(MigrationJob job, MigrationJob.CollectionJob collStatus, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + collStatus.Copied = 0; + collStatus.Complete = false; + collStatus.Exception = null; + + var filter = collStatus.Filter == null + ? "{ }" + : collStatus.Filter.ToString()!; + + var loadTask = LoadIds(job, collStatus, filter, token); + var clearTask = ClearDestination(job, collStatus, filter, token); + + await Task.WhenAll( loadTask, clearTask ); + + var indexes = await DisableIndexes(job, collStatus, token); + + try + { + var ids = loadTask.Result; + + // parallel inserts + + collStatus.StartedAtUtc = DateTime.UtcNow; // reset start time for correct DPS + + var options = new ParallelOptions + { + CancellationToken = token, + MaxDegreeOfParallelism = job.MaxDegreeOfParallelism + }; + + await Parallel.ForEachAsync( + ids.Chunk(job.BatchSize), + options, + async (x,t) => await ProcessBatch(x, job, collStatus, t) + ); + + } + catch (Exception ex) + { + collStatus.Exception = ex; + job.Exception = ex; + } + finally + { + if ( indexes.Length > 0 ) + { + // can't use token here as it can be already cancelled + // using very long timeout to ensure indexes are created + var cts3 = new CancellationTokenSource(TimeSpan.FromMinutes(60)); + await EnableIndexes(indexes, job, collStatus, cts3.Token); + } + } + collStatus.Complete = true; + + } + + private async Task DisableIndexes(MigrationJob job, MigrationJob.CollectionJob collStatus, CancellationToken token) + { + if ( !job.DisableIndexes) + return []; + + var config = MongoDbServiceFactory.GetConfig(_databases.Databases[job.DestinationDatabase].Config, job.DestinationDatabaseInstance); + IMongoDbDatabaseAdminService mongo = new MongoDbDatabaseAdminService(config, _settings.Value.Settings, job.DestinationDatabaseInstance); + + var indexes = (await DatabaseStructureLoader.LoadIndexes( + mongo, + collStatus.DestinationCollection, + token + )) + .Where(x => !IsPrimaryIndex(x) ) + .ToArray(); + + try + { + var command = new BsonDocument + { + ["dropIndexes"] = collStatus.DestinationCollection, + ["index"] = new BsonArray(indexes.Select(x => x.Name)) + }; + + await mongo.RunCommand(command, token); + } + catch (Exception ex) + { + collStatus.Exception = ex; + job.Exception = ex; + throw; + } + + return indexes; + } + + private async Task EnableIndexes(DatabaseStructureLoader.IndexStructure[] indexes, MigrationJob job, MigrationJob.CollectionJob collStatus, CancellationToken token) + { + if ( !job.DisableIndexes || indexes.Length == 0) + return; + + try + { + var config = MongoDbServiceFactory.GetConfig(_databases.Databases[job.DestinationDatabase].Config, job.DestinationDatabaseInstance); + var mongo = new MongoDbDatabaseAdminService(config, _settings.Value.Settings, job.DestinationDatabaseInstance); + + await DatabaseStructureLoader.CreateIndexes(mongo, collStatus.DestinationCollection, indexes, token); + } + catch (Exception ex) + { + collStatus.Exception = ex; + job.Exception = ex; + throw; + } + } + + private static bool IsPrimaryIndex(DatabaseStructureLoader.IndexStructure indexStructure) + { + if ( indexStructure.Name == "_id_" ) + return true; + if ( indexStructure.Key.Count == 1 && indexStructure.Key[0].Key == "_id" ) + return true; + + return false; + } + + private readonly Lock _lock = new(); + + private async Task> LoadIds( + MigrationJob job, + MigrationJob.CollectionJob collStatus, + string filter, + CancellationToken token + ) + { + var source = _factory.Create(job.SourceDatabase, collStatus.SourceCollection, job.SourceDatabaseInstance); + + var sw = Stopwatch.StartNew(); + var ids = await ExtractIDs(source, filter, token); + sw.Stop(); + _log.Debug($"Loading Count={ids.Count} IDs took Elapsed=\"{sw.Elapsed}\" DPS={1000.0 * ids.Count / sw.ElapsedMilliseconds:N0}"); + + lock (_lock) + collStatus.Count = ids.Count; + + return ids; + } + + private async Task ClearDestination( + MigrationJob job, + MigrationJob.CollectionJob collStatus, + string filter, + CancellationToken token + ) + { + if (!job.ClearDestinationBefore) + return; + + collStatus.Cleared = 0; + + var destCollection = string.IsNullOrWhiteSpace(collStatus.DestinationCollection) + ? collStatus.SourceCollection + : collStatus.DestinationCollection + ; + + var destination = _factory.Create(job.DestinationDatabase, destCollection, job.DestinationDatabaseInstance); + + collStatus.Cleared = await destination.Delete(filter, token); + } + + private async Task ProcessBatch( + BsonValue[] ids, + MigrationJob job, + MigrationJob.CollectionJob collStatus, + CancellationToken token + ) + { + var source = _factory.Create(job.SourceDatabase, collStatus.SourceCollection, job.SourceDatabaseInstance); + + var filter = new BsonDocument + { + ["_id"] = new BsonDocument() + { + ["$in"] = new BsonArray(ids) + } + }; + + var destCollection = string.IsNullOrWhiteSpace(collStatus.DestinationCollection) + ? collStatus.SourceCollection + : collStatus.DestinationCollection + ; + + var destination = _factory.Create(job.DestinationDatabase, destCollection, job.DestinationDatabaseInstance); + + var batch =new List(); + + var readSw = Stopwatch.StartNew(); + + await foreach (var doc in source + .FindAsync(filter, false, collStatus.Projection?.ToString(), limit: null, token) + ) + { + token.ThrowIfCancellationRequested(); + + batch.Add(doc); + } + + readSw.Stop(); + + if (batch.Count <= 0) + return; + + token.ThrowIfCancellationRequested(); + + var writeSw = Stopwatch.StartNew(); + + var c = await destination.InsertAsync(batch, job.Upsert, true, token: token); + + writeSw.Stop(); + + lock (_lock) + { + collStatus.Copied += c; + collStatus.TotalReadMSec += readSw.ElapsedMilliseconds; + collStatus.TotalWriteMSec += writeSw.ElapsedMilliseconds; + } + } + + private static async Task> ExtractIDs( + IMongoDbService source, + string filter, + CancellationToken token + ) + { + token.ThrowIfCancellationRequested(); + + var projection ="{ _id : 1 }"; + + var batch = new List(); + + await foreach (var doc in source + .FindAsync(filter, false, projection, limit: null, token) + ) + { + token.ThrowIfCancellationRequested(); + + var id = doc["_id"]; + batch.Add(id); + } + + return batch; + } + private Dictionary>> GroupAndChunkEntriesByFolder( + IEnumerable entries, int batchSize, HashSet needToUpload) + { + var groupedChunks = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in entries) + { + var folder = Path.GetDirectoryName(entry.FullName) ?? string.Empty; + folder = folder.TrimEnd('/').TrimEnd('\\'); + + if ( !needToUpload.Contains(folder) ) + continue; + + if (!groupedChunks.TryGetValue(folder, out var chunks)) + { + chunks = new(); + groupedChunks[folder] = chunks; + } + + var fileName = Path.GetFileName(entry.FullName); + if (string.IsNullOrWhiteSpace(fileName)) + continue; + + if (chunks.Count == 0 || chunks.Last().Count >= batchSize) + { + chunks.Add(new()); + } + + chunks.Last().Add(entry.FullName); + } + + return groupedChunks; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/MongoDbOnlyProxyDataSource.cs b/Rms.Risk.Mango/Services/MongoDbOnlyProxyDataSource.cs new file mode 100644 index 0000000..08ae6d4 --- /dev/null +++ b/Rms.Risk.Mango/Services/MongoDbOnlyProxyDataSource.cs @@ -0,0 +1,195 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.Models; +using Rms.Risk.Mango.Pivot.UI.Services; + +namespace Rms.Risk.Mango.Services; + +public class MongoDbOnlyProxyDataSource(IPivotTableDataSource mongoDb) : IPivotTableDataSource, IPivotTableDataSourceMetaProvider +{ + //private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + // public static int ConnectionAttempts = 3; + // public static int ConnectionAttemptDelaySec = 3; + + public string SourceId => MongoDb.SourceId; + public string Prefix => ""; + private IPivotTableDataSource MongoDb => mongoDb; + private IPivotTableDataSourceMetaProvider MongoDbMeta => (IPivotTableDataSourceMetaProvider)mongoDb; + + private IPivotTableDataSource ClickHouse + { + get; + } = new DummyDataSource(); + + private IPivotTableDataSourceMetaProvider ClickHouseMeta => (IPivotTableDataSourceMetaProvider)ClickHouse; + + public string User + { + get => MongoDb.User; + set + { + MongoDb.User = value; + ClickHouse.User = value; + } + } + + private class ParsedCollectionName + { + public bool IsClickHouse { get; init; } + public required string Name { get; init; } + } + + private static ParsedCollectionName ParseCollectionName(string collectionName) + { + var s = collectionName.Split(":"); + if (s.Length == 1) + s = ["Forge", s[0].Trim()]; + if (s.Length != 2) + throw new ApplicationException($"Invalid CollectionName=\"{collectionName}\""); + + return new() + { + IsClickHouse = s[0].Equals("BFG", StringComparison.OrdinalIgnoreCase), + Name = s[1].Trim() + }; + } + + private bool IsClickHouse { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string CollectionName { get; set; } = ""; + + + public Task> GetAllMeta(bool force = false, CancellationToken token = default) => MongoDb.GetAllMeta(force, token); + + public async Task GetCollectionsAsync(CollectionType includeMeta = CollectionType.All, CancellationToken token = default) + { + var tasks = new[] {MongoDbMeta.GetCollectionsAsync(includeMeta, token), ClickHouseMeta.GetCollectionsAsync(includeMeta, token)}; + await Task.WhenAll(tasks); + var mongoDbCollections = tasks[0].Result; + var clickHouseCollections = tasks[1].Result; + + return mongoDbCollections.Select(x => $"Forge: {x}") + .Concat(clickHouseCollections.Select(x => $"BFG: {x}")) + .ToArray(); + } + + public Task GetCobDatesAsync(string collectionName, bool force = false, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetCobDatesAsync(coll.Name, force, token); + } + + public Task GetDepartmentsAsync(string collectionName, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetDepartmentsAsync(coll.Name, token); + } + + public Task<(string, string)[]> GetDesksWithDepartmentAsync(string collectionName, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetDesksWithDepartmentAsync(coll.Name, token); + } + + public Task GetKeyFieldsAsync(string collectionName, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetKeyFieldsAsync(coll.Name, token); + } + + public Task GetDrilldownKeyFieldsAsync(string collectionName, PivotFieldPurpose keyLevel, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetDrilldownKeyFieldsAsync(coll.Name, keyLevel, token); + } + + public Task GetDataFieldsAsync(string collectionName, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetDataFieldsAsync(coll.Name, token); + } + + public Task GetColumnDescriptorsAsync(string collectionName, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetColumnDescriptorsAsync(coll.Name, token); + } + + public Dictionary GetFieldTypes(string collectionName) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetFieldTypes(coll.Name); + } + + public Task GetDrilldownAsync(string collectionName, string name, string value = "\"\"", bool equals = false, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).GetDrilldownAsync(coll.Name, name, value, equals, token); + } + + public Task PivotAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, + bool skipCache, string? userName = null, int maxFetchSize = -1, + CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).PivotAsync(coll.Name, def, extraFilter, skipCache, userName,maxFetchSize, token); + } + + public Task> GetPivotsAsync(string collectionName, IPivotTableDataSource.PivotType pivotType, + string? userName = null, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouseMeta : MongoDbMeta).GetPivotsAsync(coll.Name, pivotType, userName, token); + } + + public Task UpdatePredefinedPivotsAsync(string collectionName, IEnumerable pivots, bool predefined = false, + string? userName = null, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).UpdatePredefinedPivotsAsync(coll.Name, pivots, predefined, userName, token); + } + + public Task GetQueryTextAsync(string collectionName, PivotDefinition def, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).GetQueryTextAsync(coll.Name, def, extraFilter, token); + } + + public Task GetDocumentAsync(string collectionName, KeyValuePair[] keys, FilterExpressionTree.ExpressionGroup? extraFilter, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).GetDocumentAsync(coll.Name, keys, extraFilter, token); + } + + public Task GetDocumentAsync(string collectionName, FilterExpressionTree.ExpressionGroup extraFilter, CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).GetDocumentAsync(coll.Name, extraFilter, token); + } + + public Task DeletePivotAsync(string collectionName, string pivotName, string groupName, string userName, + CancellationToken token = default) + { + var coll = ParseCollectionName(collectionName); + return (coll.IsClickHouse ? ClickHouse : MongoDb).DeletePivotAsync(coll.Name, pivotName, groupName, userName, token); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/MongoDbServiceFactory.cs b/Rms.Risk.Mango/Services/MongoDbServiceFactory.cs new file mode 100644 index 0000000..a9099b3 --- /dev/null +++ b/Rms.Risk.Mango/Services/MongoDbServiceFactory.cs @@ -0,0 +1,143 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.Models; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Rms.Risk.Mango.Services.Audit; +using Rms.Risk.Mango.Services.Context; + +namespace Rms.Risk.Mango.Services; + +public class MongoDbServiceFactory : IMongoDbServiceFactory +{ + private const int SessionExpirationMinutes = 30; + private const int SessionExpirationCheckMinutes = 5; + + private record AuditServiceKey(string AuditDatabase, string DatabaseInstance); + private record PivotServiceKey(string Database, string DatabaseInstance); + private record AdminServiceKey (string Database, string DatabaseInstance, IUserSession Session); + + private readonly IDatabaseConfigurationService _databases; + private readonly IOptions _settings; + private IAuditService? _inOracle; + + private readonly ExpiringConcurrentDictionary _auditServices; + private readonly ExpiringConcurrentDictionary _pivotServices; + private readonly ExpiringConcurrentDictionary _adminServices; + private readonly IDbMangoPlugin? _dbMangoPlugin; + + public MongoDbServiceFactory(IDatabaseConfigurationService databases, IOptions settings, IDbMangoPlugin? dbMangoPlugin = null) + { + _databases = databases; + _settings = settings; + _dbMangoPlugin = dbMangoPlugin; + + _auditServices = new ( + InternalCreateAudit, + TimeSpan.FromMinutes(SessionExpirationMinutes), + TimeSpan.FromMinutes(SessionExpirationCheckMinutes), + false + ); + _adminServices = new ( + InternalCreateAdmin, + TimeSpan.FromMinutes(SessionExpirationMinutes), + TimeSpan.FromMinutes(SessionExpirationCheckMinutes), + false + ); + _pivotServices = new ( + InternalCreatePivot, + TimeSpan.FromMinutes(SessionExpirationMinutes), + TimeSpan.FromMinutes(SessionExpirationCheckMinutes), + false + ); + } + + public static MongoDbConfigRecord GetConfig(MongoDbConfigRecord config, string databaseInstance) + { + if (string.IsNullOrWhiteSpace(config.MongoDbDatabase) && string.IsNullOrWhiteSpace(databaseInstance)) + throw new ApplicationException("MongoDB database name is not provided and it's not set in the configuration either."); + + if (!string.IsNullOrWhiteSpace(databaseInstance)) + { + var copy = config.Clone(); + copy.MongoDbDatabase = databaseInstance; + return copy; + } + + return config; + } + + public IPivotTableDataSource CreatePivot(string database, string databaseInstance) + => _pivotServices.GetOrAdd(new(database,databaseInstance)); + + public IMongoDbDatabaseAdminService CreateAdmin(string database, IUserSession session, string databaseInstance) + => _adminServices.GetOrAdd(new(database, databaseInstance, session)); + + public IMongoDbService Create(string database, string collection, string databaseInstance) + => new BsonMongoDbService(GetConfig(_databases.Databases[database].Config, databaseInstance), _settings.Value.Settings, collection, databaseInstance); + + public IMongoDbDatabaseAdminService CreateAdmin(MongoDbConfigRecord config, string auditDatabase, IUserSession session, string databaseInstance) + => new AuditedMongoDbDatabaseAdminService( + config, + _settings.Value.Settings, + session, + CreateAudit(auditDatabase, string.IsNullOrWhiteSpace(config.MongoDbDatabase) ? databaseInstance : config.MongoDbDatabase), databaseInstance); + + public IAuditService CreateAudit(string auditDatabase, string databaseInstance) + => _auditServices.GetOrAdd(new(auditDatabase,databaseInstance)); + +#region Internal Methods + + private IPivotTableDataSource InternalCreatePivot(PivotServiceKey key) => + new MongoDbOnlyProxyDataSource(new MongoDbDataSource( + GetConfig(_databases.Databases[key.Database].Config, key.DatabaseInstance), + _settings.Value.Settings, + key.DatabaseInstance + )); + + private IMongoDbDatabaseAdminService InternalCreateAdmin(AdminServiceKey key) + => new AuditedMongoDbDatabaseAdminService( + GetConfig(_databases.Databases[key.Database].Config, key.DatabaseInstance), + _settings.Value.Settings, + key.Session, + CreateAudit(key.Database, key.DatabaseInstance), key.DatabaseInstance); + + private IAuditService InternalCreateAudit(AuditServiceKey key) + { + + var inMongo = new AuditService( + GetConfig(_databases.Databases[key.AuditDatabase].Config, key.DatabaseInstance), + _settings.Value.Settings, + _settings.Value.AuditExpireDays + ); + + _inOracle ??= _settings.Value.AuditLogsInOracle && _dbMangoPlugin != null + ? _dbMangoPlugin.CreateSecureAuditService(_settings.Value.OracleConnectionSettings) + : null; + + return _inOracle == null + ? inMongo + : new ChainedAuditService([_inOracle, inMongo]); + } + +#endregion +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/NoChangeNumberChecker.cs b/Rms.Risk.Mango/Services/NoChangeNumberChecker.cs new file mode 100644 index 0000000..c436a58 --- /dev/null +++ b/Rms.Risk.Mango/Services/NoChangeNumberChecker.cs @@ -0,0 +1,27 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Rms.Risk.Mango.Interfaces; + +namespace Rms.Risk.Mango.Services; + +public class NoChangeNumberChecker : IChangeNumberChecker +{ + public Task IsValid(string taskNumber, string email, DateTime whenTimeUtc = default) + => Task.FromResult(new CheckerReply(true, ValidFromUtc : DateTime.UtcNow, ValidToUtc: DateTime.MaxValue)); +} diff --git a/Rms.Risk.Mango/Services/PivotSharingService.cs b/Rms.Risk.Mango/Services/PivotSharingService.cs new file mode 100644 index 0000000..1f9f068 --- /dev/null +++ b/Rms.Risk.Mango/Services/PivotSharingService.cs @@ -0,0 +1,83 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using Rms.Risk.Mango.Controllers; +using Rms.Risk.Mango.Pivot.UI.Services; +using Rms.Service.Bootstrap.Security; +using System.Reflection; + +namespace Rms.Risk.Mango.Services; + +public class PivotSharingService( + ITempFileStorage tempFileStorage, + IPasswordManager passwordManager, + ISingleUseTokenService singleUseTokenService + ) : IPivotSharingService +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + public async Task ExportToCsv(string destFileName, string fileContents) + { + var url = await DownloadController.GetDownloadLink( + tempFileStorage, + passwordManager, + singleUseTokenService, + async fileName => await File.WriteAllTextAsync(fileName, fileContents), + destFileName + ); + + return url; + } + + public async Task SharePivot(SharedPivotDef def) + { + // Serialize SharedPivotDef to JSON using Newtonsoft.Json + var json = Newtonsoft.Json.JsonConvert.SerializeObject(def); + + var guid = Guid.NewGuid().ToString("D"); + + var fileName = Path.Combine(tempFileStorage.TempFolder, guid + ".json"); + await File.WriteAllTextAsync(fileName, json); + + return guid; + } + + public async Task GetSharedPivot(string guid) + { + + var fileName = Path.Combine(tempFileStorage.TempFolder, FileUtils.Shield(guid).Replace("..", ".") + ".json"); + if (!File.Exists(fileName)) + return null; + + var json = await File.ReadAllTextAsync(fileName); + + try + { + // Deserialize JSON to SharedPivotDef using Newtonsoft.Json + var def = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + return def; + } + catch (Exception ex) + { + // Log the error if needed + _log.Error($"Error deserializing SharedPivotDef: {ex.Message}"); + return null; + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/RstToMarkdownConverter.cs b/Rms.Risk.Mango/Services/RstToMarkdownConverter.cs new file mode 100644 index 0000000..19b8a22 --- /dev/null +++ b/Rms.Risk.Mango/Services/RstToMarkdownConverter.cs @@ -0,0 +1,264 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Text.RegularExpressions; + +namespace Rms.Risk.Mango.Services; + +/// +/// Converts ReStructuredText (RST) to Markdown (MD). +/// Unknown tags are omitted during conversion. +/// +public class RstToMarkdownConverter +{ + + private static readonly Dictionary _conversionRules = new () + { + { new (@"([^\*]|^)\*([^\*]+)\*([^\*]|$)"), "_$2_" }, // Italic text + { new (@"\*\*([^\*]+)\*\*" ), "**$1**" }, // Bold text + { new (@"^\.\.\s+code-block::\s*(\w+)" ), "```$1" }, // Code block start + { new (@"^\.\.\s+collflag::\s*(\w+)" ), "\n**$1**\n" }, // ? + { new (@"^\.\.\s+data::\s*(.+)" ), " * $1\n\n" }, // Data annotation + { new (@":\w+:`([^`]+)`" ), "$1" }, // Replace :tag:`word` with `word` + + { new (@"&" ), "&" }, + { new (@"<" ), "<" }, + { new (@">" ), ">" }, + + { new (@"\s*:local:.*" ), "" }, + { new (@"\s*:backlinks:\s*\w+.*" ), "" }, + { new (@"\s*:depth:\s*[0-9]+.*" ), "" }, + { new (@"\s*:class:\s*\w+.*" ), "" }, + }; + + private static readonly Regex _h1 = new(@"^=+\s*$"); + private static readonly Regex _h2 = new(@"^-+\s*$"); + private static readonly Regex _h3 = new(@"^\~+\s*$"); + private static readonly Regex _copyable = new (@"\s*:copyable:\s*(true|false).*" ); + + /// + /// Converts RST content to Markdown. + /// + /// The RST content to convert. + /// The converted Markdown content. + public string Convert(string[] lines) + { + if (lines.Length == 0) + return string.Empty; + + // Conversion rules + var markdownLines = new List(); + var insideCodeBlock = false; + var insideTable = false; + var tableRows = new List(); + + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var convertedLine = line; + + if (i == 0 && _h1.IsMatch(line)) + continue; + + convertedLine = ConvertLine(convertedLine); + + if (insideCodeBlock) + { + if ( _copyable.IsMatch(line) ) + continue; + + if (!line.StartsWith(" ") && !string.IsNullOrWhiteSpace(line)) + { + markdownLines.Add("```"); + markdownLines.Add(""); + insideCodeBlock = false; + // continue processing this line + } + else + { + markdownLines.Add(line); + continue; + } + } + + // Handle tables + if (line.TrimStart().StartsWith(".. list-table::")) + { + insideTable = true; + tableRows.Clear(); + continue; + } + + if (convertedLine.StartsWith("```")) + { + markdownLines.Add(convertedLine); + insideCodeBlock = true; + continue; + } + + if (insideTable) + { + if (!line.StartsWith(" ") && !string.IsNullOrWhiteSpace(line)) + { + // End of table + if (tableRows.Count > 0) + markdownLines.Add(ConvertTableToMarkdownList(tableRows)); + + insideTable = false; + // continue processing this line + } + else + { + tableRows.Add(line); + continue; + } + } + + // Handle headers underlined with "=", "-", "~" + if (i < lines.Length - 1) + { + var nextLine = lines[i + 1]; + if (_h1.IsMatch(nextLine)) + { + markdownLines.Add($"# {line}"); + i++; + continue; + } + + if (_h2.IsMatch(nextLine)) + { + markdownLines.Add($"## {line}"); + i++; + continue; + } + + if (_h3.IsMatch(nextLine)) + { + markdownLines.Add($"### {line}"); + i++; + continue; + } + } + + // Skip unknown tags and ".. include::" lines + if (convertedLine.TrimStart().StartsWith("..")) + continue; + + if (convertedLine.StartsWith(" ") && convertedLine.Length > 2 && convertedLine[2] != ' ') + markdownLines.Add(convertedLine[2..]); + else + markdownLines.Add(convertedLine); + } + + if (insideCodeBlock) + { + markdownLines.Add("```"); + } + + return string.Join(Environment.NewLine, markdownLines); + } + + private static string ConvertLine(string convertedLine) + { + foreach (var rule in _conversionRules) + { + convertedLine = rule.Key.Replace(convertedLine, rule.Value); + } + + return convertedLine; + } + + private static string ConvertTableToMarkdownList(List tableRows) + { + var converted = new List(); + // Stack to keep track of the current position of "-" character + var tabs = new Stack(); + + var level = 0; + + // current position of "-" character in the row + var startPos = 0; + + foreach (var row in tableRows) + { + if ( row.TrimStart().StartsWith('*') ) + { + var pos = row.IndexOf('*', StringComparison.Ordinal); + if ( pos > 0 ) + { + level = 1; + startPos = pos; + tabs.Clear(); + tabs.Push(startPos); + + pos = row.IndexOf('-', StringComparison.Ordinal); + converted.Add(row[pos..]); + } + + continue; + } + + if ( level == 0 ) + continue; + + // Converting logic for list items: + // list level starts with N*2 spaces and "-" character + // if line is empty or whitespace, add an empty line to the output + // if row[pos] != '-' and row[..pos] == spaces, continue the current list item (add a new line to the converted list starting with N*2+2 spaces) + + if (string.IsNullOrWhiteSpace(row)) + { + converted.Add(string.Empty); + continue; + } + + // if !string.IsNullOrWhiteSpace(row[..pos]) end current list item, decrease level (level -= 1; pos -= 2) and recheck the line with new values (i.e. here must be the inner loop) + while ( !string.IsNullOrWhiteSpace(row[..Math.Min(row.Length, startPos)]) && level > 0 ) + { + level -= 1; + startPos = tabs.Pop(); + } + + // this should not ever happen because of the previous check at the caller level + if ( level <= 0 ) + break; + + // if row[pos] == '-' continue current level and start a new item + if ( row.Length > startPos && row[startPos] == '-') + { + converted.Add(new string(' ', (level-1) * 2) + "- " + row[(startPos+1)..].Trim()); + continue; + } + + // if row[pos..].Trim().StartsWith( '-' ) start a new level and item + if ( row.Length > startPos && row[startPos..].Trim().StartsWith( '-' ) ) + { + level += 1; + startPos = row.IndexOf('-', StringComparison.Ordinal); + tabs.Push(startPos); + + converted.Add(new string(' ', (level-1) * 2) + "- " + row[(startPos+1)..].Trim()); + continue; + } + + converted.Add(new string(' ', level * 2) + row[(startPos+1)..].Trim()); + } + + return string.Join(Environment.NewLine, converted.Select(ConvertLine)); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/Security/DatabaseAccessAuthorizationHandler.cs b/Rms.Risk.Mango/Services/Security/DatabaseAccessAuthorizationHandler.cs new file mode 100644 index 0000000..b3505b3 --- /dev/null +++ b/Rms.Risk.Mango/Services/Security/DatabaseAccessAuthorizationHandler.cs @@ -0,0 +1,191 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Novell.Directory.Ldap; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Services.Context; +using Rms.Service.Bootstrap.Security; +// ReSharper disable InconsistentNaming + +namespace Rms.Risk.Mango.Services.Security; + +public class AdminAccessRequirement : IAuthorizationRequirement; +public class ReadAccessRequirement : IAuthorizationRequirement; +public class WriteAccessRequirement : IAuthorizationRequirement; + +public class AdminAuthorizationHandler(IDatabaseConfigurationService _databases, LdapChecker _ldap) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + AdminAccessRequirement requirement, + string /*database name*/ resource) + { + using var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + var error = await HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.Admin, + cts.Token + ); + + if (error == null) + context.Succeed(requirement); + else + context.Fail(new(this, error)); + } + + + public static async Task HandleRequirement( + IDatabaseConfigurationService config, + LdapChecker ldap, + ClaimsPrincipal user, + string database, + Func getAcl, + CancellationToken token) + { + if (string.IsNullOrWhiteSpace(database)) + return null; + + if (!config.Databases.TryGetValue(database, out var conf)) + return $"Database {database} is not configured"; + + var requiredGroup = getAcl(conf.Groups); + + if (string.IsNullOrWhiteSpace(requiredGroup)) + return null; + + try + { + var success = await ldap.IsGroupMember(UserService.GetEmail(user), requiredGroup, token); + + return success + ? null + : $"To access database {database} user should be a member of the group: {requiredGroup}" + ; + } + catch (LdapException ex) + { + return $"To access database {database} user should be a member of the group: {requiredGroup}. Exception: {ex.LdapErrorMessage}"; + } + catch (Exception ex) + { + return $"To access database {database} user should be a member of the group: {requiredGroup}. Exception: {ex.Message}"; + } + } +} + +public class ReadWriteAuthorizationHandler(IDatabaseConfigurationService _databases, LdapChecker _ldap) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + WriteAccessRequirement requirement, + string /*database name*/ resource) + { + using var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + var error = await AdminAuthorizationHandler.HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.Admin, + cts.Token + ); + + if (error == null) + { + context.Succeed(requirement); + return; + } + + error = await AdminAuthorizationHandler.HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.ReadWrite, + cts.Token + ); + + if (error == null) + context.Succeed(requirement); + else + context.Fail(new(this, error)); + } +} + +public class ReadOnlyAuthorizationHandler(IDatabaseConfigurationService _databases, LdapChecker _ldap) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ReadAccessRequirement requirement, + string /*database name*/ resource) + { + using var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); + + var error = await AdminAuthorizationHandler.HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.ReadWrite, + cts.Token); + + if (error == null) + { + context.Succeed(requirement); + return; + } + + error = await AdminAuthorizationHandler.HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.Admin, + cts.Token + ); + + if (error == null) + { + context.Succeed(requirement); + return; + } + + error = await AdminAuthorizationHandler.HandleRequirement( + _databases, + _ldap, + context.User, + resource, + x => x.ReadOnly, + cts.Token + ); + + if (error == null) + context.Succeed(requirement); + else + context.Fail(new(this, error)); + } +} + diff --git a/Rms.Risk.Mango/Services/Security/DatabaseAccessPolicyExtensions.cs b/Rms.Risk.Mango/Services/Security/DatabaseAccessPolicyExtensions.cs new file mode 100644 index 0000000..2840995 --- /dev/null +++ b/Rms.Risk.Mango/Services/Security/DatabaseAccessPolicyExtensions.cs @@ -0,0 +1,45 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Authorization; + +namespace Rms.Risk.Mango.Services.Security; + +public static class DatabaseAccessPolicyExtensions +{ + public const string AdminAccessPolicy = "AdminAccess"; + public const string ReadAccessPolicy = "ReadAccess"; + public const string WriteAccessPolicy = "WriteAccess"; + + public static IServiceCollection AddMongoDbAccess(this IServiceCollection services) + { + services.AddAuthorization(options => + { + options.AddPolicy(AdminAccessPolicy, policy => policy.Requirements.Add(new AdminAccessRequirement())); + options.AddPolicy(ReadAccessPolicy, policy => policy.Requirements.Add(new ReadAccessRequirement())); + options.AddPolicy(WriteAccessPolicy, policy => policy.Requirements.Add(new WriteAccessRequirement())); + }) + .AddSingleton() + .AddSingleton() + .AddSingleton() + ; + + return services; + } + +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/SingleUseTokenService.cs b/Rms.Risk.Mango/Services/SingleUseTokenService.cs new file mode 100644 index 0000000..084167f --- /dev/null +++ b/Rms.Risk.Mango/Services/SingleUseTokenService.cs @@ -0,0 +1,71 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +public class SingleUseTokenService : ISingleUseTokenService +{ + public static TimeSpan TokenValidityInterval = TimeSpan.FromSeconds( 30 ); + + private readonly Lock _syncObject = new(); + private readonly List> _tokens = []; + + public string GetSingleUseToken() + { + var guid = Guid.NewGuid().ToString().Replace( "-", "" ); + lock ( _syncObject ) + { + _tokens.Add( Tuple.Create( DateTime.Now + TokenValidityInterval, guid ) ); + } + + return guid; + } + + public bool CheckSingleUseToken( string token ) + { + lock ( _syncObject ) + { + var toDelete = new List( _tokens.Count ); + var valid = false; + var now = DateTime.Now; + for ( var i = 0; i < _tokens.Count; i++ ) + { + var (expireAt, guid) = _tokens[i]; + if ( expireAt >= now ) + { + if ( !valid && guid == token ) + { + valid = true; + // token is one-off + toDelete.Add( i ); + } + } + else + { + // token expired + toDelete.Add( i ); + } + } + + for ( var i = toDelete.Count-1; i >= 0; i-- ) + _tokens.RemoveAt( i ); + + return valid; + } + } +} diff --git a/Rms.Risk.Mango/Services/TempFileStorage.cs b/Rms.Risk.Mango/Services/TempFileStorage.cs new file mode 100644 index 0000000..c1ba903 --- /dev/null +++ b/Rms.Risk.Mango/Services/TempFileStorage.cs @@ -0,0 +1,149 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using log4net; +using System.Diagnostics; +using System.Reflection; + +namespace Rms.Risk.Mango.Services; + +public class TempFileStorage : ITempFileStorage, IDisposable +{ + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); + + private const int TtlHours = 24; + private const string TempFolderPrefix = "dbMango-"; + + private static int _counter; + + + public TempFileStorage( string? useThisFolder = null ) + { + LocalPersistentFolder = Path.Combine( + Environment.GetEnvironmentVariable("RMS_RISKSTORE") + ?? Environment.GetEnvironmentVariable("TEMP") + ?? Environment.GetEnvironmentVariable("TMP") + ?? Path.GetTempPath() + , "dbMango" + ); + + var tempFolderBase = useThisFolder; + + if (tempFolderBase != null) + { + if (!Directory.Exists(tempFolderBase)) + Directory.CreateDirectory(tempFolderBase); + _log.Debug($"Using specific temporary Folder=\"{tempFolderBase}\" base"); + } + + if (tempFolderBase == null || !Directory.Exists(tempFolderBase)) + { + tempFolderBase = TempFolderHelper.GetTempFolder(); + _log.Debug($"Using default temporary Folder=\"{tempFolderBase}\" base"); + } + + ClearOutdatedFiles(tempFolderBase, TimeSpan.FromHours(TtlHours)); + + var counter = Interlocked.Increment(ref _counter); + var name = $"{TempFolderPrefix}{DateTime.Now.Date:yyyy-MM-dd}-{counter}-{Process.GetCurrentProcess().Id}"; + + for ( var i = 0; i < 100; i++ ) + { + var tempName = (i == 0 ? name : name + "(" + i + ")")+".temp"; + + if ( Directory.Exists( Path.Combine( tempFolderBase, tempName ) ) ) + continue; + + TempFolder = Path.Combine( tempFolderBase, tempName ); + Directory.CreateDirectory( TempFolder ); + break; + } + + if (TempFolder == null) + throw new ApplicationException($"Failed to create temporary folder in {tempFolderBase}"); + + _log.Debug( $"Using temporary Folder=\"{TempFolder}\"" ); + } + + ~TempFileStorage() + { + Dispose( false ); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + +// ReSharper disable once UnusedParameter.Local + private void Dispose(bool disposing) + { + if ( Directory.Exists( TempFolder ) ) + { + SafeDeleteFolder( TempFolder ); + } + } + + private static void ClearOutdatedFiles(string folder, TimeSpan timeToLive) + { + var now = DateTime.UtcNow; + + var allFolders = Directory.EnumerateDirectories(folder); + var myTempFolders = allFolders + .Where(x => Path.GetFileName(x).StartsWith(TempFolderPrefix, StringComparison.OrdinalIgnoreCase) && x.EndsWith(".temp", StringComparison.OrdinalIgnoreCase)) + ; + var outdatedFolders = myTempFolders + .Where( name => now - File.GetCreationTimeUtc(name) > timeToLive ) + ; + + foreach ( var name in outdatedFolders ) + { + SafeDeleteFolder(name); + } + } + + private static void SafeDeleteFolder(string name) + { + _log.Debug( $"Deleting temporary Folder=\"{name}\"" ); + var sw = Stopwatch.StartNew(); + + Directory.Delete(name); + _log.Debug( $"Temporary Folder=\"{name}\" deleted. Elapsed=\"{sw.Elapsed:g}\"" ); + } + + + public string TempFolder { get; } + public string LocalPersistentFolder { get; } + + public string GetTempFileName( Type t ) => GetTempFileName( t.Name ); + + public string GetTempFileName() => GetTempFileName(typeof(T)); + + public string GetTempFileName(string key) + { + var counter = Interlocked.Increment(ref _counter); + + if (!Directory.Exists(TempFolder)) + Directory.CreateDirectory(TempFolder); + + var name = Path.Combine( TempFolder, $"{DateTime.Now:hh-mm-ss}-{FileUtils.Shield(key)}-{counter}.tmp"); + _log.Debug( $"Temporary file name obtained. FileName=\"{name}\"" ); + return name; + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/TempFolderHelper.cs b/Rms.Risk.Mango/Services/TempFolderHelper.cs new file mode 100644 index 0000000..f63030b --- /dev/null +++ b/Rms.Risk.Mango/Services/TempFolderHelper.cs @@ -0,0 +1,81 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Rms.Risk.Mango.Services; + +public static class TempFolderHelper +{ + private static string? _tempFolder; + private static readonly Lock _syncObject = new(); + + /// + /// Unfortunately on DB application servers TEMP folder located on very small C: drives. + /// So we can't use Path.GetTempPath(). We are using %RMS_RISKSTORE% instead. + /// + /// Temp folder path on the large disk + public static string GetTempFolder() + { + var path = _tempFolder; + if (path != null) + return path; + + lock (_syncObject) + { + if (_tempFolder != null) + return _tempFolder; + + if ( path == null || !Directory.Exists( path ) ) + { + path = Environment.GetEnvironmentVariable("RMS_RISKSTORE"); //appserver = D:\data\Rms_RiskStore + if (path != null && Directory.Exists(path)) + { + path = Path.Combine(path, "Forge", "Temp"); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + } + } + if ( path == null || !Directory.Exists( path ) ) + path = Path.GetTempPath(); + if ( !Directory.Exists( path ) ) + throw new ApplicationException("Can't get temporary folder"); + _tempFolder = path; + } + + return _tempFolder; + } + + private static long _counter; + + public static string GetTempFileName(string key = "tmp") + { + var folder = GetTempFolder(); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder!); + + for (var i = 0; i < 100_000; i++) + { + var counter = Interlocked.Increment(ref _counter); + + var name = Path.Combine(folder, $"{DateTime.Now:hh-mm-ss}-{FileUtils.Shield(key)}-{counter}.tmp"); + if ( !File.Exists(name)) + return name; + } + + throw new ApplicationException($"Can't obtain temp file name in Folder=\"{folder}\" Key=\"{key}\". Too many temp files in the folder."); + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Services/UserServiceProxy.cs b/Rms.Risk.Mango/Services/UserServiceProxy.cs new file mode 100644 index 0000000..d113615 --- /dev/null +++ b/Rms.Risk.Mango/Services/UserServiceProxy.cs @@ -0,0 +1,44 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Security.Claims; +using Rms.Risk.Mango.Pivot.UI.Services; +using Rms.Service.Bootstrap.Security; + +namespace Rms.Risk.Mango.Services +{ + public class UserServiceProxy(UserService _userService) : IUserService + { + public ClaimsPrincipal GetUser() + { + return _userService.GetUser(); + } + + public bool IsAuthenticated => _userService.IsAuthenticated; + + public string GetEmail() + { + return _userService.GetEmail(); + } + + public string? Get(string claimType) + { + return _userService.Get(claimType); + } + } +} diff --git a/Rms.Risk.Mango/Services/UserSession.cs b/Rms.Risk.Mango/Services/UserSession.cs new file mode 100644 index 0000000..1e2ad92 --- /dev/null +++ b/Rms.Risk.Mango/Services/UserSession.cs @@ -0,0 +1,295 @@ +/* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Rms.Risk.Mango.Interfaces; +using Rms.Risk.Mango.Pivot.Core; +using Rms.Risk.Mango.Pivot.Core.MongoDb; +using Rms.Risk.Mango.Services.Context; +using Rms.Risk.Mango.Services.Security; +using Rms.Service.Bootstrap.Security; + +namespace Rms.Risk.Mango.Services; + +internal class UserSession : IUserSession +{ + public UserService User => _user; + + public string? TaskNumber { get; set; } + public string? TaskCheckError { get; private set; } + + private CheckerReply? _checkReply; + private readonly UserService _user; + private readonly IMongoDbServiceFactory _mongoDbServiceFactory; + private readonly IChangeNumberChecker _changeNumberChecker; + private readonly IDatabaseConfigurationService _databases; + + public UserSession(IOptions settings, + UserService user, + IMongoDbServiceFactory mongoDbServiceFactory, + IChangeNumberChecker changeNumberChecker, + IDatabaseConfigurationService databases) + { + _user = user; + _mongoDbServiceFactory = mongoDbServiceFactory; + _changeNumberChecker = changeNumberChecker; + _databases = databases; + + Database = settings.Value.Initial; + DatabaseInstance = _databases.Databases[settings.Value.Initial].Config.MongoDbDatabase; + } + + public override bool Equals(object? obj) + { + if (obj is not IUserSession other) + return false; + + return string.Equals(_user.GetEmail(), other.User.GetEmail(), StringComparison.Ordinal) && + string.Equals(Database, other.Database, StringComparison.Ordinal) && + string.Equals(DatabaseInstance, other.DatabaseInstance, StringComparison.Ordinal) && + string.Equals(TaskNumber, other.TaskNumber, StringComparison.Ordinal); + } + + // ReSharper disable NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(_user.GetEmail(), Database, DatabaseInstance, TaskNumber); + // ReSharper restore NonReadonlyMemberInGetHashCode + + public async Task HasValidTask() + { + TaskNumber ??= "ITSK0000000000"; + TaskCheckError = null; + + if (string.IsNullOrWhiteSpace(TaskNumber) || string.IsNullOrWhiteSpace(User.GetEmail())) + { + TaskCheckError = "Task number or user email are not set."; + return false; + } + + var now = DateTime.UtcNow; + + if (_checkReply == null) + { + _checkReply = await _changeNumberChecker.IsValid(TaskNumber, User.GetEmail(), now); + if (!_checkReply.IsValid) + { + TaskCheckError = _checkReply.ErrorMessage; + TaskNumber = null; + _checkReply = null; + return false; + } + TaskCheckError = null; + } + + if ( _checkReply == null ) + { + TaskCheckError = "Task check reply is null."; + return false; + } + + var isOpen = DateTime.UtcNow > _checkReply.ValidFromUtc + && DateTime.UtcNow < _checkReply.ValidToUtc; + + if ( !isOpen) + { + TaskCheckError = $"Task {TaskNumber} is valid, but implementation window is not open (Start: {_checkReply.ValidFromUtc}, End: {_checkReply.ValidToUtc} UTC)"; + return false; + } + + return true; + } + + public async Task CanAccess(IAuthorizationService auth, string policyName, string databaseName) + { + var res = await auth.AuthorizeAsync(User.GetUser(), databaseName, + [ + policyName switch + { + DatabaseAccessPolicyExtensions.ReadAccessPolicy => new ReadAccessRequirement(), + DatabaseAccessPolicyExtensions.WriteAccessPolicy => new WriteAccessRequirement(), + DatabaseAccessPolicyExtensions.AdminAccessPolicy => new AdminAccessRequirement(), + _ => throw new ($"Policy name \"{policyName}\" is invalid. Expecting {DatabaseAccessPolicyExtensions.ReadAccessPolicy}, {DatabaseAccessPolicyExtensions.WriteAccessPolicy}, or {DatabaseAccessPolicyExtensions.AdminAccessPolicy}") + } + ]); + + return res.Succeeded; + } + + private Dictionary Databases => _databases.Databases; + + public string Database + { + get; + set + { + if (field == value) + return; + + Clear(); + field = value; + + var config = GetDatabaseConfig(Database); + + // this is to make sure that DatabaseChanged is only called once + var newInstance = IsDatabaseInstanceSelectionAllowed + ? "" + : config.Config.MongoDbDatabase + ; + + if (DatabaseInstance != newInstance) + DatabaseInstance = newInstance; + else + DatabaseChanged?.Invoke(); + } + } + + public bool IsDatabaseInstanceSelectionAllowed => IsInstanceSelectionAllowed(Database); + + public bool IsInstanceSelectionAllowed(string database) + { + if (string.IsNullOrWhiteSpace(database)) + return false; + + if (Databases.TryGetValue(database, out var config)) + { + return string.IsNullOrWhiteSpace(config.Config.MongoDbDatabase); + } + + return false; + } + + private void Clear() + { + } + + public event Action? DatabaseChanged; + + public string Collection { get; set; } = ""; + + public string DatabaseInstance + { + get + { + if ( !string.IsNullOrWhiteSpace(field) ) + return field; + + if (!string.IsNullOrWhiteSpace(Database)) + return ""; + + var config = GetDatabaseConfig(Database); + + // this is to make sure that DatabaseChanged is only called once + if ( !IsDatabaseInstanceSelectionAllowed ) + field = config.Config.MongoDbDatabase; + + return field; + } + set + { + if (field == value) + return; + Clear(); + field = string.IsNullOrWhiteSpace(value) ? "" : value.Trim(); + DatabaseChanged?.Invoke(); + } + } + + public IMongoDbService MongoDb + { + get + { + if (string.IsNullOrWhiteSpace(Collection)) + throw new("Collection is not selected"); + + _ = GetDatabaseConfig(Database); + + return _mongoDbServiceFactory.Create(Database, Collection, DatabaseInstance); + } + } + + public IMongoDbDatabaseAdminService MongoDbAdmin => GetCustomAdmin(Database, DatabaseInstance); + + public IMongoDbDatabaseAdminService GetCustomAdmin(string databaseName, string databaseInstance) + { + _ = GetDatabaseConfig(Database); + return _mongoDbServiceFactory.CreateAdmin(databaseName, this, databaseInstance); + } + + public IMongoDbDatabaseAdminService MongoDbAdminForAdminDatabase + { + get + { + _ = GetDatabaseConfig(Database); + return _mongoDbServiceFactory.CreateAdmin(Database, this, "admin"); + } + } + + public IPivotTableDataSource PivotDataSource + { + get + { + _ = GetDatabaseConfig(Database); + return _mongoDbServiceFactory.CreatePivot(Database, DatabaseInstance); + } + } + + public IAuditService Audit + { + get + { + _ = GetDatabaseConfig(Database); + return _mongoDbServiceFactory.CreateAudit(Database, DatabaseInstance); + } + } + + public IMongoDbDatabaseAdminService GetShardConnection( + string host, + int port + ) + { + var config = GetDatabaseConfig(Database); + + var shardConfig = config.Config.Clone(); + shardConfig.DirectConnection = true; + shardConfig.AllowShardAccess = false; + shardConfig.MongoDbUrl = $"mongodb://{host}:{port}"; + + var db = _mongoDbServiceFactory.CreateAdmin(shardConfig, Database, this, DatabaseInstance); + return db; + } + + public MongoDbConfigRecord DatabaseConfig => GetDatabaseConfig(Database).Config; + + private DatabasesConfig.DatabaseConfig GetDatabaseConfig(string database) + { + if (!Databases.TryGetValue(database, out var cfg)) + throw new($"Database=\"{database}\" is not configured"); + return cfg; + } + + public DatabasesConfig.DatabaseConfig.LdapGroups LdapGroups + { + get + { + if (!Databases.TryGetValue(Database, out var cfg)) + throw new($"Database=\"{Database}\" is not configured"); + return cfg.Groups; + } + } +} \ No newline at end of file diff --git a/Rms.Risk.Mango/Shared/MainLayout.razor b/Rms.Risk.Mango/Shared/MainLayout.razor new file mode 100644 index 0000000..f75f826 --- /dev/null +++ b/Rms.Risk.Mango/Shared/MainLayout.razor @@ -0,0 +1,78 @@ +@using Rms.Service.Bootstrap.Security + +@inherits LayoutComponentBase +@implements IDisposable + +@inject IJSRuntime JS +@inject LogoutHandler Handler +@inject IConnectedUserList ConnectedUserList +@inject NavigationManager NavigationManager +@inject UserService UserService +@inject IConnectedUser ConnectedUser +@inject ILogger Log + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + +
+ + +
+
+ @Body +
+
+
+ +@code { + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender ) + await Handler.OnAfterRenderAsync(JS); + await base.OnAfterRenderAsync(firstRender); + } + + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + var userName = UserService.GetEmail(); + ConnectedUser.Name = userName; + ConnectedUserList.Add(ConnectedUser); + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs args) + { + try + { + Log.LogDebug($"Navigation: User=\"{ConnectedUser.Name}\" Url=\"{args.Location}\""); + } + catch (Exception e) + { + Log.LogError(e.Message, e); + } + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + ConnectedUserList.Remove(ConnectedUser); + } + +} + diff --git a/Rms.Risk.Mango/Shared/NavMenu.razor b/Rms.Risk.Mango/Shared/NavMenu.razor new file mode 100644 index 0000000..941bab0 --- /dev/null +++ b/Rms.Risk.Mango/Shared/NavMenu.razor @@ -0,0 +1,180 @@ +@implements IDisposable + +@inject IJSRuntime JsRuntime +@inject IUserSession Session +@inject IMenuService MenuService + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + + + +@code { + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + + //this is the first opportunity i have to set the title as JS has been initialised + await JsRuntime.InvokeVoidAsync("DashboardUtils.SetDocumentTitle", Header + " " + AppName); + await JsRuntime.InvokeVoidAsync("DashboardUtils.SetFavicon", FavIcon); + } + + private static string Env => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environments.Development; + private static string FavIcon => GetFavIcon(); + + public static string GetFavIcon() => (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environments.Development) switch + { + "Development" => @"images\favicon-dev.svg", + "UAT" => @"images\favicon.svg", + "PROD" => @"images\favicon-prod.svg", + _ => @"images\favicon.svg" + }; + + public static string GetMango() => (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environments.Development) switch + { + "Development" => @"images\mango-dev.svg", + "UAT" => @"images\mango.svg", + "PROD" => @"images\mango-prod.svg", + _ => @"images\mango.svg" + }; + + + private string Header => string.IsNullOrWhiteSpace(Session?.Database) ? Env : Session.Database; + private string AppName => "dbMango"; + + protected override void OnInitialized() + { + Session.DatabaseChanged += OnDatabaseChangeHandler; + } + + public void Dispose() + { + Session.DatabaseChanged -= OnDatabaseChangeHandler; + } + + private void OnDatabaseChangeHandler() + { + _ = InvokeAsync(StateHasChanged); + } +} diff --git a/Rms.Risk.Mango/_Imports.razor b/Rms.Risk.Mango/_Imports.razor new file mode 100644 index 0000000..99a1cdc --- /dev/null +++ b/Rms.Risk.Mango/_Imports.razor @@ -0,0 +1,47 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Rms.Risk.Mango +@using Rms.Risk.Mango.Shared +@using Rms.Risk.Mango.Components +@using Rms.Risk.Mango.Pivot.UI.Pivot +@using Rms.Risk.Mango.Pivot.UI.Tree +@using Rms.Risk.Mango.Pivot.UI.Controls +@using Rms.Risk.Mango.Pivot.UI.Forms +@using Blazored.Modal +@using Blazored.Modal.Services +@using System.Collections.Concurrent +@using System.Dynamic +@using MongoDB.Bson +@using Rms.Risk.Mango.Pivot.Core +@using Rms.Risk.Mango.Pivot.Core.Models +@using Rms.Risk.Mango.Services +@using Rms.Risk.Mango.Pages.Admin +@using Rms.Risk.Mango.Pivot.UI.Services +@using Rms.Risk.Mango.Services.Security +@using Rms.Risk.Mango.Interfaces + +@* + * dbMango + * + * Copyright 2025 Deutsche Bank AG + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *@ + diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css new file mode 100644 index 0000000..259a9e2 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css @@ -0,0 +1,3899 @@ +/*! + * Bootstrap Grid v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +html { + box-sizing: border-box; + -ms-overflow-style: scrollbar; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid, .container-sm, .container-md, .container-lg, .container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css.map b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css.map new file mode 100644 index 0000000..8661e3e --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ADEF;EACE,sBAAsB;EACtB,6BAA6B;ACA/B;;ADGA;;;EAGE,mBAAmB;ACArB;;ACTE;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;AFcnB;;AGqCI;EFtDF;ICWI,gBEqMK;EJ1LT;AACF;;AG+BI;EFtDF;ICWI,gBEsMK;EJrLT;AACF;;AGyBI;EFtDF;ICWI,gBEuMK;EJhLT;AACF;;AGmBI;EFtDF;ICWI,iBEwMM;EJ3KV;AACF;;ACnCE;ECPA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;AF8CnB;;AGKI;EFrCE;IACE,gBG8LG;EJ1JT;AACF;;AGDI;EFrCE;IACE,gBG+LG;EJrJT;AACF;;AGPI;EFrCE;IACE,gBGgMG;EJhJT;AACF;;AGbI;EFrCE;IACE,iBGiMI;EJ3IV;AACF;;ACnCE;ECrBA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AF4D3B;;ACpCE;EACE,eAAe;EACf,cAAc;ADuClB;;ACzCE;;EAMI,gBAAgB;EAChB,eAAe;ADwCrB;;AK1FE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;ALkG7B;;AK/EM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;ALkFvB;;AK9EQ;EH4BJ,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AFsD5B;;AKnFQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF2D5B;;AKxFQ;EH4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AFgE5B;;AK7FQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AFqE5B;;AKlGQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF0E5B;;AKvGQ;EH4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AF+E5B;;AKvGM;EHMJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AFqGjB;;AKxGQ;EHPN,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;AFgHzC;;AK7GQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFqHzC;;AKlHQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AF0HzC;;AKvHQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF+HzC;;AK5HQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFoIzC;;AKjIQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AFyIzC;;AKtIQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF8IzC;;AK3IQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFmJzC;;AKhJQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AFwJzC;;AKrJQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF6JzC;;AK1JQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFkKzC;;AK/JQ;EHPN,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AFuKzC;;AK/JM;EAAwB,kBAAS;EAAT,SAAS;ALmKvC;;AKjKM;EAAuB,kBD6KG;EC7KH,SD6KG;AJRhC;;AKlKQ;EAAwB,iBADZ;EACY,QADZ;ALuKpB;;AKtKQ;EAAwB,iBADZ;EACY,QADZ;AL2KpB;;AK1KQ;EAAwB,iBADZ;EACY,QADZ;AL+KpB;;AK9KQ;EAAwB,iBADZ;EACY,QADZ;ALmLpB;;AKlLQ;EAAwB,iBADZ;EACY,QADZ;ALuLpB;;AKtLQ;EAAwB,iBADZ;EACY,QADZ;AL2LpB;;AK1LQ;EAAwB,iBADZ;EACY,QADZ;AL+LpB;;AK9LQ;EAAwB,iBADZ;EACY,QADZ;ALmMpB;;AKlMQ;EAAwB,iBADZ;EACY,QADZ;ALuMpB;;AKtMQ;EAAwB,iBADZ;EACY,QADZ;AL2MpB;;AK1MQ;EAAwB,kBADZ;EACY,SADZ;AL+MpB;;AK9MQ;EAAwB,kBADZ;EACY,SADZ;ALmNpB;;AKlNQ;EAAwB,kBADZ;EACY,SADZ;ALuNpB;;AKhNU;EHRR,sBAA8C;AF4NhD;;AKpNU;EHRR,uBAA8C;AFgOhD;;AKxNU;EHRR,gBAA8C;AFoOhD;;AK5NU;EHRR,uBAA8C;AFwOhD;;AKhOU;EHRR,uBAA8C;AF4OhD;;AKpOU;EHRR,gBAA8C;AFgPhD;;AKxOU;EHRR,uBAA8C;AFoPhD;;AK5OU;EHRR,uBAA8C;AFwPhD;;AKhPU;EHRR,gBAA8C;AF4PhD;;AKpPU;EHRR,uBAA8C;AFgQhD;;AKxPU;EHRR,uBAA8C;AFoQhD;;AG/PI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELiSrB;EK7RM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFoQ1B;EKjSM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFwQ1B;EKrSM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF4Q1B;EKzSM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFgR1B;EK7SM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFoR1B;EKjTM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFwR1B;EKhTI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EF6Sf;EKhTM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFuTvC;EKpTM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2TvC;EKxTM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF+TvC;EK5TM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmUvC;EKhUM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuUvC;EKpUM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF2UvC;EKxUM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+UvC;EK5UM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmVvC;EKhVM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFuVvC;EKpVM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2VvC;EKxVM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+VvC;EK5VM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFmWvC;EK3VI;IAAwB,kBAAS;IAAT,SAAS;EL8VrC;EK5VI;IAAuB,kBD6KG;IC7KH,SD6KG;EJkL9B;EK5VM;IAAwB,iBADZ;IACY,QADZ;ELgWlB;EK/VM;IAAwB,iBADZ;IACY,QADZ;ELmWlB;EKlWM;IAAwB,iBADZ;IACY,QADZ;ELsWlB;EKrWM;IAAwB,iBADZ;IACY,QADZ;ELyWlB;EKxWM;IAAwB,iBADZ;IACY,QADZ;EL4WlB;EK3WM;IAAwB,iBADZ;IACY,QADZ;EL+WlB;EK9WM;IAAwB,iBADZ;IACY,QADZ;ELkXlB;EKjXM;IAAwB,iBADZ;IACY,QADZ;ELqXlB;EKpXM;IAAwB,iBADZ;IACY,QADZ;ELwXlB;EKvXM;IAAwB,iBADZ;IACY,QADZ;EL2XlB;EK1XM;IAAwB,kBADZ;IACY,SADZ;EL8XlB;EK7XM;IAAwB,kBADZ;IACY,SADZ;ELiYlB;EKhYM;IAAwB,kBADZ;IACY,SADZ;ELoYlB;EK7XQ;IHRR,cAA4B;EFwY5B;EKhYQ;IHRR,sBAA8C;EF2Y9C;EKnYQ;IHRR,uBAA8C;EF8Y9C;EKtYQ;IHRR,gBAA8C;EFiZ9C;EKzYQ;IHRR,uBAA8C;EFoZ9C;EK5YQ;IHRR,uBAA8C;EFuZ9C;EK/YQ;IHRR,gBAA8C;EF0Z9C;EKlZQ;IHRR,uBAA8C;EF6Z9C;EKrZQ;IHRR,uBAA8C;EFga9C;EKxZQ;IHRR,gBAA8C;EFma9C;EK3ZQ;IHRR,uBAA8C;EFsa9C;EK9ZQ;IHRR,uBAA8C;EFya9C;AACF;;AGraI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELucrB;EKncM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EF0a1B;EKvcM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF8a1B;EK3cM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFkb1B;EK/cM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFsb1B;EKndM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF0b1B;EKvdM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF8b1B;EKtdI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFmdf;EKtdM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EF6dvC;EK1dM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFievC;EK9dM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFqevC;EKleM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyevC;EKteM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF6evC;EK1eM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFifvC;EK9eM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqfvC;EKlfM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyfvC;EKtfM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF6fvC;EK1fM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFigBvC;EK9fM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqgBvC;EKlgBM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFygBvC;EKjgBI;IAAwB,kBAAS;IAAT,SAAS;ELogBrC;EKlgBI;IAAuB,kBD6KG;IC7KH,SD6KG;EJwV9B;EKlgBM;IAAwB,iBADZ;IACY,QADZ;ELsgBlB;EKrgBM;IAAwB,iBADZ;IACY,QADZ;ELygBlB;EKxgBM;IAAwB,iBADZ;IACY,QADZ;EL4gBlB;EK3gBM;IAAwB,iBADZ;IACY,QADZ;EL+gBlB;EK9gBM;IAAwB,iBADZ;IACY,QADZ;ELkhBlB;EKjhBM;IAAwB,iBADZ;IACY,QADZ;ELqhBlB;EKphBM;IAAwB,iBADZ;IACY,QADZ;ELwhBlB;EKvhBM;IAAwB,iBADZ;IACY,QADZ;EL2hBlB;EK1hBM;IAAwB,iBADZ;IACY,QADZ;EL8hBlB;EK7hBM;IAAwB,iBADZ;IACY,QADZ;ELiiBlB;EKhiBM;IAAwB,kBADZ;IACY,SADZ;ELoiBlB;EKniBM;IAAwB,kBADZ;IACY,SADZ;ELuiBlB;EKtiBM;IAAwB,kBADZ;IACY,SADZ;EL0iBlB;EKniBQ;IHRR,cAA4B;EF8iB5B;EKtiBQ;IHRR,sBAA8C;EFijB9C;EKziBQ;IHRR,uBAA8C;EFojB9C;EK5iBQ;IHRR,gBAA8C;EFujB9C;EK/iBQ;IHRR,uBAA8C;EF0jB9C;EKljBQ;IHRR,uBAA8C;EF6jB9C;EKrjBQ;IHRR,gBAA8C;EFgkB9C;EKxjBQ;IHRR,uBAA8C;EFmkB9C;EK3jBQ;IHRR,uBAA8C;EFskB9C;EK9jBQ;IHRR,gBAA8C;EFykB9C;EKjkBQ;IHRR,uBAA8C;EF4kB9C;EKpkBQ;IHRR,uBAA8C;EF+kB9C;AACF;;AG3kBI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EL6mBrB;EKzmBM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFglB1B;EK7mBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFolB1B;EKjnBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFwlB1B;EKrnBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF4lB1B;EKznBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFgmB1B;EK7nBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFomB1B;EK5nBI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFynBf;EK5nBM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFmoBvC;EKhoBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuoBvC;EKpoBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF2oBvC;EKxoBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+oBvC;EK5oBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmpBvC;EKhpBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFupBvC;EKppBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2pBvC;EKxpBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+pBvC;EK5pBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFmqBvC;EKhqBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuqBvC;EKpqBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2qBvC;EKxqBM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EF+qBvC;EKvqBI;IAAwB,kBAAS;IAAT,SAAS;EL0qBrC;EKxqBI;IAAuB,kBD6KG;IC7KH,SD6KG;EJ8f9B;EKxqBM;IAAwB,iBADZ;IACY,QADZ;EL4qBlB;EK3qBM;IAAwB,iBADZ;IACY,QADZ;EL+qBlB;EK9qBM;IAAwB,iBADZ;IACY,QADZ;ELkrBlB;EKjrBM;IAAwB,iBADZ;IACY,QADZ;ELqrBlB;EKprBM;IAAwB,iBADZ;IACY,QADZ;ELwrBlB;EKvrBM;IAAwB,iBADZ;IACY,QADZ;EL2rBlB;EK1rBM;IAAwB,iBADZ;IACY,QADZ;EL8rBlB;EK7rBM;IAAwB,iBADZ;IACY,QADZ;ELisBlB;EKhsBM;IAAwB,iBADZ;IACY,QADZ;ELosBlB;EKnsBM;IAAwB,iBADZ;IACY,QADZ;ELusBlB;EKtsBM;IAAwB,kBADZ;IACY,SADZ;EL0sBlB;EKzsBM;IAAwB,kBADZ;IACY,SADZ;EL6sBlB;EK5sBM;IAAwB,kBADZ;IACY,SADZ;ELgtBlB;EKzsBQ;IHRR,cAA4B;EFotB5B;EK5sBQ;IHRR,sBAA8C;EFutB9C;EK/sBQ;IHRR,uBAA8C;EF0tB9C;EKltBQ;IHRR,gBAA8C;EF6tB9C;EKrtBQ;IHRR,uBAA8C;EFguB9C;EKxtBQ;IHRR,uBAA8C;EFmuB9C;EK3tBQ;IHRR,gBAA8C;EFsuB9C;EK9tBQ;IHRR,uBAA8C;EFyuB9C;EKjuBQ;IHRR,uBAA8C;EF4uB9C;EKpuBQ;IHRR,gBAA8C;EF+uB9C;EKvuBQ;IHRR,uBAA8C;EFkvB9C;EK1uBQ;IHRR,uBAA8C;EFqvB9C;AACF;;AGjvBI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELmxBrB;EK/wBM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFsvB1B;EKnxBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF0vB1B;EKvxBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF8vB1B;EK3xBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFkwB1B;EK/xBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFswB1B;EKnyBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF0wB1B;EKlyBI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EF+xBf;EKlyBM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFyyBvC;EKtyBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF6yBvC;EK1yBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFizBvC;EK9yBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqzBvC;EKlzBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyzBvC;EKtzBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF6zBvC;EK1zBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFi0BvC;EK9zBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFq0BvC;EKl0BM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFy0BvC;EKt0BM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF60BvC;EK10BM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFi1BvC;EK90BM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFq1BvC;EK70BI;IAAwB,kBAAS;IAAT,SAAS;ELg1BrC;EK90BI;IAAuB,kBD6KG;IC7KH,SD6KG;EJoqB9B;EK90BM;IAAwB,iBADZ;IACY,QADZ;ELk1BlB;EKj1BM;IAAwB,iBADZ;IACY,QADZ;ELq1BlB;EKp1BM;IAAwB,iBADZ;IACY,QADZ;ELw1BlB;EKv1BM;IAAwB,iBADZ;IACY,QADZ;EL21BlB;EK11BM;IAAwB,iBADZ;IACY,QADZ;EL81BlB;EK71BM;IAAwB,iBADZ;IACY,QADZ;ELi2BlB;EKh2BM;IAAwB,iBADZ;IACY,QADZ;ELo2BlB;EKn2BM;IAAwB,iBADZ;IACY,QADZ;ELu2BlB;EKt2BM;IAAwB,iBADZ;IACY,QADZ;EL02BlB;EKz2BM;IAAwB,iBADZ;IACY,QADZ;EL62BlB;EK52BM;IAAwB,kBADZ;IACY,SADZ;ELg3BlB;EK/2BM;IAAwB,kBADZ;IACY,SADZ;ELm3BlB;EKl3BM;IAAwB,kBADZ;IACY,SADZ;ELs3BlB;EK/2BQ;IHRR,cAA4B;EF03B5B;EKl3BQ;IHRR,sBAA8C;EF63B9C;EKr3BQ;IHRR,uBAA8C;EFg4B9C;EKx3BQ;IHRR,gBAA8C;EFm4B9C;EK33BQ;IHRR,uBAA8C;EFs4B9C;EK93BQ;IHRR,uBAA8C;EFy4B9C;EKj4BQ;IHRR,gBAA8C;EF44B9C;EKp4BQ;IHRR,uBAA8C;EF+4B9C;EKv4BQ;IHRR,uBAA8C;EFk5B9C;EK14BQ;IHRR,gBAA8C;EFq5B9C;EK74BQ;IHRR,uBAA8C;EFw5B9C;EKh5BQ;IHRR,uBAA8C;EF25B9C;AACF;;AMx8BM;EAAwB,wBAA0B;AN48BxD;;AM58BM;EAAwB,0BAA0B;ANg9BxD;;AMh9BM;EAAwB,gCAA0B;ANo9BxD;;AMp9BM;EAAwB,yBAA0B;ANw9BxD;;AMx9BM;EAAwB,yBAA0B;AN49BxD;;AM59BM;EAAwB,6BAA0B;ANg+BxD;;AMh+BM;EAAwB,8BAA0B;ANo+BxD;;AMp+BM;EAAwB,+BAA0B;EAA1B,wBAA0B;ANw+BxD;;AMx+BM;EAAwB,sCAA0B;EAA1B,+BAA0B;AN4+BxD;;AG37BI;EGjDE;IAAwB,wBAA0B;ENi/BtD;EMj/BI;IAAwB,0BAA0B;ENo/BtD;EMp/BI;IAAwB,gCAA0B;ENu/BtD;EMv/BI;IAAwB,yBAA0B;EN0/BtD;EM1/BI;IAAwB,yBAA0B;EN6/BtD;EM7/BI;IAAwB,6BAA0B;ENggCtD;EMhgCI;IAAwB,8BAA0B;ENmgCtD;EMngCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENsgCtD;EMtgCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENygCtD;AACF;;AGz9BI;EGjDE;IAAwB,wBAA0B;EN+gCtD;EM/gCI;IAAwB,0BAA0B;ENkhCtD;EMlhCI;IAAwB,gCAA0B;ENqhCtD;EMrhCI;IAAwB,yBAA0B;ENwhCtD;EMxhCI;IAAwB,yBAA0B;EN2hCtD;EM3hCI;IAAwB,6BAA0B;EN8hCtD;EM9hCI;IAAwB,8BAA0B;ENiiCtD;EMjiCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENoiCtD;EMpiCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENuiCtD;AACF;;AGv/BI;EGjDE;IAAwB,wBAA0B;EN6iCtD;EM7iCI;IAAwB,0BAA0B;ENgjCtD;EMhjCI;IAAwB,gCAA0B;ENmjCtD;EMnjCI;IAAwB,yBAA0B;ENsjCtD;EMtjCI;IAAwB,yBAA0B;ENyjCtD;EMzjCI;IAAwB,6BAA0B;EN4jCtD;EM5jCI;IAAwB,8BAA0B;EN+jCtD;EM/jCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENkkCtD;EMlkCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENqkCtD;AACF;;AGrhCI;EGjDE;IAAwB,wBAA0B;EN2kCtD;EM3kCI;IAAwB,0BAA0B;EN8kCtD;EM9kCI;IAAwB,gCAA0B;ENilCtD;EMjlCI;IAAwB,yBAA0B;ENolCtD;EMplCI;IAAwB,yBAA0B;ENulCtD;EMvlCI;IAAwB,6BAA0B;EN0lCtD;EM1lCI;IAAwB,8BAA0B;EN6lCtD;EM7lCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENgmCtD;EMhmCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENmmCtD;AACF;;AM1lCA;EAEI;IAAqB,wBAA0B;EN6lCjD;EM7lCE;IAAqB,0BAA0B;ENgmCjD;EMhmCE;IAAqB,gCAA0B;ENmmCjD;EMnmCE;IAAqB,yBAA0B;ENsmCjD;EMtmCE;IAAqB,yBAA0B;ENymCjD;EMzmCE;IAAqB,6BAA0B;EN4mCjD;EM5mCE;IAAqB,8BAA0B;EN+mCjD;EM/mCE;IAAqB,+BAA0B;IAA1B,wBAA0B;ENknCjD;EMlnCE;IAAqB,sCAA0B;IAA1B,+BAA0B;ENqnCjD;AACF;;AOnoCI;EAAgC,kCAA8B;EAA9B,8BAA8B;APuoClE;;AOtoCI;EAAgC,qCAAiC;EAAjC,iCAAiC;AP0oCrE;;AOzoCI;EAAgC,0CAAsC;EAAtC,sCAAsC;AP6oC1E;;AO5oCI;EAAgC,6CAAyC;EAAzC,yCAAyC;APgpC7E;;AO9oCI;EAA8B,8BAA0B;EAA1B,0BAA0B;APkpC5D;;AOjpCI;EAA8B,gCAA4B;EAA5B,4BAA4B;APqpC9D;;AOppCI;EAA8B,sCAAkC;EAAlC,kCAAkC;APwpCpE;;AOvpCI;EAA8B,6BAAyB;EAAzB,yBAAyB;AP2pC3D;;AO1pCI;EAA8B,+BAAuB;EAAvB,uBAAuB;AP8pCzD;;AO7pCI;EAA8B,+BAAuB;EAAvB,uBAAuB;APiqCzD;;AOhqCI;EAA8B,+BAAyB;EAAzB,yBAAyB;APoqC3D;;AOnqCI;EAA8B,+BAAyB;EAAzB,yBAAyB;APuqC3D;;AOrqCI;EAAoC,+BAAsC;EAAtC,sCAAsC;APyqC9E;;AOxqCI;EAAoC,6BAAoC;EAApC,oCAAoC;AP4qC5E;;AO3qCI;EAAoC,gCAAkC;EAAlC,kCAAkC;AP+qC1E;;AO9qCI;EAAoC,iCAAyC;EAAzC,yCAAyC;APkrCjF;;AOjrCI;EAAoC,oCAAwC;EAAxC,wCAAwC;APqrChF;;AOnrCI;EAAiC,gCAAkC;EAAlC,kCAAkC;APurCvE;;AOtrCI;EAAiC,8BAAgC;EAAhC,gCAAgC;AP0rCrE;;AOzrCI;EAAiC,iCAA8B;EAA9B,8BAA8B;AP6rCnE;;AO5rCI;EAAiC,mCAAgC;EAAhC,gCAAgC;APgsCrE;;AO/rCI;EAAiC,kCAA+B;EAA/B,+BAA+B;APmsCpE;;AOjsCI;EAAkC,oCAAoC;EAApC,oCAAoC;APqsC1E;;AOpsCI;EAAkC,kCAAkC;EAAlC,kCAAkC;APwsCxE;;AOvsCI;EAAkC,qCAAgC;EAAhC,gCAAgC;AP2sCtE;;AO1sCI;EAAkC,sCAAuC;EAAvC,uCAAuC;AP8sC7E;;AO7sCI;EAAkC,yCAAsC;EAAtC,sCAAsC;APitC5E;;AOhtCI;EAAkC,sCAAiC;EAAjC,iCAAiC;APotCvE;;AOltCI;EAAgC,oCAA2B;EAA3B,2BAA2B;APstC/D;;AOrtCI;EAAgC,qCAAiC;EAAjC,iCAAiC;APytCrE;;AOxtCI;EAAgC,mCAA+B;EAA/B,+BAA+B;AP4tCnE;;AO3tCI;EAAgC,sCAA6B;EAA7B,6BAA6B;AP+tCjE;;AO9tCI;EAAgC,wCAA+B;EAA/B,+BAA+B;APkuCnE;;AOjuCI;EAAgC,uCAA8B;EAA9B,8BAA8B;APquClE;;AGztCI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPgxChE;EO/wCE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPkxCnE;EOjxCE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPoxCxE;EOnxCE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPsxC3E;EOpxCE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPuxC1D;EOtxCE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPyxC5D;EOxxCE;IAA8B,sCAAkC;IAAlC,kCAAkC;EP2xClE;EO1xCE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP6xCzD;EO5xCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP+xCvD;EO9xCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPiyCvD;EOhyCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPmyCzD;EOlyCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPqyCzD;EOnyCE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPsyC5E;EOryCE;IAAoC,6BAAoC;IAApC,oCAAoC;EPwyC1E;EOvyCE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP0yCxE;EOzyCE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP4yC/E;EO3yCE;IAAoC,oCAAwC;IAAxC,wCAAwC;EP8yC9E;EO5yCE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP+yCrE;EO9yCE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPizCnE;EOhzCE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPmzCjE;EOlzCE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPqzCnE;EOpzCE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPuzClE;EOrzCE;IAAkC,oCAAoC;IAApC,oCAAoC;EPwzCxE;EOvzCE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP0zCtE;EOzzCE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP4zCpE;EO3zCE;IAAkC,sCAAuC;IAAvC,uCAAuC;EP8zC3E;EO7zCE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPg0C1E;EO/zCE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPk0CrE;EOh0CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPm0C7D;EOl0CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPq0CnE;EOp0CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPu0CjE;EOt0CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPy0C/D;EOx0CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EP20CjE;EO10CE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP60ChE;AACF;;AGl0CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPy3ChE;EOx3CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP23CnE;EO13CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EP63CxE;EO53CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EP+3C3E;EO73CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPg4C1D;EO/3CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPk4C5D;EOj4CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPo4ClE;EOn4CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPs4CzD;EOr4CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPw4CvD;EOv4CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP04CvD;EOz4CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP44CzD;EO34CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP84CzD;EO54CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EP+4C5E;EO94CE;IAAoC,6BAAoC;IAApC,oCAAoC;EPi5C1E;EOh5CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPm5CxE;EOl5CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPq5C/E;EOp5CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPu5C9E;EOr5CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPw5CrE;EOv5CE;IAAiC,8BAAgC;IAAhC,gCAAgC;EP05CnE;EOz5CE;IAAiC,iCAA8B;IAA9B,8BAA8B;EP45CjE;EO35CE;IAAiC,mCAAgC;IAAhC,gCAAgC;EP85CnE;EO75CE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPg6ClE;EO95CE;IAAkC,oCAAoC;IAApC,oCAAoC;EPi6CxE;EOh6CE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPm6CtE;EOl6CE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPq6CpE;EOp6CE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPu6C3E;EOt6CE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPy6C1E;EOx6CE;IAAkC,sCAAiC;IAAjC,iCAAiC;EP26CrE;EOz6CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EP46C7D;EO36CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP86CnE;EO76CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPg7CjE;EO/6CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPk7C/D;EOj7CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPo7CjE;EOn7CE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPs7ChE;AACF;;AG36CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPk+ChE;EOj+CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPo+CnE;EOn+CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPs+CxE;EOr+CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPw+C3E;EOt+CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPy+C1D;EOx+CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EP2+C5D;EO1+CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EP6+ClE;EO5+CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP++CzD;EO9+CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPi/CvD;EOh/CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPm/CvD;EOl/CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPq/CzD;EOp/CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPu/CzD;EOr/CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPw/C5E;EOv/CE;IAAoC,6BAAoC;IAApC,oCAAoC;EP0/C1E;EOz/CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP4/CxE;EO3/CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP8/C/E;EO7/CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPggD9E;EO9/CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPigDrE;EOhgDE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPmgDnE;EOlgDE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPqgDjE;EOpgDE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPugDnE;EOtgDE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPygDlE;EOvgDE;IAAkC,oCAAoC;IAApC,oCAAoC;EP0gDxE;EOzgDE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP4gDtE;EO3gDE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP8gDpE;EO7gDE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPghD3E;EO/gDE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPkhD1E;EOjhDE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPohDrE;EOlhDE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPqhD7D;EOphDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPuhDnE;EOthDE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPyhDjE;EOxhDE;IAAgC,sCAA6B;IAA7B,6BAA6B;EP2hD/D;EO1hDE;IAAgC,wCAA+B;IAA/B,+BAA+B;EP6hDjE;EO5hDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP+hDhE;AACF;;AGphDI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EP2kDhE;EO1kDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP6kDnE;EO5kDE;IAAgC,0CAAsC;IAAtC,sCAAsC;EP+kDxE;EO9kDE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPilD3E;EO/kDE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPklD1D;EOjlDE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPolD5D;EOnlDE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPslDlE;EOrlDE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPwlDzD;EOvlDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP0lDvD;EOzlDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP4lDvD;EO3lDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP8lDzD;EO7lDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPgmDzD;EO9lDE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPimD5E;EOhmDE;IAAoC,6BAAoC;IAApC,oCAAoC;EPmmD1E;EOlmDE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPqmDxE;EOpmDE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPumD/E;EOtmDE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPymD9E;EOvmDE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP0mDrE;EOzmDE;IAAiC,8BAAgC;IAAhC,gCAAgC;EP4mDnE;EO3mDE;IAAiC,iCAA8B;IAA9B,8BAA8B;EP8mDjE;EO7mDE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPgnDnE;EO/mDE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPknDlE;EOhnDE;IAAkC,oCAAoC;IAApC,oCAAoC;EPmnDxE;EOlnDE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPqnDtE;EOpnDE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPunDpE;EOtnDE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPynD3E;EOxnDE;IAAkC,yCAAsC;IAAtC,sCAAsC;EP2nD1E;EO1nDE;IAAkC,sCAAiC;IAAjC,iCAAiC;EP6nDrE;EO3nDE;IAAgC,oCAA2B;IAA3B,2BAA2B;EP8nD7D;EO7nDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPgoDnE;EO/nDE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPkoDjE;EOjoDE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPooD/D;EOnoDE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPsoDjE;EOroDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPwoDhE;AACF;;AQ/qDQ;EAAgC,oBAA4B;ARmrDpE;;AQlrDQ;;EAEE,wBAAoC;ARqrD9C;;AQnrDQ;;EAEE,0BAAwC;ARsrDlD;;AQprDQ;;EAEE,2BAA0C;ARurDpD;;AQrrDQ;;EAEE,yBAAsC;ARwrDhD;;AQvsDQ;EAAgC,0BAA4B;AR2sDpE;;AQ1sDQ;;EAEE,8BAAoC;AR6sD9C;;AQ3sDQ;;EAEE,gCAAwC;AR8sDlD;;AQ5sDQ;;EAEE,iCAA0C;AR+sDpD;;AQ7sDQ;;EAEE,+BAAsC;ARgtDhD;;AQ/tDQ;EAAgC,yBAA4B;ARmuDpE;;AQluDQ;;EAEE,6BAAoC;ARquD9C;;AQnuDQ;;EAEE,+BAAwC;ARsuDlD;;AQpuDQ;;EAEE,gCAA0C;ARuuDpD;;AQruDQ;;EAEE,8BAAsC;ARwuDhD;;AQvvDQ;EAAgC,uBAA4B;AR2vDpE;;AQ1vDQ;;EAEE,2BAAoC;AR6vD9C;;AQ3vDQ;;EAEE,6BAAwC;AR8vDlD;;AQ5vDQ;;EAEE,8BAA0C;AR+vDpD;;AQ7vDQ;;EAEE,4BAAsC;ARgwDhD;;AQ/wDQ;EAAgC,yBAA4B;ARmxDpE;;AQlxDQ;;EAEE,6BAAoC;ARqxD9C;;AQnxDQ;;EAEE,+BAAwC;ARsxDlD;;AQpxDQ;;EAEE,gCAA0C;ARuxDpD;;AQrxDQ;;EAEE,8BAAsC;ARwxDhD;;AQvyDQ;EAAgC,uBAA4B;AR2yDpE;;AQ1yDQ;;EAEE,2BAAoC;AR6yD9C;;AQ3yDQ;;EAEE,6BAAwC;AR8yDlD;;AQ5yDQ;;EAEE,8BAA0C;AR+yDpD;;AQ7yDQ;;EAEE,4BAAsC;ARgzDhD;;AQ/zDQ;EAAgC,qBAA4B;ARm0DpE;;AQl0DQ;;EAEE,yBAAoC;ARq0D9C;;AQn0DQ;;EAEE,2BAAwC;ARs0DlD;;AQp0DQ;;EAEE,4BAA0C;ARu0DpD;;AQr0DQ;;EAEE,0BAAsC;ARw0DhD;;AQv1DQ;EAAgC,2BAA4B;AR21DpE;;AQ11DQ;;EAEE,+BAAoC;AR61D9C;;AQ31DQ;;EAEE,iCAAwC;AR81DlD;;AQ51DQ;;EAEE,kCAA0C;AR+1DpD;;AQ71DQ;;EAEE,gCAAsC;ARg2DhD;;AQ/2DQ;EAAgC,0BAA4B;ARm3DpE;;AQl3DQ;;EAEE,8BAAoC;ARq3D9C;;AQn3DQ;;EAEE,gCAAwC;ARs3DlD;;AQp3DQ;;EAEE,iCAA0C;ARu3DpD;;AQr3DQ;;EAEE,+BAAsC;ARw3DhD;;AQv4DQ;EAAgC,wBAA4B;AR24DpE;;AQ14DQ;;EAEE,4BAAoC;AR64D9C;;AQ34DQ;;EAEE,8BAAwC;AR84DlD;;AQ54DQ;;EAEE,+BAA0C;AR+4DpD;;AQ74DQ;;EAEE,6BAAsC;ARg5DhD;;AQ/5DQ;EAAgC,0BAA4B;ARm6DpE;;AQl6DQ;;EAEE,8BAAoC;ARq6D9C;;AQn6DQ;;EAEE,gCAAwC;ARs6DlD;;AQp6DQ;;EAEE,iCAA0C;ARu6DpD;;AQr6DQ;;EAEE,+BAAsC;ARw6DhD;;AQv7DQ;EAAgC,wBAA4B;AR27DpE;;AQ17DQ;;EAEE,4BAAoC;AR67D9C;;AQ37DQ;;EAEE,8BAAwC;AR87DlD;;AQ57DQ;;EAEE,+BAA0C;AR+7DpD;;AQ77DQ;;EAEE,6BAAsC;ARg8DhD;;AQx7DQ;EAAwB,2BAA2B;AR47D3D;;AQ37DQ;;EAEE,+BAA+B;AR87DzC;;AQ57DQ;;EAEE,iCAAiC;AR+7D3C;;AQ77DQ;;EAEE,kCAAkC;ARg8D5C;;AQ97DQ;;EAEE,gCAAgC;ARi8D1C;;AQh9DQ;EAAwB,0BAA2B;ARo9D3D;;AQn9DQ;;EAEE,8BAA+B;ARs9DzC;;AQp9DQ;;EAEE,gCAAiC;ARu9D3C;;AQr9DQ;;EAEE,iCAAkC;ARw9D5C;;AQt9DQ;;EAEE,+BAAgC;ARy9D1C;;AQx+DQ;EAAwB,wBAA2B;AR4+D3D;;AQ3+DQ;;EAEE,4BAA+B;AR8+DzC;;AQ5+DQ;;EAEE,8BAAiC;AR++D3C;;AQ7+DQ;;EAEE,+BAAkC;ARg/D5C;;AQ9+DQ;;EAEE,6BAAgC;ARi/D1C;;AQhgEQ;EAAwB,0BAA2B;ARogE3D;;AQngEQ;;EAEE,8BAA+B;ARsgEzC;;AQpgEQ;;EAEE,gCAAiC;ARugE3C;;AQrgEQ;;EAEE,iCAAkC;ARwgE5C;;AQtgEQ;;EAEE,+BAAgC;ARygE1C;;AQxhEQ;EAAwB,wBAA2B;AR4hE3D;;AQ3hEQ;;EAEE,4BAA+B;AR8hEzC;;AQ5hEQ;;EAEE,8BAAiC;AR+hE3C;;AQ7hEQ;;EAEE,+BAAkC;ARgiE5C;;AQ9hEQ;;EAEE,6BAAgC;ARiiE1C;;AQ3hEI;EAAmB,uBAAuB;AR+hE9C;;AQ9hEI;;EAEE,2BAA2B;ARiiEjC;;AQ/hEI;;EAEE,6BAA6B;ARkiEnC;;AQhiEI;;EAEE,8BAA8B;ARmiEpC;;AQjiEI;;EAEE,4BAA4B;ARoiElC;;AG7iEI;EKlDI;IAAgC,oBAA4B;ERomElE;EQnmEM;;IAEE,wBAAoC;ERqmE5C;EQnmEM;;IAEE,0BAAwC;ERqmEhD;EQnmEM;;IAEE,2BAA0C;ERqmElD;EQnmEM;;IAEE,yBAAsC;ERqmE9C;EQpnEM;IAAgC,0BAA4B;ERunElE;EQtnEM;;IAEE,8BAAoC;ERwnE5C;EQtnEM;;IAEE,gCAAwC;ERwnEhD;EQtnEM;;IAEE,iCAA0C;ERwnElD;EQtnEM;;IAEE,+BAAsC;ERwnE9C;EQvoEM;IAAgC,yBAA4B;ER0oElE;EQzoEM;;IAEE,6BAAoC;ER2oE5C;EQzoEM;;IAEE,+BAAwC;ER2oEhD;EQzoEM;;IAEE,gCAA0C;ER2oElD;EQzoEM;;IAEE,8BAAsC;ER2oE9C;EQ1pEM;IAAgC,uBAA4B;ER6pElE;EQ5pEM;;IAEE,2BAAoC;ER8pE5C;EQ5pEM;;IAEE,6BAAwC;ER8pEhD;EQ5pEM;;IAEE,8BAA0C;ER8pElD;EQ5pEM;;IAEE,4BAAsC;ER8pE9C;EQ7qEM;IAAgC,yBAA4B;ERgrElE;EQ/qEM;;IAEE,6BAAoC;ERirE5C;EQ/qEM;;IAEE,+BAAwC;ERirEhD;EQ/qEM;;IAEE,gCAA0C;ERirElD;EQ/qEM;;IAEE,8BAAsC;ERirE9C;EQhsEM;IAAgC,uBAA4B;ERmsElE;EQlsEM;;IAEE,2BAAoC;ERosE5C;EQlsEM;;IAEE,6BAAwC;ERosEhD;EQlsEM;;IAEE,8BAA0C;ERosElD;EQlsEM;;IAEE,4BAAsC;ERosE9C;EQntEM;IAAgC,qBAA4B;ERstElE;EQrtEM;;IAEE,yBAAoC;ERutE5C;EQrtEM;;IAEE,2BAAwC;ERutEhD;EQrtEM;;IAEE,4BAA0C;ERutElD;EQrtEM;;IAEE,0BAAsC;ERutE9C;EQtuEM;IAAgC,2BAA4B;ERyuElE;EQxuEM;;IAEE,+BAAoC;ER0uE5C;EQxuEM;;IAEE,iCAAwC;ER0uEhD;EQxuEM;;IAEE,kCAA0C;ER0uElD;EQxuEM;;IAEE,gCAAsC;ER0uE9C;EQzvEM;IAAgC,0BAA4B;ER4vElE;EQ3vEM;;IAEE,8BAAoC;ER6vE5C;EQ3vEM;;IAEE,gCAAwC;ER6vEhD;EQ3vEM;;IAEE,iCAA0C;ER6vElD;EQ3vEM;;IAEE,+BAAsC;ER6vE9C;EQ5wEM;IAAgC,wBAA4B;ER+wElE;EQ9wEM;;IAEE,4BAAoC;ERgxE5C;EQ9wEM;;IAEE,8BAAwC;ERgxEhD;EQ9wEM;;IAEE,+BAA0C;ERgxElD;EQ9wEM;;IAEE,6BAAsC;ERgxE9C;EQ/xEM;IAAgC,0BAA4B;ERkyElE;EQjyEM;;IAEE,8BAAoC;ERmyE5C;EQjyEM;;IAEE,gCAAwC;ERmyEhD;EQjyEM;;IAEE,iCAA0C;ERmyElD;EQjyEM;;IAEE,+BAAsC;ERmyE9C;EQlzEM;IAAgC,wBAA4B;ERqzElE;EQpzEM;;IAEE,4BAAoC;ERszE5C;EQpzEM;;IAEE,8BAAwC;ERszEhD;EQpzEM;;IAEE,+BAA0C;ERszElD;EQpzEM;;IAEE,6BAAsC;ERszE9C;EQ9yEM;IAAwB,2BAA2B;ERizEzD;EQhzEM;;IAEE,+BAA+B;ERkzEvC;EQhzEM;;IAEE,iCAAiC;ERkzEzC;EQhzEM;;IAEE,kCAAkC;ERkzE1C;EQhzEM;;IAEE,gCAAgC;ERkzExC;EQj0EM;IAAwB,0BAA2B;ERo0EzD;EQn0EM;;IAEE,8BAA+B;ERq0EvC;EQn0EM;;IAEE,gCAAiC;ERq0EzC;EQn0EM;;IAEE,iCAAkC;ERq0E1C;EQn0EM;;IAEE,+BAAgC;ERq0ExC;EQp1EM;IAAwB,wBAA2B;ERu1EzD;EQt1EM;;IAEE,4BAA+B;ERw1EvC;EQt1EM;;IAEE,8BAAiC;ERw1EzC;EQt1EM;;IAEE,+BAAkC;ERw1E1C;EQt1EM;;IAEE,6BAAgC;ERw1ExC;EQv2EM;IAAwB,0BAA2B;ER02EzD;EQz2EM;;IAEE,8BAA+B;ER22EvC;EQz2EM;;IAEE,gCAAiC;ER22EzC;EQz2EM;;IAEE,iCAAkC;ER22E1C;EQz2EM;;IAEE,+BAAgC;ER22ExC;EQ13EM;IAAwB,wBAA2B;ER63EzD;EQ53EM;;IAEE,4BAA+B;ER83EvC;EQ53EM;;IAEE,8BAAiC;ER83EzC;EQ53EM;;IAEE,+BAAkC;ER83E1C;EQ53EM;;IAEE,6BAAgC;ER83ExC;EQx3EE;IAAmB,uBAAuB;ER23E5C;EQ13EE;;IAEE,2BAA2B;ER43E/B;EQ13EE;;IAEE,6BAA6B;ER43EjC;EQ13EE;;IAEE,8BAA8B;ER43ElC;EQ13EE;;IAEE,4BAA4B;ER43EhC;AACF;;AGt4EI;EKlDI;IAAgC,oBAA4B;ER67ElE;EQ57EM;;IAEE,wBAAoC;ER87E5C;EQ57EM;;IAEE,0BAAwC;ER87EhD;EQ57EM;;IAEE,2BAA0C;ER87ElD;EQ57EM;;IAEE,yBAAsC;ER87E9C;EQ78EM;IAAgC,0BAA4B;ERg9ElE;EQ/8EM;;IAEE,8BAAoC;ERi9E5C;EQ/8EM;;IAEE,gCAAwC;ERi9EhD;EQ/8EM;;IAEE,iCAA0C;ERi9ElD;EQ/8EM;;IAEE,+BAAsC;ERi9E9C;EQh+EM;IAAgC,yBAA4B;ERm+ElE;EQl+EM;;IAEE,6BAAoC;ERo+E5C;EQl+EM;;IAEE,+BAAwC;ERo+EhD;EQl+EM;;IAEE,gCAA0C;ERo+ElD;EQl+EM;;IAEE,8BAAsC;ERo+E9C;EQn/EM;IAAgC,uBAA4B;ERs/ElE;EQr/EM;;IAEE,2BAAoC;ERu/E5C;EQr/EM;;IAEE,6BAAwC;ERu/EhD;EQr/EM;;IAEE,8BAA0C;ERu/ElD;EQr/EM;;IAEE,4BAAsC;ERu/E9C;EQtgFM;IAAgC,yBAA4B;ERygFlE;EQxgFM;;IAEE,6BAAoC;ER0gF5C;EQxgFM;;IAEE,+BAAwC;ER0gFhD;EQxgFM;;IAEE,gCAA0C;ER0gFlD;EQxgFM;;IAEE,8BAAsC;ER0gF9C;EQzhFM;IAAgC,uBAA4B;ER4hFlE;EQ3hFM;;IAEE,2BAAoC;ER6hF5C;EQ3hFM;;IAEE,6BAAwC;ER6hFhD;EQ3hFM;;IAEE,8BAA0C;ER6hFlD;EQ3hFM;;IAEE,4BAAsC;ER6hF9C;EQ5iFM;IAAgC,qBAA4B;ER+iFlE;EQ9iFM;;IAEE,yBAAoC;ERgjF5C;EQ9iFM;;IAEE,2BAAwC;ERgjFhD;EQ9iFM;;IAEE,4BAA0C;ERgjFlD;EQ9iFM;;IAEE,0BAAsC;ERgjF9C;EQ/jFM;IAAgC,2BAA4B;ERkkFlE;EQjkFM;;IAEE,+BAAoC;ERmkF5C;EQjkFM;;IAEE,iCAAwC;ERmkFhD;EQjkFM;;IAEE,kCAA0C;ERmkFlD;EQjkFM;;IAEE,gCAAsC;ERmkF9C;EQllFM;IAAgC,0BAA4B;ERqlFlE;EQplFM;;IAEE,8BAAoC;ERslF5C;EQplFM;;IAEE,gCAAwC;ERslFhD;EQplFM;;IAEE,iCAA0C;ERslFlD;EQplFM;;IAEE,+BAAsC;ERslF9C;EQrmFM;IAAgC,wBAA4B;ERwmFlE;EQvmFM;;IAEE,4BAAoC;ERymF5C;EQvmFM;;IAEE,8BAAwC;ERymFhD;EQvmFM;;IAEE,+BAA0C;ERymFlD;EQvmFM;;IAEE,6BAAsC;ERymF9C;EQxnFM;IAAgC,0BAA4B;ER2nFlE;EQ1nFM;;IAEE,8BAAoC;ER4nF5C;EQ1nFM;;IAEE,gCAAwC;ER4nFhD;EQ1nFM;;IAEE,iCAA0C;ER4nFlD;EQ1nFM;;IAEE,+BAAsC;ER4nF9C;EQ3oFM;IAAgC,wBAA4B;ER8oFlE;EQ7oFM;;IAEE,4BAAoC;ER+oF5C;EQ7oFM;;IAEE,8BAAwC;ER+oFhD;EQ7oFM;;IAEE,+BAA0C;ER+oFlD;EQ7oFM;;IAEE,6BAAsC;ER+oF9C;EQvoFM;IAAwB,2BAA2B;ER0oFzD;EQzoFM;;IAEE,+BAA+B;ER2oFvC;EQzoFM;;IAEE,iCAAiC;ER2oFzC;EQzoFM;;IAEE,kCAAkC;ER2oF1C;EQzoFM;;IAEE,gCAAgC;ER2oFxC;EQ1pFM;IAAwB,0BAA2B;ER6pFzD;EQ5pFM;;IAEE,8BAA+B;ER8pFvC;EQ5pFM;;IAEE,gCAAiC;ER8pFzC;EQ5pFM;;IAEE,iCAAkC;ER8pF1C;EQ5pFM;;IAEE,+BAAgC;ER8pFxC;EQ7qFM;IAAwB,wBAA2B;ERgrFzD;EQ/qFM;;IAEE,4BAA+B;ERirFvC;EQ/qFM;;IAEE,8BAAiC;ERirFzC;EQ/qFM;;IAEE,+BAAkC;ERirF1C;EQ/qFM;;IAEE,6BAAgC;ERirFxC;EQhsFM;IAAwB,0BAA2B;ERmsFzD;EQlsFM;;IAEE,8BAA+B;ERosFvC;EQlsFM;;IAEE,gCAAiC;ERosFzC;EQlsFM;;IAEE,iCAAkC;ERosF1C;EQlsFM;;IAEE,+BAAgC;ERosFxC;EQntFM;IAAwB,wBAA2B;ERstFzD;EQrtFM;;IAEE,4BAA+B;ERutFvC;EQrtFM;;IAEE,8BAAiC;ERutFzC;EQrtFM;;IAEE,+BAAkC;ERutF1C;EQrtFM;;IAEE,6BAAgC;ERutFxC;EQjtFE;IAAmB,uBAAuB;ERotF5C;EQntFE;;IAEE,2BAA2B;ERqtF/B;EQntFE;;IAEE,6BAA6B;ERqtFjC;EQntFE;;IAEE,8BAA8B;ERqtFlC;EQntFE;;IAEE,4BAA4B;ERqtFhC;AACF;;AG/tFI;EKlDI;IAAgC,oBAA4B;ERsxFlE;EQrxFM;;IAEE,wBAAoC;ERuxF5C;EQrxFM;;IAEE,0BAAwC;ERuxFhD;EQrxFM;;IAEE,2BAA0C;ERuxFlD;EQrxFM;;IAEE,yBAAsC;ERuxF9C;EQtyFM;IAAgC,0BAA4B;ERyyFlE;EQxyFM;;IAEE,8BAAoC;ER0yF5C;EQxyFM;;IAEE,gCAAwC;ER0yFhD;EQxyFM;;IAEE,iCAA0C;ER0yFlD;EQxyFM;;IAEE,+BAAsC;ER0yF9C;EQzzFM;IAAgC,yBAA4B;ER4zFlE;EQ3zFM;;IAEE,6BAAoC;ER6zF5C;EQ3zFM;;IAEE,+BAAwC;ER6zFhD;EQ3zFM;;IAEE,gCAA0C;ER6zFlD;EQ3zFM;;IAEE,8BAAsC;ER6zF9C;EQ50FM;IAAgC,uBAA4B;ER+0FlE;EQ90FM;;IAEE,2BAAoC;ERg1F5C;EQ90FM;;IAEE,6BAAwC;ERg1FhD;EQ90FM;;IAEE,8BAA0C;ERg1FlD;EQ90FM;;IAEE,4BAAsC;ERg1F9C;EQ/1FM;IAAgC,yBAA4B;ERk2FlE;EQj2FM;;IAEE,6BAAoC;ERm2F5C;EQj2FM;;IAEE,+BAAwC;ERm2FhD;EQj2FM;;IAEE,gCAA0C;ERm2FlD;EQj2FM;;IAEE,8BAAsC;ERm2F9C;EQl3FM;IAAgC,uBAA4B;ERq3FlE;EQp3FM;;IAEE,2BAAoC;ERs3F5C;EQp3FM;;IAEE,6BAAwC;ERs3FhD;EQp3FM;;IAEE,8BAA0C;ERs3FlD;EQp3FM;;IAEE,4BAAsC;ERs3F9C;EQr4FM;IAAgC,qBAA4B;ERw4FlE;EQv4FM;;IAEE,yBAAoC;ERy4F5C;EQv4FM;;IAEE,2BAAwC;ERy4FhD;EQv4FM;;IAEE,4BAA0C;ERy4FlD;EQv4FM;;IAEE,0BAAsC;ERy4F9C;EQx5FM;IAAgC,2BAA4B;ER25FlE;EQ15FM;;IAEE,+BAAoC;ER45F5C;EQ15FM;;IAEE,iCAAwC;ER45FhD;EQ15FM;;IAEE,kCAA0C;ER45FlD;EQ15FM;;IAEE,gCAAsC;ER45F9C;EQ36FM;IAAgC,0BAA4B;ER86FlE;EQ76FM;;IAEE,8BAAoC;ER+6F5C;EQ76FM;;IAEE,gCAAwC;ER+6FhD;EQ76FM;;IAEE,iCAA0C;ER+6FlD;EQ76FM;;IAEE,+BAAsC;ER+6F9C;EQ97FM;IAAgC,wBAA4B;ERi8FlE;EQh8FM;;IAEE,4BAAoC;ERk8F5C;EQh8FM;;IAEE,8BAAwC;ERk8FhD;EQh8FM;;IAEE,+BAA0C;ERk8FlD;EQh8FM;;IAEE,6BAAsC;ERk8F9C;EQj9FM;IAAgC,0BAA4B;ERo9FlE;EQn9FM;;IAEE,8BAAoC;ERq9F5C;EQn9FM;;IAEE,gCAAwC;ERq9FhD;EQn9FM;;IAEE,iCAA0C;ERq9FlD;EQn9FM;;IAEE,+BAAsC;ERq9F9C;EQp+FM;IAAgC,wBAA4B;ERu+FlE;EQt+FM;;IAEE,4BAAoC;ERw+F5C;EQt+FM;;IAEE,8BAAwC;ERw+FhD;EQt+FM;;IAEE,+BAA0C;ERw+FlD;EQt+FM;;IAEE,6BAAsC;ERw+F9C;EQh+FM;IAAwB,2BAA2B;ERm+FzD;EQl+FM;;IAEE,+BAA+B;ERo+FvC;EQl+FM;;IAEE,iCAAiC;ERo+FzC;EQl+FM;;IAEE,kCAAkC;ERo+F1C;EQl+FM;;IAEE,gCAAgC;ERo+FxC;EQn/FM;IAAwB,0BAA2B;ERs/FzD;EQr/FM;;IAEE,8BAA+B;ERu/FvC;EQr/FM;;IAEE,gCAAiC;ERu/FzC;EQr/FM;;IAEE,iCAAkC;ERu/F1C;EQr/FM;;IAEE,+BAAgC;ERu/FxC;EQtgGM;IAAwB,wBAA2B;ERygGzD;EQxgGM;;IAEE,4BAA+B;ER0gGvC;EQxgGM;;IAEE,8BAAiC;ER0gGzC;EQxgGM;;IAEE,+BAAkC;ER0gG1C;EQxgGM;;IAEE,6BAAgC;ER0gGxC;EQzhGM;IAAwB,0BAA2B;ER4hGzD;EQ3hGM;;IAEE,8BAA+B;ER6hGvC;EQ3hGM;;IAEE,gCAAiC;ER6hGzC;EQ3hGM;;IAEE,iCAAkC;ER6hG1C;EQ3hGM;;IAEE,+BAAgC;ER6hGxC;EQ5iGM;IAAwB,wBAA2B;ER+iGzD;EQ9iGM;;IAEE,4BAA+B;ERgjGvC;EQ9iGM;;IAEE,8BAAiC;ERgjGzC;EQ9iGM;;IAEE,+BAAkC;ERgjG1C;EQ9iGM;;IAEE,6BAAgC;ERgjGxC;EQ1iGE;IAAmB,uBAAuB;ER6iG5C;EQ5iGE;;IAEE,2BAA2B;ER8iG/B;EQ5iGE;;IAEE,6BAA6B;ER8iGjC;EQ5iGE;;IAEE,8BAA8B;ER8iGlC;EQ5iGE;;IAEE,4BAA4B;ER8iGhC;AACF;;AGxjGI;EKlDI;IAAgC,oBAA4B;ER+mGlE;EQ9mGM;;IAEE,wBAAoC;ERgnG5C;EQ9mGM;;IAEE,0BAAwC;ERgnGhD;EQ9mGM;;IAEE,2BAA0C;ERgnGlD;EQ9mGM;;IAEE,yBAAsC;ERgnG9C;EQ/nGM;IAAgC,0BAA4B;ERkoGlE;EQjoGM;;IAEE,8BAAoC;ERmoG5C;EQjoGM;;IAEE,gCAAwC;ERmoGhD;EQjoGM;;IAEE,iCAA0C;ERmoGlD;EQjoGM;;IAEE,+BAAsC;ERmoG9C;EQlpGM;IAAgC,yBAA4B;ERqpGlE;EQppGM;;IAEE,6BAAoC;ERspG5C;EQppGM;;IAEE,+BAAwC;ERspGhD;EQppGM;;IAEE,gCAA0C;ERspGlD;EQppGM;;IAEE,8BAAsC;ERspG9C;EQrqGM;IAAgC,uBAA4B;ERwqGlE;EQvqGM;;IAEE,2BAAoC;ERyqG5C;EQvqGM;;IAEE,6BAAwC;ERyqGhD;EQvqGM;;IAEE,8BAA0C;ERyqGlD;EQvqGM;;IAEE,4BAAsC;ERyqG9C;EQxrGM;IAAgC,yBAA4B;ER2rGlE;EQ1rGM;;IAEE,6BAAoC;ER4rG5C;EQ1rGM;;IAEE,+BAAwC;ER4rGhD;EQ1rGM;;IAEE,gCAA0C;ER4rGlD;EQ1rGM;;IAEE,8BAAsC;ER4rG9C;EQ3sGM;IAAgC,uBAA4B;ER8sGlE;EQ7sGM;;IAEE,2BAAoC;ER+sG5C;EQ7sGM;;IAEE,6BAAwC;ER+sGhD;EQ7sGM;;IAEE,8BAA0C;ER+sGlD;EQ7sGM;;IAEE,4BAAsC;ER+sG9C;EQ9tGM;IAAgC,qBAA4B;ERiuGlE;EQhuGM;;IAEE,yBAAoC;ERkuG5C;EQhuGM;;IAEE,2BAAwC;ERkuGhD;EQhuGM;;IAEE,4BAA0C;ERkuGlD;EQhuGM;;IAEE,0BAAsC;ERkuG9C;EQjvGM;IAAgC,2BAA4B;ERovGlE;EQnvGM;;IAEE,+BAAoC;ERqvG5C;EQnvGM;;IAEE,iCAAwC;ERqvGhD;EQnvGM;;IAEE,kCAA0C;ERqvGlD;EQnvGM;;IAEE,gCAAsC;ERqvG9C;EQpwGM;IAAgC,0BAA4B;ERuwGlE;EQtwGM;;IAEE,8BAAoC;ERwwG5C;EQtwGM;;IAEE,gCAAwC;ERwwGhD;EQtwGM;;IAEE,iCAA0C;ERwwGlD;EQtwGM;;IAEE,+BAAsC;ERwwG9C;EQvxGM;IAAgC,wBAA4B;ER0xGlE;EQzxGM;;IAEE,4BAAoC;ER2xG5C;EQzxGM;;IAEE,8BAAwC;ER2xGhD;EQzxGM;;IAEE,+BAA0C;ER2xGlD;EQzxGM;;IAEE,6BAAsC;ER2xG9C;EQ1yGM;IAAgC,0BAA4B;ER6yGlE;EQ5yGM;;IAEE,8BAAoC;ER8yG5C;EQ5yGM;;IAEE,gCAAwC;ER8yGhD;EQ5yGM;;IAEE,iCAA0C;ER8yGlD;EQ5yGM;;IAEE,+BAAsC;ER8yG9C;EQ7zGM;IAAgC,wBAA4B;ERg0GlE;EQ/zGM;;IAEE,4BAAoC;ERi0G5C;EQ/zGM;;IAEE,8BAAwC;ERi0GhD;EQ/zGM;;IAEE,+BAA0C;ERi0GlD;EQ/zGM;;IAEE,6BAAsC;ERi0G9C;EQzzGM;IAAwB,2BAA2B;ER4zGzD;EQ3zGM;;IAEE,+BAA+B;ER6zGvC;EQ3zGM;;IAEE,iCAAiC;ER6zGzC;EQ3zGM;;IAEE,kCAAkC;ER6zG1C;EQ3zGM;;IAEE,gCAAgC;ER6zGxC;EQ50GM;IAAwB,0BAA2B;ER+0GzD;EQ90GM;;IAEE,8BAA+B;ERg1GvC;EQ90GM;;IAEE,gCAAiC;ERg1GzC;EQ90GM;;IAEE,iCAAkC;ERg1G1C;EQ90GM;;IAEE,+BAAgC;ERg1GxC;EQ/1GM;IAAwB,wBAA2B;ERk2GzD;EQj2GM;;IAEE,4BAA+B;ERm2GvC;EQj2GM;;IAEE,8BAAiC;ERm2GzC;EQj2GM;;IAEE,+BAAkC;ERm2G1C;EQj2GM;;IAEE,6BAAgC;ERm2GxC;EQl3GM;IAAwB,0BAA2B;ERq3GzD;EQp3GM;;IAEE,8BAA+B;ERs3GvC;EQp3GM;;IAEE,gCAAiC;ERs3GzC;EQp3GM;;IAEE,iCAAkC;ERs3G1C;EQp3GM;;IAEE,+BAAgC;ERs3GxC;EQr4GM;IAAwB,wBAA2B;ERw4GzD;EQv4GM;;IAEE,4BAA+B;ERy4GvC;EQv4GM;;IAEE,8BAAiC;ERy4GzC;EQv4GM;;IAEE,+BAAkC;ERy4G1C;EQv4GM;;IAEE,6BAAgC;ERy4GxC;EQn4GE;IAAmB,uBAAuB;ERs4G5C;EQr4GE;;IAEE,2BAA2B;ERu4G/B;EQr4GE;;IAEE,6BAA6B;ERu4GjC;EQr4GE;;IAEE,8BAA8B;ERu4GlC;EQr4GE;;IAEE,4BAA4B;ERu4GhC;AACF","file":"bootstrap-grid.css","sourcesContent":["/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 0000000..6533f31 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css.map b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css.map new file mode 100644 index 0000000..1b393db --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","dist/css/bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;AAOA,KACE,WAAA,WACA,mBAAA,UAGF,ECCA,QADA,SDGE,WAAA,QETA,WCDA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFtDF,WCWI,UAAA,OC2CF,yBFtDF,WCWI,UAAA,OC2CF,yBFtDF,WCWI,UAAA,OC2CF,0BFtDF,WCWI,UAAA,QDLJ,iBAAA,cAAA,cAAA,cAAA,cCPA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFrCE,WAAA,cACE,UAAA,OEoCJ,yBFrCE,WAAA,cAAA,cACE,UAAA,OEoCJ,yBFrCE,WAAA,cAAA,cAAA,cACE,UAAA,OEoCJ,0BFrCE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QAoBN,KCrBA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,MACA,YAAA,MDwBA,YACE,aAAA,EACA,YAAA,EAFF,iBD8CF,0BCxCM,cAAA,EACA,aAAA,EGlDJ,KAAA,OAAA,QAAA,QAAA,QAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OJ+FF,UAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFkJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACnG,aAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aIlGI,SAAA,SACA,MAAA,KACA,cAAA,KACA,aAAA,KAmBE,KACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,cF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,UFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,OFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,QFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,QFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,QFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,aAAwB,eAAA,GAAA,MAAA,GAExB,YAAuB,eAAA,GAAA,MAAA,GAGrB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAMtB,UFRR,YAAA,UEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,WFRR,YAAA,WEQQ,WFRR,YAAA,WCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,0BC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YG5CI,QAAwB,QAAA,eAAxB,UAAwB,QAAA,iBAAxB,gBAAwB,QAAA,uBAAxB,SAAwB,QAAA,gBAAxB,SAAwB,QAAA,gBAAxB,aAAwB,QAAA,oBAAxB,cAAwB,QAAA,qBAAxB,QAAwB,QAAA,sBAAA,QAAA,eAAxB,eAAwB,QAAA,6BAAA,QAAA,sBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,0BEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBAU9B,aAEI,cAAqB,QAAA,eAArB,gBAAqB,QAAA,iBAArB,sBAAqB,QAAA,uBAArB,eAAqB,QAAA,gBAArB,eAAqB,QAAA,gBAArB,mBAAqB,QAAA,oBAArB,oBAAqB,QAAA,qBAArB,cAAqB,QAAA,sBAAA,QAAA,eAArB,qBAAqB,QAAA,6BAAA,QAAA,uBCbrB,UAAgC,mBAAA,cAAA,eAAA,cAChC,aAAgC,mBAAA,iBAAA,eAAA,iBAChC,kBAAgC,mBAAA,sBAAA,eAAA,sBAChC,qBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,WAA8B,cAAA,eAAA,UAAA,eAC9B,aAA8B,cAAA,iBAAA,UAAA,iBAC9B,mBAA8B,cAAA,uBAAA,UAAA,uBAC9B,WAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAE9B,uBAAoC,cAAA,gBAAA,gBAAA,qBACpC,qBAAoC,cAAA,cAAA,gBAAA,mBACpC,wBAAoC,cAAA,iBAAA,gBAAA,iBACpC,yBAAoC,cAAA,kBAAA,gBAAA,wBACpC,wBAAoC,cAAA,qBAAA,gBAAA,uBAEpC,mBAAiC,eAAA,gBAAA,YAAA,qBACjC,iBAAiC,eAAA,cAAA,YAAA,mBACjC,oBAAiC,eAAA,iBAAA,YAAA,iBACjC,sBAAiC,eAAA,mBAAA,YAAA,mBACjC,qBAAiC,eAAA,kBAAA,YAAA,kBAEjC,qBAAkC,mBAAA,gBAAA,cAAA,qBAClC,mBAAkC,mBAAA,cAAA,cAAA,mBAClC,sBAAkC,mBAAA,iBAAA,cAAA,iBAClC,uBAAkC,mBAAA,kBAAA,cAAA,wBAClC,sBAAkC,mBAAA,qBAAA,cAAA,uBAClC,uBAAkC,mBAAA,kBAAA,cAAA,kBAElC,iBAAgC,oBAAA,eAAA,WAAA,eAChC,kBAAgC,oBAAA,gBAAA,WAAA,qBAChC,gBAAgC,oBAAA,cAAA,WAAA,mBAChC,mBAAgC,oBAAA,iBAAA,WAAA,iBAChC,qBAAgC,oBAAA,mBAAA,WAAA,mBAChC,oBAAgC,oBAAA,kBAAA,WAAA,kBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,0BGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBCtC5B,KAAgC,OAAA,YAChC,MPiiER,MO/hEU,WAAA,YAEF,MPkiER,MOhiEU,aAAA,YAEF,MPmiER,MOjiEU,cAAA,YAEF,MPoiER,MOliEU,YAAA,YAfF,KAAgC,OAAA,iBAChC,MPyjER,MOvjEU,WAAA,iBAEF,MP0jER,MOxjEU,aAAA,iBAEF,MP2jER,MOzjEU,cAAA,iBAEF,MP4jER,MO1jEU,YAAA,iBAfF,KAAgC,OAAA,gBAChC,MPilER,MO/kEU,WAAA,gBAEF,MPklER,MOhlEU,aAAA,gBAEF,MPmlER,MOjlEU,cAAA,gBAEF,MPolER,MOllEU,YAAA,gBAfF,KAAgC,OAAA,eAChC,MPymER,MOvmEU,WAAA,eAEF,MP0mER,MOxmEU,aAAA,eAEF,MP2mER,MOzmEU,cAAA,eAEF,MP4mER,MO1mEU,YAAA,eAfF,KAAgC,OAAA,iBAChC,MPioER,MO/nEU,WAAA,iBAEF,MPkoER,MOhoEU,aAAA,iBAEF,MPmoER,MOjoEU,cAAA,iBAEF,MPooER,MOloEU,YAAA,iBAfF,KAAgC,OAAA,eAChC,MPypER,MOvpEU,WAAA,eAEF,MP0pER,MOxpEU,aAAA,eAEF,MP2pER,MOzpEU,cAAA,eAEF,MP4pER,MO1pEU,YAAA,eAfF,KAAgC,QAAA,YAChC,MPirER,MO/qEU,YAAA,YAEF,MPkrER,MOhrEU,cAAA,YAEF,MPmrER,MOjrEU,eAAA,YAEF,MPorER,MOlrEU,aAAA,YAfF,KAAgC,QAAA,iBAChC,MPysER,MOvsEU,YAAA,iBAEF,MP0sER,MOxsEU,cAAA,iBAEF,MP2sER,MOzsEU,eAAA,iBAEF,MP4sER,MO1sEU,aAAA,iBAfF,KAAgC,QAAA,gBAChC,MPiuER,MO/tEU,YAAA,gBAEF,MPkuER,MOhuEU,cAAA,gBAEF,MPmuER,MOjuEU,eAAA,gBAEF,MPouER,MOluEU,aAAA,gBAfF,KAAgC,QAAA,eAChC,MPyvER,MOvvEU,YAAA,eAEF,MP0vER,MOxvEU,cAAA,eAEF,MP2vER,MOzvEU,eAAA,eAEF,MP4vER,MO1vEU,aAAA,eAfF,KAAgC,QAAA,iBAChC,MPixER,MO/wEU,YAAA,iBAEF,MPkxER,MOhxEU,cAAA,iBAEF,MPmxER,MOjxEU,eAAA,iBAEF,MPoxER,MOlxEU,aAAA,iBAfF,KAAgC,QAAA,eAChC,MPyyER,MOvyEU,YAAA,eAEF,MP0yER,MOxyEU,cAAA,eAEF,MP2yER,MOzyEU,eAAA,eAEF,MP4yER,MO1yEU,aAAA,eAQF,MAAwB,OAAA,kBACxB,OP0yER,OOxyEU,WAAA,kBAEF,OP2yER,OOzyEU,aAAA,kBAEF,OP4yER,OO1yEU,cAAA,kBAEF,OP6yER,OO3yEU,YAAA,kBAfF,MAAwB,OAAA,iBACxB,OPk0ER,OOh0EU,WAAA,iBAEF,OPm0ER,OOj0EU,aAAA,iBAEF,OPo0ER,OOl0EU,cAAA,iBAEF,OPq0ER,OOn0EU,YAAA,iBAfF,MAAwB,OAAA,gBACxB,OP01ER,OOx1EU,WAAA,gBAEF,OP21ER,OOz1EU,aAAA,gBAEF,OP41ER,OO11EU,cAAA,gBAEF,OP61ER,OO31EU,YAAA,gBAfF,MAAwB,OAAA,kBACxB,OPk3ER,OOh3EU,WAAA,kBAEF,OPm3ER,OOj3EU,aAAA,kBAEF,OPo3ER,OOl3EU,cAAA,kBAEF,OPq3ER,OOn3EU,YAAA,kBAfF,MAAwB,OAAA,gBACxB,OP04ER,OOx4EU,WAAA,gBAEF,OP24ER,OOz4EU,aAAA,gBAEF,OP44ER,OO14EU,cAAA,gBAEF,OP64ER,OO34EU,YAAA,gBAMN,QAAmB,OAAA,eACnB,SP64EJ,SO34EM,WAAA,eAEF,SP84EJ,SO54EM,aAAA,eAEF,SP+4EJ,SO74EM,cAAA,eAEF,SPg5EJ,SO94EM,YAAA,eJTF,yBIlDI,QAAgC,OAAA,YAChC,SPi9EN,SO/8EQ,WAAA,YAEF,SPi9EN,SO/8EQ,aAAA,YAEF,SPi9EN,SO/8EQ,cAAA,YAEF,SPi9EN,SO/8EQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPo+EN,SOl+EQ,WAAA,iBAEF,SPo+EN,SOl+EQ,aAAA,iBAEF,SPo+EN,SOl+EQ,cAAA,iBAEF,SPo+EN,SOl+EQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPu/EN,SOr/EQ,WAAA,gBAEF,SPu/EN,SOr/EQ,aAAA,gBAEF,SPu/EN,SOr/EQ,cAAA,gBAEF,SPu/EN,SOr/EQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP0gFN,SOxgFQ,WAAA,eAEF,SP0gFN,SOxgFQ,aAAA,eAEF,SP0gFN,SOxgFQ,cAAA,eAEF,SP0gFN,SOxgFQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP6hFN,SO3hFQ,WAAA,iBAEF,SP6hFN,SO3hFQ,aAAA,iBAEF,SP6hFN,SO3hFQ,cAAA,iBAEF,SP6hFN,SO3hFQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPgjFN,SO9iFQ,WAAA,eAEF,SPgjFN,SO9iFQ,aAAA,eAEF,SPgjFN,SO9iFQ,cAAA,eAEF,SPgjFN,SO9iFQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPmkFN,SOjkFQ,YAAA,YAEF,SPmkFN,SOjkFQ,cAAA,YAEF,SPmkFN,SOjkFQ,eAAA,YAEF,SPmkFN,SOjkFQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPslFN,SOplFQ,YAAA,iBAEF,SPslFN,SOplFQ,cAAA,iBAEF,SPslFN,SOplFQ,eAAA,iBAEF,SPslFN,SOplFQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPymFN,SOvmFQ,YAAA,gBAEF,SPymFN,SOvmFQ,cAAA,gBAEF,SPymFN,SOvmFQ,eAAA,gBAEF,SPymFN,SOvmFQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP4nFN,SO1nFQ,YAAA,eAEF,SP4nFN,SO1nFQ,cAAA,eAEF,SP4nFN,SO1nFQ,eAAA,eAEF,SP4nFN,SO1nFQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP+oFN,SO7oFQ,YAAA,iBAEF,SP+oFN,SO7oFQ,cAAA,iBAEF,SP+oFN,SO7oFQ,eAAA,iBAEF,SP+oFN,SO7oFQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPkqFN,SOhqFQ,YAAA,eAEF,SPkqFN,SOhqFQ,cAAA,eAEF,SPkqFN,SOhqFQ,eAAA,eAEF,SPkqFN,SOhqFQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UP8pFN,UO5pFQ,WAAA,kBAEF,UP8pFN,UO5pFQ,aAAA,kBAEF,UP8pFN,UO5pFQ,cAAA,kBAEF,UP8pFN,UO5pFQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPirFN,UO/qFQ,WAAA,iBAEF,UPirFN,UO/qFQ,aAAA,iBAEF,UPirFN,UO/qFQ,cAAA,iBAEF,UPirFN,UO/qFQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPosFN,UOlsFQ,WAAA,gBAEF,UPosFN,UOlsFQ,aAAA,gBAEF,UPosFN,UOlsFQ,cAAA,gBAEF,UPosFN,UOlsFQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPutFN,UOrtFQ,WAAA,kBAEF,UPutFN,UOrtFQ,aAAA,kBAEF,UPutFN,UOrtFQ,cAAA,kBAEF,UPutFN,UOrtFQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP0uFN,UOxuFQ,WAAA,gBAEF,UP0uFN,UOxuFQ,aAAA,gBAEF,UP0uFN,UOxuFQ,cAAA,gBAEF,UP0uFN,UOxuFQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPwuFF,YOtuFI,WAAA,eAEF,YPwuFF,YOtuFI,aAAA,eAEF,YPwuFF,YOtuFI,cAAA,eAEF,YPwuFF,YOtuFI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SP0yFN,SOxyFQ,WAAA,YAEF,SP0yFN,SOxyFQ,aAAA,YAEF,SP0yFN,SOxyFQ,cAAA,YAEF,SP0yFN,SOxyFQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SP6zFN,SO3zFQ,WAAA,iBAEF,SP6zFN,SO3zFQ,aAAA,iBAEF,SP6zFN,SO3zFQ,cAAA,iBAEF,SP6zFN,SO3zFQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPg1FN,SO90FQ,WAAA,gBAEF,SPg1FN,SO90FQ,aAAA,gBAEF,SPg1FN,SO90FQ,cAAA,gBAEF,SPg1FN,SO90FQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPm2FN,SOj2FQ,WAAA,eAEF,SPm2FN,SOj2FQ,aAAA,eAEF,SPm2FN,SOj2FQ,cAAA,eAEF,SPm2FN,SOj2FQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPs3FN,SOp3FQ,WAAA,iBAEF,SPs3FN,SOp3FQ,aAAA,iBAEF,SPs3FN,SOp3FQ,cAAA,iBAEF,SPs3FN,SOp3FQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPy4FN,SOv4FQ,WAAA,eAEF,SPy4FN,SOv4FQ,aAAA,eAEF,SPy4FN,SOv4FQ,cAAA,eAEF,SPy4FN,SOv4FQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SP45FN,SO15FQ,YAAA,YAEF,SP45FN,SO15FQ,cAAA,YAEF,SP45FN,SO15FQ,eAAA,YAEF,SP45FN,SO15FQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SP+6FN,SO76FQ,YAAA,iBAEF,SP+6FN,SO76FQ,cAAA,iBAEF,SP+6FN,SO76FQ,eAAA,iBAEF,SP+6FN,SO76FQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPk8FN,SOh8FQ,YAAA,gBAEF,SPk8FN,SOh8FQ,cAAA,gBAEF,SPk8FN,SOh8FQ,eAAA,gBAEF,SPk8FN,SOh8FQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPq9FN,SOn9FQ,YAAA,eAEF,SPq9FN,SOn9FQ,cAAA,eAEF,SPq9FN,SOn9FQ,eAAA,eAEF,SPq9FN,SOn9FQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPw+FN,SOt+FQ,YAAA,iBAEF,SPw+FN,SOt+FQ,cAAA,iBAEF,SPw+FN,SOt+FQ,eAAA,iBAEF,SPw+FN,SOt+FQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SP2/FN,SOz/FQ,YAAA,eAEF,SP2/FN,SOz/FQ,cAAA,eAEF,SP2/FN,SOz/FQ,eAAA,eAEF,SP2/FN,SOz/FQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPu/FN,UOr/FQ,WAAA,kBAEF,UPu/FN,UOr/FQ,aAAA,kBAEF,UPu/FN,UOr/FQ,cAAA,kBAEF,UPu/FN,UOr/FQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UP0gGN,UOxgGQ,WAAA,iBAEF,UP0gGN,UOxgGQ,aAAA,iBAEF,UP0gGN,UOxgGQ,cAAA,iBAEF,UP0gGN,UOxgGQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UP6hGN,UO3hGQ,WAAA,gBAEF,UP6hGN,UO3hGQ,aAAA,gBAEF,UP6hGN,UO3hGQ,cAAA,gBAEF,UP6hGN,UO3hGQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPgjGN,UO9iGQ,WAAA,kBAEF,UPgjGN,UO9iGQ,aAAA,kBAEF,UPgjGN,UO9iGQ,cAAA,kBAEF,UPgjGN,UO9iGQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPmkGN,UOjkGQ,WAAA,gBAEF,UPmkGN,UOjkGQ,aAAA,gBAEF,UPmkGN,UOjkGQ,cAAA,gBAEF,UPmkGN,UOjkGQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPikGF,YO/jGI,WAAA,eAEF,YPikGF,YO/jGI,aAAA,eAEF,YPikGF,YO/jGI,cAAA,eAEF,YPikGF,YO/jGI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SPmoGN,SOjoGQ,WAAA,YAEF,SPmoGN,SOjoGQ,aAAA,YAEF,SPmoGN,SOjoGQ,cAAA,YAEF,SPmoGN,SOjoGQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPspGN,SOppGQ,WAAA,iBAEF,SPspGN,SOppGQ,aAAA,iBAEF,SPspGN,SOppGQ,cAAA,iBAEF,SPspGN,SOppGQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPyqGN,SOvqGQ,WAAA,gBAEF,SPyqGN,SOvqGQ,aAAA,gBAEF,SPyqGN,SOvqGQ,cAAA,gBAEF,SPyqGN,SOvqGQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP4rGN,SO1rGQ,WAAA,eAEF,SP4rGN,SO1rGQ,aAAA,eAEF,SP4rGN,SO1rGQ,cAAA,eAEF,SP4rGN,SO1rGQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP+sGN,SO7sGQ,WAAA,iBAEF,SP+sGN,SO7sGQ,aAAA,iBAEF,SP+sGN,SO7sGQ,cAAA,iBAEF,SP+sGN,SO7sGQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPkuGN,SOhuGQ,WAAA,eAEF,SPkuGN,SOhuGQ,aAAA,eAEF,SPkuGN,SOhuGQ,cAAA,eAEF,SPkuGN,SOhuGQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPqvGN,SOnvGQ,YAAA,YAEF,SPqvGN,SOnvGQ,cAAA,YAEF,SPqvGN,SOnvGQ,eAAA,YAEF,SPqvGN,SOnvGQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPwwGN,SOtwGQ,YAAA,iBAEF,SPwwGN,SOtwGQ,cAAA,iBAEF,SPwwGN,SOtwGQ,eAAA,iBAEF,SPwwGN,SOtwGQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SP2xGN,SOzxGQ,YAAA,gBAEF,SP2xGN,SOzxGQ,cAAA,gBAEF,SP2xGN,SOzxGQ,eAAA,gBAEF,SP2xGN,SOzxGQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP8yGN,SO5yGQ,YAAA,eAEF,SP8yGN,SO5yGQ,cAAA,eAEF,SP8yGN,SO5yGQ,eAAA,eAEF,SP8yGN,SO5yGQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPi0GN,SO/zGQ,YAAA,iBAEF,SPi0GN,SO/zGQ,cAAA,iBAEF,SPi0GN,SO/zGQ,eAAA,iBAEF,SPi0GN,SO/zGQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPo1GN,SOl1GQ,YAAA,eAEF,SPo1GN,SOl1GQ,cAAA,eAEF,SPo1GN,SOl1GQ,eAAA,eAEF,SPo1GN,SOl1GQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPg1GN,UO90GQ,WAAA,kBAEF,UPg1GN,UO90GQ,aAAA,kBAEF,UPg1GN,UO90GQ,cAAA,kBAEF,UPg1GN,UO90GQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPm2GN,UOj2GQ,WAAA,iBAEF,UPm2GN,UOj2GQ,aAAA,iBAEF,UPm2GN,UOj2GQ,cAAA,iBAEF,UPm2GN,UOj2GQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPs3GN,UOp3GQ,WAAA,gBAEF,UPs3GN,UOp3GQ,aAAA,gBAEF,UPs3GN,UOp3GQ,cAAA,gBAEF,UPs3GN,UOp3GQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPy4GN,UOv4GQ,WAAA,kBAEF,UPy4GN,UOv4GQ,aAAA,kBAEF,UPy4GN,UOv4GQ,cAAA,kBAEF,UPy4GN,UOv4GQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP45GN,UO15GQ,WAAA,gBAEF,UP45GN,UO15GQ,aAAA,gBAEF,UP45GN,UO15GQ,cAAA,gBAEF,UP45GN,UO15GQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YP05GF,YOx5GI,WAAA,eAEF,YP05GF,YOx5GI,aAAA,eAEF,YP05GF,YOx5GI,cAAA,eAEF,YP05GF,YOx5GI,YAAA,gBJTF,0BIlDI,QAAgC,OAAA,YAChC,SP49GN,SO19GQ,WAAA,YAEF,SP49GN,SO19GQ,aAAA,YAEF,SP49GN,SO19GQ,cAAA,YAEF,SP49GN,SO19GQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SP++GN,SO7+GQ,WAAA,iBAEF,SP++GN,SO7+GQ,aAAA,iBAEF,SP++GN,SO7+GQ,cAAA,iBAEF,SP++GN,SO7+GQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPkgHN,SOhgHQ,WAAA,gBAEF,SPkgHN,SOhgHQ,aAAA,gBAEF,SPkgHN,SOhgHQ,cAAA,gBAEF,SPkgHN,SOhgHQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPqhHN,SOnhHQ,WAAA,eAEF,SPqhHN,SOnhHQ,aAAA,eAEF,SPqhHN,SOnhHQ,cAAA,eAEF,SPqhHN,SOnhHQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPwiHN,SOtiHQ,WAAA,iBAEF,SPwiHN,SOtiHQ,aAAA,iBAEF,SPwiHN,SOtiHQ,cAAA,iBAEF,SPwiHN,SOtiHQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SP2jHN,SOzjHQ,WAAA,eAEF,SP2jHN,SOzjHQ,aAAA,eAEF,SP2jHN,SOzjHQ,cAAA,eAEF,SP2jHN,SOzjHQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SP8kHN,SO5kHQ,YAAA,YAEF,SP8kHN,SO5kHQ,cAAA,YAEF,SP8kHN,SO5kHQ,eAAA,YAEF,SP8kHN,SO5kHQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPimHN,SO/lHQ,YAAA,iBAEF,SPimHN,SO/lHQ,cAAA,iBAEF,SPimHN,SO/lHQ,eAAA,iBAEF,SPimHN,SO/lHQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPonHN,SOlnHQ,YAAA,gBAEF,SPonHN,SOlnHQ,cAAA,gBAEF,SPonHN,SOlnHQ,eAAA,gBAEF,SPonHN,SOlnHQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPuoHN,SOroHQ,YAAA,eAEF,SPuoHN,SOroHQ,cAAA,eAEF,SPuoHN,SOroHQ,eAAA,eAEF,SPuoHN,SOroHQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP0pHN,SOxpHQ,YAAA,iBAEF,SP0pHN,SOxpHQ,cAAA,iBAEF,SP0pHN,SOxpHQ,eAAA,iBAEF,SP0pHN,SOxpHQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SP6qHN,SO3qHQ,YAAA,eAEF,SP6qHN,SO3qHQ,cAAA,eAEF,SP6qHN,SO3qHQ,eAAA,eAEF,SP6qHN,SO3qHQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPyqHN,UOvqHQ,WAAA,kBAEF,UPyqHN,UOvqHQ,aAAA,kBAEF,UPyqHN,UOvqHQ,cAAA,kBAEF,UPyqHN,UOvqHQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UP4rHN,UO1rHQ,WAAA,iBAEF,UP4rHN,UO1rHQ,aAAA,iBAEF,UP4rHN,UO1rHQ,cAAA,iBAEF,UP4rHN,UO1rHQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UP+sHN,UO7sHQ,WAAA,gBAEF,UP+sHN,UO7sHQ,aAAA,gBAEF,UP+sHN,UO7sHQ,cAAA,gBAEF,UP+sHN,UO7sHQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPkuHN,UOhuHQ,WAAA,kBAEF,UPkuHN,UOhuHQ,aAAA,kBAEF,UPkuHN,UOhuHQ,cAAA,kBAEF,UPkuHN,UOhuHQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPqvHN,UOnvHQ,WAAA,gBAEF,UPqvHN,UOnvHQ,aAAA,gBAEF,UPqvHN,UOnvHQ,cAAA,gBAEF,UPqvHN,UOnvHQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPmvHF,YOjvHI,WAAA,eAEF,YPmvHF,YOjvHI,aAAA,eAEF,YPmvHF,YOjvHI,cAAA,eAEF,YPmvHF,YOjvHI,YAAA","sourcesContent":["/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 0000000..91b0fc4 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,327 @@ +/*! + * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css.map b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css.map new file mode 100644 index 0000000..701f671 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","bootstrap-reboot.css","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ECME;ACYF;;;EAGE,sBAAsB;ADVxB;;ACaA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AFCf;;ACgBA;EACE,cAAc;ADbhB;;ACuBA;EACE,SAAS;EACT,kMCyOiN;ECzJ7M,eAtCY;EFxChB,gBCkP+B;EDjP/B,gBCsP+B;EDrP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AF0Bf;;AAEA;EC+BE,qBAAqB;AD7BvB;;ACsCA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;ADnCnB;;ACgDA;EACE,aAAa;EACb,qBCoNuC;AFjQzC;;ACoDA;EACE,aAAa;EACb,mBCuF8B;AFxIhC;;AC4DA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;ADzDhC;;AC4DA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;ADzDtB;;AC4DA;;;EAGE,aAAa;EACb,mBAAmB;ADzDrB;;AC4DA;;;;EAIE,gBAAgB;ADzDlB;;AC4DA;EACE,gBCqJ+B;AF9MjC;;AC4DA;EACE,oBAAoB;EACpB,cAAc;ADzDhB;;AC4DA;EACE,gBAAgB;ADzDlB;;AC4DA;;EAEE,mBCwIkC;AFjMpC;;AC4DA;EExFI,cAAW;AHgCf;;ACiEA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AD9D1B;;ACiEA;EAAM,cAAc;AD7DpB;;AC8DA;EAAM,UAAU;AD1DhB;;ACiEA;EACE,cCtJe;EDuJf,qBCR4C;EDS5C,6BAA6B;AD9D/B;;AIlHE;EHmLE,cCX8D;EDY9D,0BCX+C;AFlDnD;;ACsEA;EACE,cAAc;EACd,qBAAqB;ADnEvB;;AI5HE;EHkME,cAAc;EACd,qBAAqB;ADlEzB;;AC2EA;;;;EAIE,iGC6DgH;ECjN9G,cAAW;AH6Ef;;AC2EA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;AD3EhB;;ACmFA;EAEE,gBAAgB;ADjFlB;;ACyFA;EACE,sBAAsB;EACtB,kBAAkB;ADtFpB;;ACyFA;EAGE,gBAAgB;EAChB,sBAAsB;ADxFxB;;ACgGA;EACE,yBAAyB;AD7F3B;;ACgGA;EACE,oBCoFkC;EDnFlC,uBCmFkC;EDlFlC,cCnQgB;EDoQhB,gBAAgB;EAChB,oBAAoB;AD7FtB;;ACgGA;EAGE,mBAAmB;AD/FrB;;ACuGA;EAEE,qBAAqB;EACrB,qBCqK2C;AF1Q7C;;AC2GA;EAEE,gBAAgB;ADzGlB;;ACgHA;EACE,mBAAmB;EACnB,0CAA0C;AD7G5C;;ACgHA;;;;;EAKE,SAAS;EACT,oBAAoB;EErPlB,kBAAW;EFuPb,oBAAoB;AD7GtB;;ACgHA;;EAEE,iBAAiB;AD7GnB;;ACgHA;;EAEE,oBAAoB;AD7GtB;;ACmHA;EACE,iBAAiB;ADhHnB;;ACuHA;;;;EAIE,0BAA0B;ADpH5B;;ACyHE;;;;EAKI,eAAe;ADvHrB;;AC6HA;;;;EAIE,UAAU;EACV,kBAAkB;AD1HpB;;AC6HA;;EAEE,sBAAsB;EACtB,UAAU;AD1HZ;;AC8HA;;;;EASE,2BAA2B;ADhI7B;;ACmIA;EACE,cAAc;EAEd,gBAAgB;ADjIlB;;ACoIA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;ADvIX;;AC4IA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEjShB,iBAtCY;EFyUhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;ADzIrB;;AC4IA;EACE,wBAAwB;ADzI1B;;AAEA;;EC6IE,YAAY;AD1Id;;AAEA;ECgJE,oBAAoB;EACpB,wBAAwB;AD9I1B;;AAEA;ECoJE,wBAAwB;ADlJ1B;;AC0JA;EACE,aAAa;EACb,0BAA0B;ADvJ5B;;AC8JA;EACE,qBAAqB;AD3JvB;;AC8JA;EACE,kBAAkB;EAClB,eAAe;AD3JjB;;AC8JA;EACE,aAAa;AD3Jf;;AAEA;EC+JE,wBAAwB;AD7J1B","file":"bootstrap-reboot.css","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css new file mode 100644 index 0000000..5308df6 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css @@ -0,0 +1,8 @@ +/*! + * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} +/*# sourceMappingURL=bootstrap-reboot.min.css.map */ \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css.map b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css.map new file mode 100644 index 0000000..b8551f7 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap-reboot.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,cACE,MAAA,QACA,gBAAA,KI/LA,oBJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBCxGF,OD2GA,MCzGA,SADA,OAEA,SD6GE,OAAA,EACA,YAAA,QErPE,UAAA,QFuPF,YAAA,QAGF,OC3GA,MD6GE,SAAA,QAGF,OC3GA,OD6GE,eAAA,KAMF,OACE,UAAA,OC3GF,cACA,aACA,cDgHA,OAIE,mBAAA,OC/GF,6BACA,4BACA,6BDkHE,sBAKI,OAAA,QClHN,gCACA,+BACA,gCDsHA,yBAIE,QAAA,EACA,aAAA,KCrHF,qBDwHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCxHA,2BACA,kBAFA,iBDkIE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MEjSI,UAAA,OFmSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGvIF,yCFGA,yCD0IE,OAAA,KGxIF,cHgJE,eAAA,KACA,mBAAA,KG5IF,yCHoJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGzJF,SH+JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css new file mode 100644 index 0000000..7efc139 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css @@ -0,0 +1,10225 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1, .h1 { + font-size: 2.5rem; +} + +h2, .h2 { + font-size: 2rem; +} + +h3, .h3 { + font-size: 1.75rem; +} + +h4, .h4 { + font-size: 1.5rem; +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #6c757d; +} + +.blockquote-footer::before { + content: "\2014\00A0"; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #6c757d; +} + +code { + font-size: 87.5%; + color: #e83e8c; + word-wrap: break-word; +} + +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + font-size: 87.5%; + color: #212529; +} + +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid, .container-sm, .container-md, .container-lg, .container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +.table tbody + tbody { + border-top: 2px solid #dee2e6; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + border-color: #7abaff; +} + +.table-hover .table-primary:hover { + background-color: #9fcdff; +} + +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #9fcdff; +} + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + border-color: #b3b7bb; +} + +.table-hover .table-secondary:hover { + background-color: #c8cbcf; +} + +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #c8cbcf; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + border-color: #8fd19e; +} + +.table-hover .table-success:hover { + background-color: #b1dfbb; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #b1dfbb; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + border-color: #86cfda; +} + +.table-hover .table-info:hover { + background-color: #abdde5; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #abdde5; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + border-color: #ffdf7e; +} + +.table-hover .table-warning:hover { + background-color: #ffe8a1; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #ffe8a1; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + border-color: #ed969e; +} + +.table-hover .table-danger:hover { + background-color: #f1b0b7; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #f1b0b7; +} + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + border-color: #fbfcfc; +} + +.table-hover .table-light:hover { + background-color: #ececf6; +} + +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #ececf6; +} + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + border-color: #95999c; +} + +.table-hover .table-dark:hover { + background-color: #b9bbbe; +} + +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #b9bbbe; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); +} + +.table .thead-dark th { + color: #fff; + background-color: #343a40; + border-color: #454d55; +} + +.table .thead-light th { + color: #495057; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.table-dark { + color: #fff; + background-color: #343a40; +} + +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #454d55; +} + +.table-dark.table-bordered { + border: 0; +} + +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.075); +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-sm > .table-bordered { + border: 0; + } +} + +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-md > .table-bordered { + border: 0; + } +} + +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-lg > .table-bordered { + border: 0; + } +} + +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-xl > .table-bordered { + border: 0; + } +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive > .table-bordered { + border: 0; +} + +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control::-webkit-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::-moz-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::-ms-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; +} + +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + font-size: 1rem; + line-height: 1.5; + color: #212529; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} + +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.25rem; +} + +.form-row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; +} + +.form-row > .col, +.form-row > [class*="col-"] { + padding-right: 5px; + padding-left: 5px; +} + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} + +.form-check-input[disabled] ~ .form-check-label, +.form-check-input:disabled ~ .form-check-label { + color: #6c757d; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-inline { + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-align: center; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.form-inline { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; +} + +.form-inline .form-check { + width: 100%; +} + +@media (min-width: 576px) { + .form-inline label { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: -ms-flexbox; + display: flex; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group, + .form-inline .custom-select { + width: auto; + } + .form-inline .form-check { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} + +.btn:hover { + color: #212529; + text-decoration: none; +} + +.btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.65; +} + +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; +} + +.btn-primary:focus, .btn-primary.focus { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, +.show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0062cc; + border-color: #005cbf; +} + +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + color: #fff; + background-color: #5a6268; + border-color: #545b62; +} + +.btn-secondary:focus, .btn-secondary.focus { + color: #fff; + background-color: #5a6268; + border-color: #545b62; + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} + +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, +.show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #545b62; + border-color: #4e555b; +} + +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} + +.btn-success { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + color: #fff; + background-color: #218838; + border-color: #1e7e34; +} + +.btn-success:focus, .btn-success.focus { + color: #fff; + background-color: #218838; + border-color: #1e7e34; + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); +} + +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #1e7e34; + border-color: #1c7430; +} + +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); +} + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; +} + +.btn-info:focus, .btn-info.focus { + color: #fff; + background-color: #138496; + border-color: #117a8b; + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, +.show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #117a8b; + border-color: #10707f; +} + +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; +} + +.btn-warning:focus, .btn-warning.focus { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.btn-warning.disabled, .btn-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, +.show > .btn-warning.dropdown-toggle { + color: #212529; + background-color: #d39e00; + border-color: #c69500; +} + +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + color: #fff; + background-color: #c82333; + border-color: #bd2130; +} + +.btn-danger:focus, .btn-danger.focus { + color: #fff; + background-color: #c82333; + border-color: #bd2130; + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); +} + +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, +.show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #bd2130; + border-color: #b21f2d; +} + +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); +} + +.btn-light { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:hover { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; +} + +.btn-light:focus, .btn-light.focus { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); +} + +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, +.show > .btn-light.dropdown-toggle { + color: #212529; + background-color: #dae0e5; + border-color: #d3d9df; +} + +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); +} + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:hover { + color: #fff; + background-color: #23272b; + border-color: #1d2124; +} + +.btn-dark:focus, .btn-dark.focus { + color: #fff; + background-color: #23272b; + border-color: #1d2124; + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} + +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, +.show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1d2124; + border-color: #171a1d; +} + +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} + +.btn-outline-primary { + color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #007bff; + background-color: transparent; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-success { + color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #28a745; + background-color: transparent; +} + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-info { + color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; +} + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-warning { + color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-danger { + color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #dc3545; + background-color: transparent; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-light { + color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f8f9fa; + background-color: transparent; +} + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-dark { + color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-link { + font-weight: 400; + color: #007bff; + text-decoration: none; +} + +.btn-link:hover { + color: #0056b3; + text-decoration: underline; +} + +.btn-link:focus, .btn-link.focus { + text-decoration: underline; + box-shadow: none; +} + +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; + pointer-events: none; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.15s linear; +} + +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} + +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-left { + right: auto; + left: 0; + } + .dropdown-menu-sm-right { + right: 0; + left: auto; + } +} + +@media (min-width: 768px) { + .dropdown-menu-md-left { + right: auto; + left: 0; + } + .dropdown-menu-md-right { + right: 0; + left: auto; + } +} + +@media (min-width: 992px) { + .dropdown-menu-lg-left { + right: auto; + left: 0; + } + .dropdown-menu-lg-right { + right: 0; + left: auto; + } +} + +@media (min-width: 1200px) { + .dropdown-menu-xl-left { + right: auto; + left: 0; + } + .dropdown-menu-xl-right { + right: 0; + left: auto; + } +} + +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} + +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} + +.dropright .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-toggle::after { + vertical-align: 0; +} + +.dropleft .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.1rem 0.75rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + color: #16181b; + text-decoration: none; + background-color: #f8f9fa; +} + +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #007bff; +} + +.dropdown-item.disabled, .dropdown-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + font-weight: 900; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 1rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: center; + justify-content: center; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; +} + +.btn-group-toggle > .btn input[type="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: stretch; + align-items: stretch; + width: 100%; +} + +.input-group > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 0%; + flex: 1 1 0%; + min-width: 0; + margin-bottom: 0; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group > .custom-file { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; +} + +.input-group > .custom-file:not(:last-child) .custom-file-label, +.input-group > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .custom-file:not(:first-child) .custom-file-label { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; +} + +.input-group-prepend { + margin-right: -1px; +} + +.input-group-append { + margin-left: -1px; +} + +.input-group-text { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.custom-control { + position: relative; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; +} + +.custom-control-inline { + display: -ms-inline-flexbox; + display: inline-flex; + margin-right: 1rem; +} + +.custom-control-input { + position: absolute; + left: 0; + z-index: -1; + width: 1rem; + height: 1.25rem; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #007bff; + background-color: #007bff; +} + +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-control-input:focus:not(:checked) ~ .custom-control-label::before { + border-color: #80bdff; +} + +.custom-control-input:not(:disabled):active ~ .custom-control-label::before { + color: #fff; + background-color: #b3d7ff; + border-color: #b3d7ff; +} + +.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label { + color: #6c757d; +} + +.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before { + background-color: #e9ecef; +} + +.custom-control-label { + position: relative; + margin-bottom: 0; + vertical-align: top; +} + +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + background-color: #fff; + border: #adb5bd solid 1px; +} + +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + content: ""; + background: no-repeat 50% / 50% 50%; +} + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e"); +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + border-color: #007bff; + background-color: #007bff; +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e"); +} + +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-radio .custom-control-label::before { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} + +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .custom-switch .custom-control-label::after { + transition: none; + } +} + +.custom-switch .custom-control-input:checked ~ .custom-control-label::after { + background-color: #fff; + -webkit-transform: translateX(0.75rem); + transform: translateX(0.75rem); +} + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + vertical-align: middle; + background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.75rem; + background-image: none; +} + +.custom-select:disabled { + color: #6c757d; + background-color: #e9ecef; +} + +.custom-select::-ms-expand { + display: none; +} + +.custom-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} + +.custom-select-sm { + height: calc(1.5em + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-file-input[disabled] ~ .custom-file-label, +.custom-file-input:disabled ~ .custom-file-label { + background-color: #e9ecef; +} + +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; +} + +.custom-file-input ~ .custom-file-label[data-browse]::after { + content: attr(data-browse); +} + +.custom-file-label { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + height: 1.4rem; + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-range:focus { + outline: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; + } +} + +.custom-range::-webkit-slider-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -moz-appearance: none; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } +} + +.custom-range::-moz-range-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-ms-thumb { + width: 1rem; + height: 1rem; + margin-top: 0; + margin-right: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-ms-thumb { + -ms-transition: none; + transition: none; + } +} + +.custom-range::-ms-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-ms-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.5rem; +} + +.custom-range::-ms-fill-lower { + background-color: #dee2e6; + border-radius: 1rem; +} + +.custom-range::-ms-fill-upper { + margin-right: 15px; + background-color: #dee2e6; + border-radius: 1rem; +} + +.custom-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; +} + +.custom-range:disabled::-webkit-slider-runnable-track { + cursor: default; +} + +.custom-range:disabled::-moz-range-thumb { + background-color: #adb5bd; +} + +.custom-range:disabled::-moz-range-track { + cursor: default; +} + +.custom-range:disabled::-ms-thumb { + background-color: #adb5bd; +} + +.custom-control-label::before, +.custom-file-label, +.custom-select { + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .custom-control-label::before, + .custom-file-label, + .custom-select { + transition: none; + } +} + +.nav { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + text-decoration: none; +} + +.nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; +} + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-item { + margin-bottom: -1px; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #007bff; +} + +.nav-fill .nav-item { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: center; +} + +.nav-justified .nav-item { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + text-align: center; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0.5rem 1rem; +} + +.navbar .container, +.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.navbar-brand { + display: inline-block; + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; +} + +.navbar-nav { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} + +.navbar-nav .dropdown-menu { + position: static; + float: none; +} + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-align: center; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + text-decoration: none; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} + +@media (max-width: 767.98px) { + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 768px) { + .navbar-expand-md { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } +} + +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 992px) { + .navbar-expand-lg { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } +} + +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 1200px) { + .navbar-expand-xl { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } +} + +.navbar-expand { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + padding-right: 0; + padding-left: 0; +} + +.navbar-expand .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; +} + +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} + +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.navbar-expand .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; +} + +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); +} + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 1px; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img, +.card-img-top, +.card-img-bottom { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-deck .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-deck { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } +} + +.card-group > .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-group { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + } + .card-group > .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} + +@media (min-width: 576px) { + .card-columns { + -webkit-column-count: 3; + -moz-column-count: 3; + column-count: 3; + -webkit-column-gap: 1.25rem; + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; + orphans: 1; + widows: 1; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.accordion > .card { + overflow: hidden; +} + +.accordion > .card:not(:last-of-type) { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.accordion > .card:not(:first-of-type) { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.accordion > .card > .card-header { + border-radius: 0; + margin-bottom: -1px; +} + +.breadcrumb { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + color: #6c757d; + content: "/"; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: -ms-flexbox; + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #007bff; + background-color: #fff; + border: 1px solid #dee2e6; +} + +.page-link:hover { + z-index: 2; + color: #0056b3; + text-decoration: none; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.page-link:focus { + z-index: 3; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + cursor: auto; + background-color: #fff; + border-color: #dee2e6; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} + +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; +} + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; +} + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; +} + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; +} + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.alert-primary { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} + +.alert-primary hr { + border-top-color: #9fcdff; +} + +.alert-primary .alert-link { + color: #002752; +} + +.alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; +} + +.alert-secondary hr { + border-top-color: #c8cbcf; +} + +.alert-secondary .alert-link { + color: #202326; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-success hr { + border-top-color: #b1dfbb; +} + +.alert-success .alert-link { + color: #0b2e13; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} + +.alert-info hr { + border-top-color: #abdde5; +} + +.alert-info .alert-link { + color: #062c33; +} + +.alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} + +.alert-warning hr { + border-top-color: #ffe8a1; +} + +.alert-warning .alert-link { + color: #533f03; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-danger hr { + border-top-color: #f1b0b7; +} + +.alert-danger .alert-link { + color: #491217; +} + +.alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; +} + +.alert-light hr { + border-top-color: #ececf6; +} + +.alert-light .alert-link { + color: #686868; +} + +.alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; +} + +.alert-dark hr { + border-top-color: #b9bbbe; +} + +.alert-dark .alert-link { + color: #040505; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: -ms-flexbox; + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.progress-bar { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #007bff; + transition: width 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + -webkit-animation: progress-bar-stripes 1s linear infinite; + animation: progress-bar-stripes 1s linear infinite; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + -webkit-animation: none; + animation: none; + } +} + +.media { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; +} + +.media-body { + -ms-flex: 1; + flex: 1; +} + +.list-group { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} + +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: #fff; +} + +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.list-group-item + .list-group-item { + border-top-width: 0; +} + +.list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; +} + +.list-group-horizontal { + -ms-flex-direction: row; + flex-direction: row; +} + +.list-group-horizontal .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; +} + +.list-group-horizontal .list-group-item.active { + margin-top: 0; +} + +.list-group-horizontal .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; +} + +.list-group-horizontal .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-sm .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-sm .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 768px) { + .list-group-horizontal-md { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-md .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-md .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-md .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 992px) { + .list-group-horizontal-lg { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-lg .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-lg .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 1200px) { + .list-group-horizontal-xl { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-xl .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xl .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +.list-group-flush .list-group-item { + border-right-width: 0; + border-left-width: 0; + border-radius: 0; +} + +.list-group-flush .list-group-item:first-child { + border-top-width: 0; +} + +.list-group-flush:last-child .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + color: #004085; + background-color: #b8daff; +} + +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #004085; + background-color: #9fcdff; +} + +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #004085; + border-color: #004085; +} + +.list-group-item-secondary { + color: #383d41; + background-color: #d6d8db; +} + +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #383d41; + background-color: #c8cbcf; +} + +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #383d41; + border-color: #383d41; +} + +.list-group-item-success { + color: #155724; + background-color: #c3e6cb; +} + +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #155724; + background-color: #b1dfbb; +} + +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #155724; + border-color: #155724; +} + +.list-group-item-info { + color: #0c5460; + background-color: #bee5eb; +} + +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #0c5460; + background-color: #abdde5; +} + +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #0c5460; + border-color: #0c5460; +} + +.list-group-item-warning { + color: #856404; + background-color: #ffeeba; +} + +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #856404; + background-color: #ffe8a1; +} + +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #856404; + border-color: #856404; +} + +.list-group-item-danger { + color: #721c24; + background-color: #f5c6cb; +} + +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #721c24; + background-color: #f1b0b7; +} + +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #721c24; + border-color: #721c24; +} + +.list-group-item-light { + color: #818182; + background-color: #fdfdfe; +} + +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #818182; + background-color: #ececf6; +} + +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #818182; + border-color: #818182; +} + +.list-group-item-dark { + color: #1b1e21; + background-color: #c6c8ca; +} + +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1b1e21; + background-color: #b9bbbe; +} + +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1b1e21; + border-color: #1b1e21; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; +} + +.close:hover { + color: #000; + text-decoration: none; +} + +.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { + opacity: .75; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +a.close.disabled { + pointer-events: none; +} + +.toast { + max-width: 350px; + overflow: hidden; + font-size: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + border-radius: 0.25rem; +} + +.toast:not(:last-child) { + margin-bottom: 0.75rem; +} + +.toast.showing { + opacity: 1; +} + +.toast.show { + display: block; + opacity: 1; +} + +.toast.hide { + display: none; +} + +.toast-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.toast-body { + padding: 0.75rem; +} + +.modal-open { + overflow: hidden; +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + display: none; + width: 100%; + height: 100%; + overflow: hidden; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; + -webkit-transform: translate(0, -50px); + transform: translate(0, -50px); +} + +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} + +.modal.show .modal-dialog { + -webkit-transform: none; + transform: none; +} + +.modal.modal-static .modal-dialog { + -webkit-transform: scale(1.02); + transform: scale(1.02); +} + +.modal-dialog-scrollable { + display: -ms-flexbox; + display: flex; + max-height: calc(100% - 1rem); +} + +.modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 1rem); + overflow: hidden; +} + +.modal-dialog-scrollable .modal-header, +.modal-dialog-scrollable .modal-footer { + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + min-height: calc(100% - 1rem); +} + +.modal-dialog-centered::before { + display: block; + height: calc(100vh - 1rem); + content: ""; +} + +.modal-dialog-centered.modal-dialog-scrollable { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + height: 100%; +} + +.modal-dialog-centered.modal-dialog-scrollable .modal-content { + max-height: none; +} + +.modal-dialog-centered.modal-dialog-scrollable::before { + content: none; +} + +.modal-content { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #dee2e6; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.modal-header .close { + padding: 1rem 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); +} + +.modal-footer > * { + margin: 0.25rem; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + .modal-dialog-scrollable { + max-height: calc(100% - 3.5rem); + } + .modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 3.5rem); + } + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); + } + .modal-dialog-centered::before { + height: calc(100vh - 3.5rem); + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + max-width: 800px; + } +} + +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc(-0.5rem - 1px); +} + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc(-0.5rem - 1px); +} + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.75rem; + color: #212529; +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + -ms-touch-action: pan-y; + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + -webkit-transform: none; + transform: none; +} + +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; +} + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: no-repeat 50% / 100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 15; + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; +} + +.carousel-indicators li { + box-sizing: content-box; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: .5; + transition: opacity 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-indicators li { + transition: none; + } +} + +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +@-webkit-keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: spinner-border .75s linear infinite; + animation: spinner-border .75s linear infinite; +} + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +@-webkit-keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +@keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + -webkit-animation: spinner-grow .75s linear infinite; + animation: spinner-grow .75s linear infinite; +} + +.spinner-grow-sm { + width: 1rem; + height: 1rem; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.bg-primary { + background-color: #007bff !important; +} + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.border-top { + border-top: 1px solid #dee2e6 !important; +} + +.border-right { + border-right: 1px solid #dee2e6 !important; +} + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-left { + border-left: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-left-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #007bff !important; +} + +.border-secondary { + border-color: #6c757d !important; +} + +.border-success { + border-color: #28a745 !important; +} + +.border-info { + border-color: #17a2b8 !important; +} + +.border-warning { + border-color: #ffc107 !important; +} + +.border-danger { + border-color: #dc3545 !important; +} + +.border-light { + border-color: #f8f9fa !important; +} + +.border-dark { + border-color: #343a40 !important; +} + +.border-white { + border-color: #fff !important; +} + +.rounded-sm { + border-radius: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.3rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: 50rem !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} + +.embed-responsive::before { + display: block; + content: ""; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9::before { + padding-top: 42.857143%; +} + +.embed-responsive-16by9::before { + padding-top: 56.25%; +} + +.embed-responsive-4by3::before { + padding-top: 75%; +} + +.embed-responsive-1by1::before { + padding-top: 100%; +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + .float-sm-right { + float: right !important; + } + .float-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + .float-md-right { + float: right !important; + } + .float-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + .float-lg-right { + float: right !important; + } + .float-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + .float-xl-right { + float: right !important; + } + .float-xl-none { + float: none !important; + } +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports ((position: -webkit-sticky) or (position: sticky)) { + .sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.vw-100 { + width: 100vw !important; +} + +.vh-100 { + height: 100vh !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + pointer-events: auto; + content: ""; + background-color: rgba(0, 0, 0, 0); +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} + +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; +} + +.text-justify { + text-align: justify !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-light { + font-weight: 300 !important; +} + +.font-weight-lighter { + font-weight: lighter !important; +} + +.font-weight-normal { + font-weight: 400 !important; +} + +.font-weight-bold { + font-weight: 700 !important; +} + +.font-weight-bolder { + font-weight: bolder !important; +} + +.font-italic { + font-style: italic !important; +} + +.text-white { + color: #fff !important; +} + +.text-primary { + color: #007bff !important; +} + +a.text-primary:hover, a.text-primary:focus { + color: #0056b3 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-break { + word-break: break-word !important; + overflow-wrap: break-word !important; +} + +.text-reset { + color: inherit !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + box-shadow: none !important; + } + a:not(.btn) { + text-decoration: underline; + } + abbr[title]::after { + content: " (" attr(title) ")"; + } + pre { + white-space: pre-wrap !important; + } + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + @page { + size: a3; + } + body { + min-width: 992px !important; + } + .container { + min-width: 992px !important; + } + .navbar { + display: none; + } + .badge { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #dee2e6 !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; + } + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css.map b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css.map new file mode 100644 index 0000000..521afc5 --- /dev/null +++ b/Rms.Risk.Mango/wwwroot/bootstrap/dist/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","bootstrap.css","../../scss/_root.scss","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_functions.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ACJF;EAGI,eAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,eAAc;EAAd,cAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAAd,eAAc;EAAd,aAAc;EAAd,eAAc;EAAd,oBAAc;EAId,kBAAc;EAAd,oBAAc;EAAd,kBAAc;EAAd,eAAc;EAAd,kBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAId,kBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,uBAAiC;EAKnC,+MAAyB;EACzB,6GAAwB;ADiB1B;;AEjBA;;;EAGE,sBAAsB;AFoBxB;;AEjBA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AH+Bf;;AEdA;EACE,cAAc;AFiBhB;;AEPA;EACE,SAAS;EACT,kMCyOiN;ECzJ7M,eAtCY;EFxChB,gBCkP+B;EDjP/B,gBCsP+B;EDrP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AHwDf;;AAEA;EECE,qBAAqB;AFCvB;;AEQA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;AFLnB;;AEkBA;EACE,aAAa;EACb,qBCoNuC;AHnOzC;;AEsBA;EACE,aAAa;EACb,mBCuF8B;AH1GhC;;AE8BA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;AF3BhC;;AE8BA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;AF3BtB;;AE8BA;;;EAGE,aAAa;EACb,mBAAmB;AF3BrB;;AE8BA;;;;EAIE,gBAAgB;AF3BlB;;AE8BA;EACE,gBCqJ+B;AHhLjC;;AE8BA;EACE,oBAAoB;EACpB,cAAc;AF3BhB;;AE8BA;EACE,gBAAgB;AF3BlB;;AE8BA;;EAEE,mBCwIkC;AHnKpC;;AE8BA;EExFI,cAAW;AJ8Df;;AEmCA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AFhC1B;;AEmCA;EAAM,cAAc;AF/BpB;;AEgCA;EAAM,UAAU;AF5BhB;;AEmCA;EACE,cCtJe;EDuJf,qBCR4C;EDS5C,6BAA6B;AFhC/B;;AKhJE;EHmLE,cCX8D;EDY9D,0BCX+C;AHpBnD;;AEwCA;EACE,cAAc;EACd,qBAAqB;AFrCvB;;AK1JE;EHkME,cAAc;EACd,qBAAqB;AFpCzB;;AE6CA;;;;EAIE,iGC6DgH;ECjN9G,cAAW;AJ2Gf;;AE6CA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;AF7ChB;;AEqDA;EAEE,gBAAgB;AFnDlB;;AE2DA;EACE,sBAAsB;EACtB,kBAAkB;AFxDpB;;AE2DA;EAGE,gBAAgB;EAChB,sBAAsB;AF1DxB;;AEkEA;EACE,yBAAyB;AF/D3B;;AEkEA;EACE,oBCoFkC;EDnFlC,uBCmFkC;EDlFlC,cCnQgB;EDoQhB,gBAAgB;EAChB,oBAAoB;AF/DtB;;AEkEA;EAGE,mBAAmB;AFjErB;;AEyEA;EAEE,qBAAqB;EACrB,qBCqK2C;AH5O7C;;AE6EA;EAEE,gBAAgB;AF3ElB;;AEkFA;EACE,mBAAmB;EACnB,0CAA0C;AF/E5C;;AEkFA;;;;;EAKE,SAAS;EACT,oBAAoB;EErPlB,kBAAW;EFuPb,oBAAoB;AF/EtB;;AEkFA;;EAEE,iBAAiB;AF/EnB;;AEkFA;;EAEE,oBAAoB;AF/EtB;;AEqFA;EACE,iBAAiB;AFlFnB;;AEyFA;;;;EAIE,0BAA0B;AFtF5B;;AE2FE;;;;EAKI,eAAe;AFzFrB;;AE+FA;;;;EAIE,UAAU;EACV,kBAAkB;AF5FpB;;AE+FA;;EAEE,sBAAsB;EACtB,UAAU;AF5FZ;;AEgGA;;;;EASE,2BAA2B;AFlG7B;;AEqGA;EACE,cAAc;EAEd,gBAAgB;AFnGlB;;AEsGA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;AFzGX;;AE8GA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEjShB,iBAtCY;EFyUhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;AF3GrB;;AE8GA;EACE,wBAAwB;AF3G1B;;AAEA;;EE+GE,YAAY;AF5Gd;;AAEA;EEkHE,oBAAoB;EACpB,wBAAwB;AFhH1B;;AAEA;EEsHE,wBAAwB;AFpH1B;;AE4HA;EACE,aAAa;EACb,0BAA0B;AFzH5B;;AEgIA;EACE,qBAAqB;AF7HvB;;AEgIA;EACE,kBAAkB;EAClB,eAAe;AF7HjB;;AEgIA;EACE,aAAa;AF7Hf;;AAEA;EEiIE,wBAAwB;AF/H1B;;AM3VA;;EAEE,qBHySuC;EGvSvC,gBHyS+B;EGxS/B,gBHyS+B;AHoDjC;;AMzVA;EFgHM,iBAtCY;AJmRlB;;AM5VA;EF+GM,eAtCY;AJuRlB;;AM/VA;EF8GM,kBAtCY;AJ2RlB;;AMlWA;EF6GM,iBAtCY;AJ+RlB;;AMrWA;EF4GM,kBAtCY;AJmSlB;;AMxWA;EF2GM,eAtCY;AJuSlB;;AM1WA;EFyGM,kBAtCY;EEjEhB,gBH2S+B;AHkEjC;;AMzWA;EFmGM,eAtCY;EE3DhB,gBH8R+B;EG7R/B,gBHqR+B;AHuFjC;;AM1WA;EF8FM,iBAtCY;EEtDhB,gBH0R+B;EGzR/B,gBHgR+B;AH6FjC;;AM3WA;EFyFM,iBAtCY;EEjDhB,gBHsR+B;EGrR/B,gBH2Q+B;AHmGjC;;AM5WA;EFoFM,iBAtCY;EE5ChB,gBHkR+B;EGjR/B,gBHsQ+B;AHyGjC;;AElVA;EIpBE,gBHiFW;EGhFX,mBHgFW;EG/EX,SAAS;EACT,wCHzCa;AHmZf;;AMlWA;;EFMI,cAAW;EEHb,gBH8N+B;AHuIjC;;AMlWA;;EAEE,cHsQgC;EGrQhC,yBH8QmC;AHuFrC;;AM7VA;EC/EE,eAAe;EACf,gBAAgB;APgblB;;AM7VA;ECpFE,eAAe;EACf,gBAAgB;APqblB;;AM/VA;EACE,qBAAqB;ANkWvB;;AMnWA;EAII,oBHwP+B;AH2GnC;;AMzVA;EFjCI,cAAW;EEmCb,yBAAyB;AN4V3B;;AMxVA;EACE,mBHwBW;ECTP,kBAtCY;AJmXlB;;AMxVA;EACE,cAAc;EF7CZ,cAAW;EE+Cb,cH1GgB;AHqclB;;AM9VA;EAMI,qBAAqB;AN4VzB;;AQ/cA;ECIE,eAAe;EAGf,YAAY;AT6cd;;AQ9cA;EACE,gBLigCwC;EKhgCxC,sBLRa;EKSb,yBLNgB;EOLd,sBP6OgC;EMvOlC,eAAe;EAGf,YAAY;ATsdd;;AQxcA;EAEE,qBAAqB;AR0cvB;;AQvcA;EACE,qBAA0B;EAC1B,cAAc;AR0chB;;AQvcA;EJkCI,cAAW;EIhCb,cL3BgB;AHqelB;;AWjfA;EPuEI,gBAAW;EOrEb,cRoCe;EQnCf,qBAAqB;AXofvB;;AWjfE;EACE,cAAc;AXoflB;;AW/eA;EACE,sBRqlCuC;EC3hCrC,gBAAW;EOxDb,WRTa;EQUb,yBRDgB;EOXd,qBP+O+B;AHgRnC;;AWvfA;EASI,UAAU;EPkDV,eAAW;EOhDX,gBR4Q6B;AHsOjC;;AE1SA;ESjME,cAAc;EPyCZ,gBAAW;EOvCb,cRjBgB;AHggBlB;;AWlfA;EP0CI,kBAAW;EOlCX,cAAc;EACd,kBAAkB;AX+etB;;AW1eA;EACE,iBR4jCuC;EQ3jCvC,kBAAkB;AX6epB;;AYrhBE;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;Ab0hBnB;;AcveI;EFtDF;ICWI,gBVqMK;EHkVT;AACF;;Ac7eI;EFtDF;ICWI,gBVsMK;EHuVT;AACF;;AcnfI;EFtDF;ICWI,gBVuMK;EH4VT;AACF;;AczfI;EFtDF;ICWI,iBVwMM;EHiWV;AACF;;AY/iBE;ECPA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;Ab0jBnB;;AcvgBI;EFrCE;IACE,gBT8LG;EHkXT;AACF;;Ac7gBI;EFrCE;IACE,gBT+LG;EHuXT;AACF;;AcnhBI;EFrCE;IACE,gBTgMG;EH4XT;AACF;;AczhBI;EFrCE;IACE,iBTiMI;EHiYV;AACF;;AY/iBE;ECrBA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AbwkB3B;;AYhjBE;EACE,eAAe;EACf,cAAc;AZmjBlB;;AYrjBE;;EAMI,gBAAgB;EAChB,eAAe;AZojBrB;;AetmBE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;Af8mB7B;;Ae3lBM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;Af8lBvB;;Ae1lBQ;EF4BJ,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AbkkB5B;;Ae/lBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbukB5B;;AepmBQ;EF4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;Ab4kB5B;;AezmBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbilB5B;;Ae9mBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbslB5B;;AennBQ;EF4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;Ab2lB5B;;AennBM;EFMJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AbinBjB;;AepnBQ;EFPN,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;Ab4nBzC;;AeznBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbioBzC;;Ae9nBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbsoBzC;;AenoBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab2oBzC;;AexoBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbgpBzC;;Ae7oBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbqpBzC;;AelpBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab0pBzC;;AevpBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab+pBzC;;Ae5pBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AboqBzC;;AejqBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbyqBzC;;AetqBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab8qBzC;;Ae3qBQ;EFPN,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AbmrBzC;;Ae3qBM;EAAwB,kBAAS;EAAT,SAAS;Af+qBvC;;Ae7qBM;EAAuB,kBZ6KG;EY7KH,SZ6KG;AHogBhC;;Ae9qBQ;EAAwB,iBADZ;EACY,QADZ;AfmrBpB;;AelrBQ;EAAwB,iBADZ;EACY,QADZ;AfurBpB;;AetrBQ;EAAwB,iBADZ;EACY,QADZ;Af2rBpB;;Ae1rBQ;EAAwB,iBADZ;EACY,QADZ;Af+rBpB;;Ae9rBQ;EAAwB,iBADZ;EACY,QADZ;AfmsBpB;;AelsBQ;EAAwB,iBADZ;EACY,QADZ;AfusBpB;;AetsBQ;EAAwB,iBADZ;EACY,QADZ;Af2sBpB;;Ae1sBQ;EAAwB,iBADZ;EACY,QADZ;Af+sBpB;;Ae9sBQ;EAAwB,iBADZ;EACY,QADZ;AfmtBpB;;AeltBQ;EAAwB,iBADZ;EACY,QADZ;AfutBpB;;AettBQ;EAAwB,kBADZ;EACY,SADZ;Af2tBpB;;Ae1tBQ;EAAwB,kBADZ;EACY,SADZ;Af+tBpB;;Ae9tBQ;EAAwB,kBADZ;EACY,SADZ;AfmuBpB;;Ae5tBU;EFRR,sBAA8C;AbwuBhD;;AehuBU;EFRR,uBAA8C;Ab4uBhD;;AepuBU;EFRR,gBAA8C;AbgvBhD;;AexuBU;EFRR,uBAA8C;AbovBhD;;Ae5uBU;EFRR,uBAA8C;AbwvBhD;;AehvBU;EFRR,gBAA8C;Ab4vBhD;;AepvBU;EFRR,uBAA8C;AbgwBhD;;AexvBU;EFRR,uBAA8C;AbowBhD;;Ae5vBU;EFRR,gBAA8C;AbwwBhD;;AehwBU;EFRR,uBAA8C;Ab4wBhD;;AepwBU;EFRR,uBAA8C;AbgxBhD;;Ac3wBI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef6yBrB;EezyBM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbgxB1B;Ee7yBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EboxB1B;EejzBM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbwxB1B;EerzBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb4xB1B;EezzBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbgyB1B;Ee7zBM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EboyB1B;Ee5zBI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EbyzBf;Ee5zBM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Ebm0BvC;Eeh0BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebu0BvC;Eep0BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb20BvC;Eex0BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+0BvC;Ee50BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebm1BvC;Eeh1BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebu1BvC;Eep1BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb21BvC;Eex1BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+1BvC;Ee51BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebm2BvC;Eeh2BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebu2BvC;Eep2BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb22BvC;Eex2BM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eb+2BvC;Eev2BI;IAAwB,kBAAS;IAAT,SAAS;Ef02BrC;Eex2BI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EH8rB9B;Eex2BM;IAAwB,iBADZ;IACY,QADZ;Ef42BlB;Ee32BM;IAAwB,iBADZ;IACY,QADZ;Ef+2BlB;Ee92BM;IAAwB,iBADZ;IACY,QADZ;Efk3BlB;Eej3BM;IAAwB,iBADZ;IACY,QADZ;Efq3BlB;Eep3BM;IAAwB,iBADZ;IACY,QADZ;Efw3BlB;Eev3BM;IAAwB,iBADZ;IACY,QADZ;Ef23BlB;Ee13BM;IAAwB,iBADZ;IACY,QADZ;Ef83BlB;Ee73BM;IAAwB,iBADZ;IACY,QADZ;Efi4BlB;Eeh4BM;IAAwB,iBADZ;IACY,QADZ;Efo4BlB;Een4BM;IAAwB,iBADZ;IACY,QADZ;Efu4BlB;Eet4BM;IAAwB,kBADZ;IACY,SADZ;Ef04BlB;Eez4BM;IAAwB,kBADZ;IACY,SADZ;Ef64BlB;Ee54BM;IAAwB,kBADZ;IACY,SADZ;Efg5BlB;Eez4BQ;IFRR,cAA4B;Ebo5B5B;Ee54BQ;IFRR,sBAA8C;Ebu5B9C;Ee/4BQ;IFRR,uBAA8C;Eb05B9C;Eel5BQ;IFRR,gBAA8C;Eb65B9C;Eer5BQ;IFRR,uBAA8C;Ebg6B9C;Eex5BQ;IFRR,uBAA8C;Ebm6B9C;Ee35BQ;IFRR,gBAA8C;Ebs6B9C;Ee95BQ;IFRR,uBAA8C;Eby6B9C;Eej6BQ;IFRR,uBAA8C;Eb46B9C;Eep6BQ;IFRR,gBAA8C;Eb+6B9C;Eev6BQ;IFRR,uBAA8C;Ebk7B9C;Ee16BQ;IFRR,uBAA8C;Ebq7B9C;AACF;;Acj7BI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Efm9BrB;Ee/8BM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;Ebs7B1B;Een9BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb07B1B;Eev9BM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb87B1B;Ee39BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Ebk8B1B;Ee/9BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Ebs8B1B;Een+BM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb08B1B;Eel+BI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb+9Bf;Eel+BM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Eby+BvC;Eet+BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6+BvC;Ee1+BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebi/BvC;Ee9+BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq/BvC;Eel/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby/BvC;Eet/BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6/BvC;Ee1/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbigCvC;Ee9/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbqgCvC;EelgCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbygCvC;EetgCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6gCvC;Ee1gCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbihCvC;Ee9gCM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EbqhCvC;Ee7gCI;IAAwB,kBAAS;IAAT,SAAS;EfghCrC;Ee9gCI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EHo2B9B;Ee9gCM;IAAwB,iBADZ;IACY,QADZ;EfkhClB;EejhCM;IAAwB,iBADZ;IACY,QADZ;EfqhClB;EephCM;IAAwB,iBADZ;IACY,QADZ;EfwhClB;EevhCM;IAAwB,iBADZ;IACY,QADZ;Ef2hClB;Ee1hCM;IAAwB,iBADZ;IACY,QADZ;Ef8hClB;Ee7hCM;IAAwB,iBADZ;IACY,QADZ;EfiiClB;EehiCM;IAAwB,iBADZ;IACY,QADZ;EfoiClB;EeniCM;IAAwB,iBADZ;IACY,QADZ;EfuiClB;EetiCM;IAAwB,iBADZ;IACY,QADZ;Ef0iClB;EeziCM;IAAwB,iBADZ;IACY,QADZ;Ef6iClB;Ee5iCM;IAAwB,kBADZ;IACY,SADZ;EfgjClB;Ee/iCM;IAAwB,kBADZ;IACY,SADZ;EfmjClB;EeljCM;IAAwB,kBADZ;IACY,SADZ;EfsjClB;Ee/iCQ;IFRR,cAA4B;Eb0jC5B;EeljCQ;IFRR,sBAA8C;Eb6jC9C;EerjCQ;IFRR,uBAA8C;EbgkC9C;EexjCQ;IFRR,gBAA8C;EbmkC9C;Ee3jCQ;IFRR,uBAA8C;EbskC9C;Ee9jCQ;IFRR,uBAA8C;EbykC9C;EejkCQ;IFRR,gBAA8C;Eb4kC9C;EepkCQ;IFRR,uBAA8C;Eb+kC9C;EevkCQ;IFRR,uBAA8C;EbklC9C;Ee1kCQ;IFRR,gBAA8C;EbqlC9C;Ee7kCQ;IFRR,uBAA8C;EbwlC9C;EehlCQ;IFRR,uBAA8C;Eb2lC9C;AACF;;AcvlCI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EfynCrB;EernCM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;Eb4lC1B;EeznCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbgmC1B;Ee7nCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbomC1B;EejoCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbwmC1B;EeroCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb4mC1B;EezoCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbgnC1B;EexoCI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EbqoCf;EexoCM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Eb+oCvC;Ee5oCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmpCvC;EehpCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbupCvC;EeppCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2pCvC;EexpCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+pCvC;Ee5pCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbmqCvC;EehqCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbuqCvC;EepqCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2qCvC;EexqCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb+qCvC;Ee5qCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmrCvC;EehrCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EburCvC;EeprCM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eb2rCvC;EenrCI;IAAwB,kBAAS;IAAT,SAAS;EfsrCrC;EeprCI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EH0gC9B;EeprCM;IAAwB,iBADZ;IACY,QADZ;EfwrClB;EevrCM;IAAwB,iBADZ;IACY,QADZ;Ef2rClB;Ee1rCM;IAAwB,iBADZ;IACY,QADZ;Ef8rClB;Ee7rCM;IAAwB,iBADZ;IACY,QADZ;EfisClB;EehsCM;IAAwB,iBADZ;IACY,QADZ;EfosClB;EensCM;IAAwB,iBADZ;IACY,QADZ;EfusClB;EetsCM;IAAwB,iBADZ;IACY,QADZ;Ef0sClB;EezsCM;IAAwB,iBADZ;IACY,QADZ;Ef6sClB;Ee5sCM;IAAwB,iBADZ;IACY,QADZ;EfgtClB;Ee/sCM;IAAwB,iBADZ;IACY,QADZ;EfmtClB;EeltCM;IAAwB,kBADZ;IACY,SADZ;EfstClB;EertCM;IAAwB,kBADZ;IACY,SADZ;EfytClB;EextCM;IAAwB,kBADZ;IACY,SADZ;Ef4tClB;EertCQ;IFRR,cAA4B;EbguC5B;EextCQ;IFRR,sBAA8C;EbmuC9C;Ee3tCQ;IFRR,uBAA8C;EbsuC9C;Ee9tCQ;IFRR,gBAA8C;EbyuC9C;EejuCQ;IFRR,uBAA8C;Eb4uC9C;EepuCQ;IFRR,uBAA8C;Eb+uC9C;EevuCQ;IFRR,gBAA8C;EbkvC9C;Ee1uCQ;IFRR,uBAA8C;EbqvC9C;Ee7uCQ;IFRR,uBAA8C;EbwvC9C;EehvCQ;IFRR,gBAA8C;Eb2vC9C;EenvCQ;IFRR,uBAA8C;Eb8vC9C;EetvCQ;IFRR,uBAA8C;EbiwC9C;AACF;;Ac7vCI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef+xCrB;Ee3xCM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbkwC1B;Ee/xCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbswC1B;EenyCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb0wC1B;EevyCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb8wC1B;Ee3yCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbkxC1B;Ee/yCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbsxC1B;Ee9yCI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb2yCf;Ee9yCM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EbqzCvC;EelzCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbyzCvC;EetzCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6zCvC;Ee1zCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi0CvC;Ee9zCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq0CvC;Eel0CM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eby0CvC;Eet0CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb60CvC;Ee10CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi1CvC;Ee90CM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebq1CvC;Eel1CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby1CvC;Eet1CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb61CvC;Ee11CM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Ebi2CvC;Eez1CI;IAAwB,kBAAS;IAAT,SAAS;Ef41CrC;Ee11CI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EHgrC9B;Ee11CM;IAAwB,iBADZ;IACY,QADZ;Ef81ClB;Ee71CM;IAAwB,iBADZ;IACY,QADZ;Efi2ClB;Eeh2CM;IAAwB,iBADZ;IACY,QADZ;Efo2ClB;Een2CM;IAAwB,iBADZ;IACY,QADZ;Efu2ClB;Eet2CM;IAAwB,iBADZ;IACY,QADZ;Ef02ClB;Eez2CM;IAAwB,iBADZ;IACY,QADZ;Ef62ClB;Ee52CM;IAAwB,iBADZ;IACY,QADZ;Efg3ClB;Ee/2CM;IAAwB,iBADZ;IACY,QADZ;Efm3ClB;Eel3CM;IAAwB,iBADZ;IACY,QADZ;Efs3ClB;Eer3CM;IAAwB,iBADZ;IACY,QADZ;Efy3ClB;Eex3CM;IAAwB,kBADZ;IACY,SADZ;Ef43ClB;Ee33CM;IAAwB,kBADZ;IACY,SADZ;Ef+3ClB;Ee93CM;IAAwB,kBADZ;IACY,SADZ;Efk4ClB;Ee33CQ;IFRR,cAA4B;Ebs4C5B;Ee93CQ;IFRR,sBAA8C;Eby4C9C;Eej4CQ;IFRR,uBAA8C;Eb44C9C;Eep4CQ;IFRR,gBAA8C;Eb+4C9C;Eev4CQ;IFRR,uBAA8C;Ebk5C9C;Ee14CQ;IFRR,uBAA8C;Ebq5C9C;Ee74CQ;IFRR,gBAA8C;Ebw5C9C;Eeh5CQ;IFRR,uBAA8C;Eb25C9C;Een5CQ;IFRR,uBAA8C;Eb85C9C;Eet5CQ;IFRR,gBAA8C;Ebi6C9C;Eez5CQ;IFRR,uBAA8C;Ebo6C9C;Ee55CQ;IFRR,uBAA8C;Ebu6C9C;AACF;;AgB39CA;EACE,WAAW;EACX,mBbkIW;EajIX,cbSgB;AHq9ClB;;AgBj+CA;;EAQI,gBbsVgC;EarVhC,mBAAmB;EACnB,6BbJc;AHk+ClB;;AgBx+CA;EAcI,sBAAsB;EACtB,gCbTc;AHu+ClB;;AgB7+CA;EAmBI,6Bbbc;AH2+ClB;;AgBr9CA;;EAGI,ebgU+B;AHupCnC;;AgB98CA;EACE,yBbnCgB;AHo/ClB;;AgBl9CA;;EAKI,yBbvCc;AHy/ClB;;AgBv9CA;;EAWM,wBAA4C;AhBi9ClD;;AgB58CA;;;;EAKI,SAAS;AhB88Cb;;AgBt8CA;EAEI,qCb1DW;AHkgDf;;AKvgDE;EW2EI,cbvEY;EawEZ,sCbvES;AHugDf;;AiBnhDE;;;EAII,yBCsF4D;AlB+7ClE;;AiBzhDE;;;;EAYM,qBC8E0D;AlBs8ClE;;AKzhDE;EYiBM,yBAJsC;AjBghD9C;;AiBjhDE;;EASQ,yBARoC;AjBqhD9C;;AiBziDE;;;EAII,yBCsF4D;AlBq9ClE;;AiB/iDE;;;;EAYM,qBC8E0D;AlB49ClE;;AK/iDE;EYiBM,yBAJsC;AjBsiD9C;;AiBviDE;;EASQ,yBARoC;AjB2iD9C;;AiB/jDE;;;EAII,yBCsF4D;AlB2+ClE;;AiBrkDE;;;;EAYM,qBC8E0D;AlBk/ClE;;AKrkDE;EYiBM,yBAJsC;AjB4jD9C;;AiB7jDE;;EASQ,yBARoC;AjBikD9C;;AiBrlDE;;;EAII,yBCsF4D;AlBigDlE;;AiB3lDE;;;;EAYM,qBC8E0D;AlBwgDlE;;AK3lDE;EYiBM,yBAJsC;AjBklD9C;;AiBnlDE;;EASQ,yBARoC;AjBulD9C;;AiB3mDE;;;EAII,yBCsF4D;AlBuhDlE;;AiBjnDE;;;;EAYM,qBC8E0D;AlB8hDlE;;AKjnDE;EYiBM,yBAJsC;AjBwmD9C;;AiBzmDE;;EASQ,yBARoC;AjB6mD9C;;AiBjoDE;;;EAII,yBCsF4D;AlB6iDlE;;AiBvoDE;;;;EAYM,qBC8E0D;AlBojDlE;;AKvoDE;EYiBM,yBAJsC;AjB8nD9C;;AiB/nDE;;EASQ,yBARoC;AjBmoD9C;;AiBvpDE;;;EAII,yBCsF4D;AlBmkDlE;;AiB7pDE;;;;EAYM,qBC8E0D;AlB0kDlE;;AK7pDE;EYiBM,yBAJsC;AjBopD9C;;AiBrpDE;;EASQ,yBARoC;AjBypD9C;;AiB7qDE;;;EAII,yBCsF4D;AlBylDlE;;AiBnrDE;;;;EAYM,qBC8E0D;AlBgmDlE;;AKnrDE;EYiBM,yBAJsC;AjB0qD9C;;AiB3qDE;;EASQ,yBARoC;AjB+qD9C;;AiBnsDE;;;EAII,sCdQS;AH6rDf;;AKlsDE;EYiBM,sCAJsC;AjByrD9C;;AiB1rDE;;EASQ,sCARoC;AjB8rD9C;;AgBxmDA;EAGM,Wb3GS;Ea4GT,yBbpGY;EaqGZ,qBbmQqD;AHs2C3D;;AgB9mDA;EAWM,cb5GY;Ea6GZ,yBblHY;EamHZ,qBblHY;AHytDlB;;AgBlmDA;EACE,Wb3Ha;Ea4Hb,yBbpHgB;AHytDlB;;AgBvmDA;;;EAOI,qBb+OuD;AHu3C3D;;AgB7mDA;EAWI,SAAS;AhBsmDb;;AgBjnDA;EAgBM,2Cb1IS;AH+uDf;;AK1uDE;EW4IM,WbjJO;EakJP,4CblJO;AHovDf;;AclrDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBolDvC;EgBzlDG;IASK,SAAS;EhBmlDjB;AACF;;Ac9rDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBgmDvC;EgBrmDG;IASK,SAAS;EhB+lDjB;AACF;;Ac1sDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhB4mDvC;EgBjnDG;IASK,SAAS;EhB2mDjB;AACF;;ActtDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBwnDvC;EgB7nDG;IASK,SAAS;EhBunDjB;AACF;;AgBtoDA;EAOQ,cAAc;EACd,WAAW;EACX,gBAAgB;EAChB,iCAAiC;AhBmoDzC;;AgB7oDA;EAcU,SAAS;AhBmoDnB;;AmBhzDA;EACE,cAAc;EACd,WAAW;EACX,mCDuG8D;ECtG9D,yBhB4XkC;ECvQ9B,eAtCY;Ee5EhB,gBhBsR+B;EgBrR/B,gBhB0R+B;EgBzR/B,chBDgB;EgBEhB,sBhBTa;EgBUb,4BAA4B;EAC5B,yBhBPgB;EONd,sBP6OgC;EiB5O9B,wEjBof4F;AH40ClG;;AoB3zDI;EDLJ;ICMM,gBAAgB;EpB+zDpB;AACF;;AmBt0DA;EAsBI,6BAA6B;EAC7B,SAAS;AnBozDb;;AmB30DA;EA4BI,kBAAkB;EAClB,0BhBrBc;AHw0DlB;;AqBz0DE;EACE,clBAc;EkBCd,sBlBRW;EkBSX,qBlBwdsE;EkBvdtE,UAAU;EAKR,gDlBcW;AH0zDjB;;AmBx1DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAiDI,yBhB9Cc;EgBgDd,UAAU;AnB+yDd;;AmB3yDA;EAOI,chBtDc;EgBuDd,sBhB9DW;AHs2Df;;AmBnyDA;;EAEE,cAAc;EACd,WAAW;AnBsyDb;;AmB5xDA;EACE,iCDwB8D;ECvB9D,oCDuB8D;ECtB9D,gBAAgB;EflBd,kBAAW;EeoBb,gBhB4M+B;AHmlDjC;;AmB5xDA;EACE,+BDgB8D;ECf9D,kCDe8D;Ede1D,kBAtCY;EeUhB,gBhByI+B;AHspDjC;;AmB5xDA;EACE,gCDS8D;ECR9D,mCDQ8D;Ede1D,mBAtCY;EeiBhB,gBhBmI+B;AH4pDjC;;AmBtxDA;EACE,cAAc;EACd,WAAW;EACX,mBAA2B;EAC3B,gBAAgB;EfQZ,eAtCY;EegChB,gBhB+K+B;EgB9K/B,chB1GgB;EgB2GhB,6BAA6B;EAC7B,yBAAyB;EACzB,mBAAmC;AnByxDrC;;AmBnyDA;EAcI,gBAAgB;EAChB,eAAe;AnByxDnB;;AmB7wDA;EACE,kCD/B8D;ECgC9D,uBhBgQiC;ECjR7B,mBAtCY;EeyDhB,gBhB2F+B;EOxO7B,qBP+O+B;AH+qDnC;;AmB7wDA;EACE,gCDvC8D;ECwC9D,oBhB6PgC;ECtR5B,kBAtCY;EeiEhB,gBhBkF+B;EOvO7B,qBP8O+B;AHwrDnC;;AmB5wDA;EAGI,YAAY;AnB6wDhB;;AmBzwDA;EACE,YAAY;AnB4wDd;;AmBpwDA;EACE,mBhBsV0C;AHi7C5C;;AmBpwDA;EACE,cAAc;EACd,mBhBuU4C;AHg8C9C;;AmB/vDA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,kBAA0C;EAC1C,iBAAyC;AnBkwD3C;;AmBtwDA;;EAQI,kBAA0C;EAC1C,iBAAyC;AnBmwD7C;;AmB1vDA;EACE,kBAAkB;EAClB,cAAc;EACd,qBhB4S6C;AHi9C/C;;AmB1vDA;EACE,kBAAkB;EAClB,kBhBwS2C;EgBvS3C,qBhBsS6C;AHu9C/C;;AmBhwDA;;EAQI,chBhNc;AH68DlB;;AmBzvDA;EACE,gBAAgB;AnB4vDlB;;AmBzvDA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,sBAAmB;EAAnB,mBAAmB;EACnB,eAAe;EACf,qBhByR4C;AHm+C9C;;AmBhwDA;EAQI,gBAAgB;EAChB,aAAa;EACb,uBhBoR4C;EgBnR5C,cAAc;AnB4vDlB;;AqBh8DE;EACE,aAAa;EACb,WAAW;EACX,mBlB6c0C;ECpb1C,cAAW;EiBvBX,clBNa;AHy8DjB;;AqBh8DE;EACE,kBAAkB;EAClB,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBmyBqC;EkBlyBrC,iBAAiB;EjBoEf,mBAtCY;EiB5Bd,gBlB2O6B;EkB1O7B,WlBvDW;EkBwDX,wClBpBa;EOtCb,sBP6OgC;AHixDpC;;AqBn+DI;;;;EAsCE,cAAc;ArBo8DpB;;AqB1+DI;EA4CE,qBlBjCW;EkBoCT,oCHiCwD;EGhCxD,iRHpCmI;EGqCnI,4BAA4B;EAC5B,2DAA6D;EAC7D,gEH6BwD;AlBm6DhE;;AqBn/DI;EAuDI,qBlB5CS;EkB6CT,gDlB7CS;AH6+DjB;;AqBx/DI;EAiEI,oCHewD;EGdxD,kFHcwD;AlB66DhE;;AqB7/DI;EAyEE,qBlB9DW;EkBiET,uCHIwD;EGHxD,ujBAA8J;ArBs7DtK;;AqBngEI;EAiFI,qBlBtES;EkBuET,gDlBvES;AH6/DjB;;AqBxgEI;EA0FI,clB/ES;AHigEjB;;AqB5gEI;;;EA+FI,cAAc;ArBm7DtB;;AqBlhEI;EAuGI,clB5FS;AH2gEjB;;AqBthEI;EA0GM,qBlB/FO;AH+gEjB;;AqB1hEI;EAgHM,qBAAkC;EC1IxC,yBD2I+C;ArB86DnD;;AqB/hEI;EAuHM,gDlB5GO;AHwhEjB;;AqBniEI;EA2HM,qBlBhHO;AH4hEjB;;AqBviEI;EAqII,qBlB1HS;AHgiEjB;;AqB3iEI;EA0IM,qBlB/HO;EkBgIP,gDlBhIO;AHqiEjB;;AqBpiEE;EACE,aAAa;EACb,WAAW;EACX,mBlB6c0C;ECpb1C,cAAW;EiBvBX,clBTa;AHgjEjB;;AqBpiEE;EACE,kBAAkB;EAClB,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBmyBqC;EkBlyBrC,iBAAiB;EjBoEf,mBAtCY;EiB5Bd,gBlB2O6B;EkB1O7B,WlBvDW;EkBwDX,wClBvBa;EOnCb,sBP6OgC;AHq3DpC;;AqBvkEI;;;;EAsCE,cAAc;ArBwiEpB;;AqB9kEI;EA4CE,qBlBpCW;EkBuCT,oCHiCwD;EGhCxD,4UHpCmI;EGqCnI,4BAA4B;EAC5B,2DAA6D;EAC7D,gEH6BwD;AlBugEhE;;AqBvlEI;EAuDI,qBlB/CS;EkBgDT,gDlBhDS;AHolEjB;;AqB5lEI;EAiEI,oCHewD;EGdxD,kFHcwD;AlBihEhE;;AqBjmEI;EAyEE,qBlBjEW;EkBoET,uCHIwD;EGHxD,knBAA8J;ArB0hEtK;;AqBvmEI;EAiFI,qBlBzES;EkB0ET,gDlB1ES;AHomEjB;;AqB5mEI;EA0FI,clBlFS;AHwmEjB;;AqBhnEI;;;EA+FI,cAAc;ArBuhEtB;;AqBtnEI;EAuGI,clB/FS;AHknEjB;;AqB1nEI;EA0GM,qBlBlGO;AHsnEjB;;AqB9nEI;EAgHM,qBAAkC;EC1IxC,yBD2I+C;ArBkhEnD;;AqBnoEI;EAuHM,gDlB/GO;AH+nEjB;;AqBvoEI;EA2HM,qBlBnHO;AHmoEjB;;AqB3oEI;EAqII,qBlB7HS;AHuoEjB;;AqB/oEI;EA0IM,qBlBlIO;EkBmIP,gDlBnIO;AH4oEjB;;AmB36DA;EACE,oBAAa;EAAb,aAAa;EACb,uBAAmB;EAAnB,mBAAmB;EACnB,sBAAmB;EAAnB,mBAAmB;AnB86DrB;;AmBj7DA;EASI,WAAW;AnB46Df;;AcloEI;EK6MJ;IAeM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,gBAAgB;EnB26DpB;EmB77DF;IAuBM,oBAAa;IAAb,aAAa;IACb,kBAAc;IAAd,cAAc;IACd,uBAAmB;IAAnB,mBAAmB;IACnB,sBAAmB;IAAnB,mBAAmB;IACnB,gBAAgB;EnBy6DpB;EmBp8DF;IAgCM,qBAAqB;IACrB,WAAW;IACX,sBAAsB;EnBu6D1B;EmBz8DF;IAuCM,qBAAqB;EnBq6DzB;EmB58DF;;IA4CM,WAAW;EnBo6Df;EmBh9DF;IAkDM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,WAAW;IACX,eAAe;EnBi6DnB;EmBv9DF;IAyDM,kBAAkB;IAClB,oBAAc;IAAd,cAAc;IACd,aAAa;IACb,qBhB2LwC;IgB1LxC,cAAc;EnBi6DlB;EmB99DF;IAiEM,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;EnBg6D3B;EmBl+DF;IAqEM,gBAAgB;EnBg6DpB;AACF;;AuBzuEA;EACE,qBAAqB;EAErB,gBpB0R+B;EoBzR/B,cpBMgB;EoBLhB,kBAAkB;EAElB,sBAAsB;EACtB,eAAsD;EACtD,yBAAiB;EAAjB,sBAAiB;EAAjB,qBAAiB;EAAjB,iBAAiB;EACjB,6BAA6B;EAC7B,6BAA2C;ECuF3C,yBrB8RkC;ECvQ9B,eAtCY;EoBiBhB,gBrB8L+B;EOnS7B,sBP6OgC;EiB5O9B,qIjB6b6I;AH6zDnJ;;AoBrvEI;EGLJ;IHMM,gBAAgB;EpByvEpB;AACF;;AK1vEE;EkBUE,cpBNc;EoBOd,qBAAqB;AvBovEzB;;AuBrwEA;EAsBI,UAAU;EACV,gDpBOa;AH4uEjB;;AuB1wEA;EA6BI,apBoZ6B;AH61DjC;;AuBluEA;;EAEE,oBAAoB;AvBquEtB;;AuB5tEE;ECvDA,WrBCa;EmBDX,yBnB8Ba;EqB5Bf,qBrB4Be;AH2vEjB;;AKnxEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBgyE7H;;AwBpxEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBkxEvF;;AwB7wEE;EAEE,WrB1BW;EqB2BX,yBrBEa;EqBDb,qBrBCa;AH8wEjB;;AwBxwEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBozEnN;;AwBrwEI;;EAKI,gDAAiF;AxBqwEzF;;AuBjwEE;ECvDA,WrBCa;EmBDX,yBnBOc;EqBLhB,qBrBKgB;AHuzElB;;AKxzEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBq0E7H;;AwBzzEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBuzEvF;;AwBlzEE;EAEE,WrB1BW;EqB2BX,yBrBrBc;EqBsBd,qBrBtBc;AH00ElB;;AwB7yEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBy1EnN;;AwB1yEI;;EAKI,iDAAiF;AxB0yEzF;;AuBtyEE;ECvDA,WrBCa;EmBDX,yBnBqCa;EqBnCf,qBrBmCe;AH8zEjB;;AK71EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB02E7H;;AwB91EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB41EvF;;AwBv1EE;EAEE,WrB1BW;EqB2BX,yBrBSa;EqBRb,qBrBQa;AHi1EjB;;AwBl1EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB83EnN;;AwB/0EI;;EAKI,+CAAiF;AxB+0EzF;;AuB30EE;ECvDA,WrBCa;EmBDX,yBnBuCa;EqBrCf,qBrBqCe;AHi2EjB;;AKl4EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB+4E7H;;AwBn4EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBi4EvF;;AwB53EE;EAEE,WrB1BW;EqB2BX,yBrBWa;EqBVb,qBrBUa;AHo3EjB;;AwBv3EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBm6EnN;;AwBp3EI;;EAKI,gDAAiF;AxBo3EzF;;AuBh3EE;ECvDA,crBUgB;EmBVd,yBnBoCa;EqBlCf,qBrBkCe;AHy4EjB;;AKv6EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxBo7E7H;;AwBx6EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBs6EvF;;AwBj6EE;EAEE,crBjBc;EqBkBd,yBrBQa;EqBPb,qBrBOa;AH45EjB;;AwB55EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBw8EnN;;AwBz5EI;;EAKI,gDAAiF;AxBy5EzF;;AuBr5EE;ECvDA,WrBCa;EmBDX,yBnBkCa;EqBhCf,qBrBgCe;AHg7EjB;;AK58EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBy9E7H;;AwB78EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB28EvF;;AwBt8EE;EAEE,WrB1BW;EqB2BX,yBrBMa;EqBLb,qBrBKa;AHm8EjB;;AwBj8EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB6+EnN;;AwB97EI;;EAKI,+CAAiF;AxB87EzF;;AuB17EE;ECvDA,crBUgB;EmBVd,yBnBEc;EqBAhB,qBrBAgB;AHq/ElB;;AKj/EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxB8/E7H;;AwBl/EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBg/EvF;;AwB3+EE;EAEE,crBjBc;EqBkBd,yBrB1Bc;EqB2Bd,qBrB3Bc;AHwgFlB;;AwBt+EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBkhFnN;;AwBn+EI;;EAKI,iDAAiF;AxBm+EzF;;AuB/9EE;ECvDA,WrBCa;EmBDX,yBnBSc;EqBPhB,qBrBOgB;AHmhFlB;;AKthFE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBmiF7H;;AwBvhFE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,8CAAiF;AxBqhFvF;;AwBhhFE;EAEE,WrB1BW;EqB2BX,yBrBnBc;EqBoBd,qBrBpBc;AHsiFlB;;AwB3gFE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBujFnN;;AwBxgFI;;EAKI,8CAAiF;AxBwgFzF;;AuB9/EE;ECHA,crB5Be;EqB6Bf,qBrB7Be;AHkiFjB;;AK1jFE;EmBwDE,WrB7DW;EqB8DX,yBrBjCa;EqBkCb,qBrBlCa;AHwiFjB;;AwBngFE;EAEE,+CrBvCa;AH4iFjB;;AwBlgFE;EAEE,crB5Ca;EqB6Cb,6BAA6B;AxBogFjC;;AwBjgFE;;EAGE,WrBhFW;EqBiFX,yBrBpDa;EqBqDb,qBrBrDa;AHwjFjB;;AwBjgFI;;EAKI,+CrB5DS;AH6jFjB;;AuB9hFE;ECHA,crBnDgB;EqBoDhB,qBrBpDgB;AHylFlB;;AK1lFE;EmBwDE,WrB7DW;EqB8DX,yBrBxDc;EqByDd,qBrBzDc;AH+lFlB;;AwBniFE;EAEE,iDrB9Dc;AHmmFlB;;AwBliFE;EAEE,crBnEc;EqBoEd,6BAA6B;AxBoiFjC;;AwBjiFE;;EAGE,WrBhFW;EqBiFX,yBrB3Ec;EqB4Ed,qBrB5Ec;AH+mFlB;;AwBjiFI;;EAKI,iDrBnFU;AHonFlB;;AuB9jFE;ECHA,crBrBe;EqBsBf,qBrBtBe;AH2lFjB;;AK1nFE;EmBwDE,WrB7DW;EqB8DX,yBrB1Ba;EqB2Bb,qBrB3Ba;AHimFjB;;AwBnkFE;EAEE,+CrBhCa;AHqmFjB;;AwBlkFE;EAEE,crBrCa;EqBsCb,6BAA6B;AxBokFjC;;AwBjkFE;;EAGE,WrBhFW;EqBiFX,yBrB7Ca;EqB8Cb,qBrB9Ca;AHinFjB;;AwBjkFI;;EAKI,+CrBrDS;AHsnFjB;;AuB9lFE;ECHA,crBnBe;EqBoBf,qBrBpBe;AHynFjB;;AK1pFE;EmBwDE,WrB7DW;EqB8DX,yBrBxBa;EqByBb,qBrBzBa;AH+nFjB;;AwBnmFE;EAEE,gDrB9Ba;AHmoFjB;;AwBlmFE;EAEE,crBnCa;EqBoCb,6BAA6B;AxBomFjC;;AwBjmFE;;EAGE,WrBhFW;EqBiFX,yBrB3Ca;EqB4Cb,qBrB5Ca;AH+oFjB;;AwBjmFI;;EAKI,gDrBnDS;AHopFjB;;AuB9nFE;ECHA,crBtBe;EqBuBf,qBrBvBe;AH4pFjB;;AK1rFE;EmBwDE,crBpDc;EqBqDd,yBrB3Ba;EqB4Bb,qBrB5Ba;AHkqFjB;;AwBnoFE;EAEE,+CrBjCa;AHsqFjB;;AwBloFE;EAEE,crBtCa;EqBuCb,6BAA6B;AxBooFjC;;AwBjoFE;;EAGE,crBvEc;EqBwEd,yBrB9Ca;EqB+Cb,qBrB/Ca;AHkrFjB;;AwBjoFI;;EAKI,+CrBtDS;AHurFjB;;AuB9pFE;ECHA,crBxBe;EqByBf,qBrBzBe;AH8rFjB;;AK1tFE;EmBwDE,WrB7DW;EqB8DX,yBrB7Ba;EqB8Bb,qBrB9Ba;AHosFjB;;AwBnqFE;EAEE,+CrBnCa;AHwsFjB;;AwBlqFE;EAEE,crBxCa;EqByCb,6BAA6B;AxBoqFjC;;AwBjqFE;;EAGE,WrBhFW;EqBiFX,yBrBhDa;EqBiDb,qBrBjDa;AHotFjB;;AwBjqFI;;EAKI,+CrBxDS;AHytFjB;;AuB9rFE;ECHA,crBxDgB;EqByDhB,qBrBzDgB;AH8vFlB;;AK1vFE;EmBwDE,crBpDc;EqBqDd,yBrB7Dc;EqB8Dd,qBrB9Dc;AHowFlB;;AwBnsFE;EAEE,iDrBnEc;AHwwFlB;;AwBlsFE;EAEE,crBxEc;EqByEd,6BAA6B;AxBosFjC;;AwBjsFE;;EAGE,crBvEc;EqBwEd,yBrBhFc;EqBiFd,qBrBjFc;AHoxFlB;;AwBjsFI;;EAKI,iDrBxFU;AHyxFlB;;AuB9tFE;ECHA,crBjDgB;EqBkDhB,qBrBlDgB;AHuxFlB;;AK1xFE;EmBwDE,WrB7DW;EqB8DX,yBrBtDc;EqBuDd,qBrBvDc;AH6xFlB;;AwBnuFE;EAEE,8CrB5Dc;AHiyFlB;;AwBluFE;EAEE,crBjEc;EqBkEd,6BAA6B;AxBouFjC;;AwBjuFE;;EAGE,WrBhFW;EqBiFX,yBrBzEc;EqB0Ed,qBrB1Ec;AH6yFlB;;AwBjuFI;;EAKI,8CrBjFU;AHkzFlB;;AuBnvFA;EACE,gBpBoN+B;EoBnN/B,cpB5Ce;EoB6Cf,qBpBkG4C;AHopF9C;;AK3zFE;EkBwEE,cpBgG8D;EoB/F9D,0BpBgG+C;AHupFnD;;AuB9vFA;EAYI,0BpB2F+C;EoB1F/C,gBAAgB;AvBsvFpB;;AuBnwFA;EAkBI,cpBnFc;EoBoFd,oBAAoB;AvBqvFxB;;AuB1uFA;ECJE,oBrB6SgC;ECtR5B,kBAtCY;EoBiBhB,gBrBkI+B;EOvO7B,qBP8O+B;AH0mFnC;;AuB7uFA;ECRE,uBrBwSiC;ECjR7B,mBAtCY;EoBiBhB,gBrBmI+B;EOxO7B,qBP+O+B;AHgnFnC;;AuB3uFA;EACE,cAAc;EACd,WAAW;AvB8uFb;;AuBhvFA;EAMI,kBpB6T+B;AHi7EnC;;AuBzuFA;;;EAII,WAAW;AvB2uFf;;AyBn3FA;ELMM,gCjB8P2C;AHmnFjD;;AoB52FI;EKXJ;ILYM,gBAAgB;EpBg3FpB;AACF;;AyB73FA;EAII,UAAU;AzB63Fd;;AyBz3FA;EAEI,aAAa;AzB23FjB;;AyBv3FA;EACE,kBAAkB;EAClB,SAAS;EACT,gBAAgB;ELXZ,6BjB+PwC;AHuoF9C;;AoBj4FI;EKGJ;ILFM,gBAAgB;EpBq4FpB;AACF;;A0Bj5FA;;;;EAIE,kBAAkB;A1Bo5FpB;;A0Bj5FA;EACE,mBAAmB;A1Bo5FrB;;A2Bh4FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAhCf,uBAA8B;EAC9B,qCAA4C;EAC5C,gBAAgB;EAChB,oCAA2C;A3Bo6F7C;;A2B/2FI;EACE,cAAc;A3Bk3FpB;;A0B55FA;EACE,kBAAkB;EAClB,SAAS;EACT,OAAO;EACP,avB4pBsC;EuB3pBtC,aAAa;EACb,WAAW;EACX,gBvBkuBuC;EuBjuBvC,iBAA8B;EAC9B,oBAA4B;EtBsGxB,eAtCY;EsB9DhB,cvBXgB;EuBYhB,gBAAgB;EAChB,gBAAgB;EAChB,sBvBvBa;EuBwBb,4BAA4B;EAC5B,qCvBfa;EOZX,sBP6OgC;AH8sFpC;;A0Bv5FI;EACE,WAAW;EACX,OAAO;A1B05Fb;;A0Bv5FI;EACE,QAAQ;EACR,UAAU;A1B05FhB;;Ac94FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bq6FX;E0Bl6FE;IACE,QAAQ;IACR,UAAU;E1Bo6Fd;AACF;;Acz5FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bg7FX;E0B76FE;IACE,QAAQ;IACR,UAAU;E1B+6Fd;AACF;;Acp6FI;EYnBA;IACE,WAAW;IACX,OAAO;E1B27FX;E0Bx7FE;IACE,QAAQ;IACR,UAAU;E1B07Fd;AACF;;Ac/6FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bs8FX;E0Bn8FE;IACE,QAAQ;IACR,UAAU;E1Bq8Fd;AACF;;A0B/7FA;EAEI,SAAS;EACT,YAAY;EACZ,aAAa;EACb,uBvB+rBuC;AHkwE3C;;A2Bh+FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAzBf,aAAa;EACb,qCAA4C;EAC5C,0BAAiC;EACjC,oCAA2C;A3B6/F7C;;A2B/8FI;EACE,cAAc;A3Bk9FpB;;A0Bx8FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,qBvBirBuC;AHyxE3C;;A2Bv/FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAlBf,mCAA0C;EAC1C,eAAe;EACf,sCAA6C;EAC7C,wBAA+B;A3B6gGjC;;A2Bt+FI;EACE,cAAc;A3By+FpB;;A2BtgGI;EDmDE,iBAAiB;A1Bu9FvB;;A0Bl9FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,sBvBgqBuC;AHozE3C;;A2BlhGI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;A3BqhGjB;;A2BzhGI;EAgBI,aAAa;A3B6gGrB;;A2B1gGM;EACE,qBAAqB;EACrB,qBxB+MwC;EwB9MxC,uBxB6MwC;EwB5MxC,WAAW;EA9BjB,mCAA0C;EAC1C,yBAAgC;EAChC,sCAA6C;A3B4iG/C;;A2B3gGI;EACE,cAAc;A3B8gGpB;;A2BxhGM;EDiDA,iBAAiB;A1B2+FvB;;A0Bp+FA;EAKI,WAAW;EACX,YAAY;A1Bm+FhB;;A0B99FA;EE9GE,SAAS;EACT,gBAAmB;EACnB,gBAAgB;EAChB,6BzBCgB;AH+kGlB;;A0B99FA;EACE,cAAc;EACd,WAAW;EACX,uBvBopBwC;EuBnpBxC,WAAW;EACX,gBvBoK+B;EuBnK/B,cvBhHgB;EuBiHhB,mBAAmB;EACnB,mBAAmB;EACnB,6BAA6B;EAC7B,SAAS;A1Bi+FX;;AKrlGE;EqBmIE,cvBqnBqD;EuBpnBrD,qBAAqB;EJ9IrB,yBnBEc;AHmmGlB;;A0Bj/FA;EAgCI,WvBnJW;EuBoJX,qBAAqB;EJrJrB,yBnB8Ba;AH6kGjB;;A0Bv/FA;EAuCI,cvBpJc;EuBqJd,oBAAoB;EACpB,6BAA6B;A1Bo9FjC;;A0B58FA;EACE,cAAc;A1B+8FhB;;A0B38FA;EACE,cAAc;EACd,sBvB+lBwC;EuB9lBxC,gBAAgB;EtBpDZ,mBAtCY;EsB4FhB,cvBxKgB;EuByKhB,mBAAmB;A1B88FrB;;A0B18FA;EACE,cAAc;EACd,uBvBqlBwC;EuBplBxC,cvB7KgB;AH0nGlB;;A6BvoGA;;EAEE,kBAAkB;EAClB,2BAAoB;EAApB,oBAAoB;EACpB,sBAAsB;A7B0oGxB;;A6B9oGA;;EAOI,kBAAkB;EAClB,kBAAc;EAAd,cAAc;A7B4oGlB;;AK3oGE;;EwBII,UAAU;A7B4oGhB;;A6BzpGA;;;;EAkBM,UAAU;A7B8oGhB;;A6BxoGA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,oBAA2B;EAA3B,2BAA2B;A7B2oG7B;;A6B9oGA;EAMI,WAAW;A7B4oGf;;A6BxoGA;;EAII,iB1BsM6B;AHm8FjC;;A6B7oGA;;EnBhBI,0BmB0B8B;EnBzB9B,6BmByB8B;A7ByoGlC;;A6BnpGA;;EnBFI,yBmBiB6B;EnBhB7B,4BmBgB6B;A7B0oGjC;;A6B1nGA;EACE,wBAAmC;EACnC,uBAAkC;A7B6nGpC;;A6B/nGA;;;EAOI,cAAc;A7B8nGlB;;A6B3nGE;EACE,eAAe;A7B8nGnB;;A6B1nGA;EACE,uBAAsC;EACtC,sBAAqC;A7B6nGvC;;A6B1nGA;EACE,sBAAsC;EACtC,qBAAqC;A7B6nGvC;;A6BzmGA;EACE,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,qBAAuB;EAAvB,uBAAuB;A7B4mGzB;;A6B/mGA;;EAOI,WAAW;A7B6mGf;;A6BpnGA;;EAYI,gB1BqH6B;AHw/FjC;;A6BznGA;;EnBlFI,6BmBoG+B;EnBnG/B,4BmBmG+B;A7B6mGnC;;A6B/nGA;;EnBhGI,yBmBuH4B;EnBtH5B,0BmBsH4B;A7B8mGhC;;A6B7lGA;;EAGI,gBAAgB;A7B+lGpB;;A6BlmGA;;;;EAOM,kBAAkB;EAClB,sBAAsB;EACtB,oBAAoB;A7BkmG1B;;A8B3vGA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,uBAAoB;EAApB,oBAAoB;EACpB,WAAW;A9B8vGb;;A8BnwGA;;;;EAWI,kBAAkB;EAClB,gBAAY;EAAZ,YAAY;EACZ,YAAY;EACZ,gBAAgB;A9B+vGpB;;A8B7wGA;;;;;;;;;;;;EAmBM,iB3BsN2B;AHmjGjC;;A8B5xGA;;;EA2BI,UAAU;A9BuwGd;;A8BlyGA;EAgCI,UAAU;A9BswGd;;A8BtyGA;;EpBeI,0BoBsBmD;EpBrBnD,6BoBqBmD;A9BuwGvD;;A8B5yGA;;EpB6BI,yBoBSmD;EpBRnD,4BoBQmD;A9B4wGvD;;A8BlzGA;EA4CI,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;A9B0wGvB;;A8BvzGA;;EpBeI,0BoBiC6E;EpBhC7E,6BoBgC6E;A9B6wGjF;;A8B7zGA;EpB6BI,yBoBoBsE;EpBnBtE,4BoBmBsE;A9BixG1E;;A8BtwGA;;EAEE,oBAAa;EAAb,aAAa;A9BywGf;;A8B3wGA;;EAQI,kBAAkB;EAClB,UAAU;A9BwwGd;;A8BjxGA;;EAYM,UAAU;A9B0wGhB;;A8BtxGA;;;;;;;;EAoBI,iB3ByJ6B;AHonGjC;;A8BzwGA;EAAuB,kB3BqJU;AHwnGjC;;A8B5wGA;EAAsB,iB3BoJW;AH4nGjC;;A8BxwGA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,yB3BgSkC;E2B/RlC,gBAAgB;E1BwBZ,eAtCY;E0BgBhB,gB3B0L+B;E2BzL/B,gB3B8L+B;E2B7L/B,c3B7FgB;E2B8FhB,kBAAkB;EAClB,mBAAmB;EACnB,yB3BrGgB;E2BsGhB,yB3BpGgB;EONd,sBP6OgC;AHyoGpC;;A8BxxGA;;EAkBI,aAAa;A9B2wGjB;;A8BjwGA;;EAEE,gCZjB8D;AlBqxGhE;;A8BjwGA;;;;;;EAME,oB3B2QgC;ECtR5B,kBAtCY;E0BmDhB,gB3BgG+B;EOvO7B,qBP8O+B;AH8pGnC;;A8BjwGA;;EAEE,kCZlC8D;AlBsyGhE;;A8BjwGA;;;;;;EAME,uB3BqPiC;ECjR7B,mBAtCY;E0BoEhB,gB3BgF+B;EOxO7B,qBP+O+B;AH8qGnC;;A8BjwGA;;EAEE,sBAA0E;A9BowG5E;;A8BzvGA;;;;;;EpBzJI,0BoB+J4B;EpB9J5B,6BoB8J4B;A9B6vGhC;;A8B1vGA;;;;;;EpBpJI,yBoB0J2B;EpBzJ3B,4BoByJ2B;A9B8vG/B;;A+Bl7GA;EACE,kBAAkB;EAClB,cAAc;EACd,kBAA+C;EAC/C,oBAAqE;A/Bq7GvE;;A+Bl7GA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,kB5B6f0C;AHw7F5C;;A+Bl7GA;EACE,kBAAkB;EAClB,OAAO;EACP,WAAW;EACX,W5Byf0C;E4Bxf1C,eAAkF;EAClF,UAAU;A/Bq7GZ;;A+B37GA;EASI,W5BvBW;E4BwBX,qB5BKa;EmB9Bb,yBnB8Ba;AHk7GjB;;A+Bj8GA;EAoBM,gD5BLW;AHs7GjB;;A+Br8GA;EAyBI,qB5B0bsE;AHs/F1E;;A+Bz8GA;EA6BI,W5B3CW;E4B4CX,yB5Bsf8E;E4Brf9E,qB5Bqf8E;AH27FlF;;A+B/8GA;EAuCM,c5B/CY;AH29GlB;;A+Bn9GA;EA0CQ,yB5BtDU;AHm+GlB;;A+Bn6GA;EACE,kBAAkB;EAClB,gBAAgB;EAEhB,mBAAmB;A/Bq6GrB;;A+Bz6GA;EASI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5B4bwC;E4B3bxC,Y5B2bwC;E4B1bxC,oBAAoB;EACpB,WAAW;EACX,sB5BnFW;E4BoFX,yB5BoJ6B;AHgxGjC;;A+Bt7GA;EAwBI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5B6awC;E4B5axC,Y5B4awC;E4B3axC,WAAW;EACX,mCAAgE;A/Bk6GpE;;A+Bz5GA;ErB5GI,sBP6OgC;AH4xGpC;;A+B75GA;EAOM,kOb5EqI;AlBs+G3I;;A+Bj6GA;EAaM,qB5B1FW;EmB9Bb,yBnB8Ba;AHm/GjB;;A+Bt6GA;EAkBM,+KbvFqI;AlB++G3I;;A+B16GA;EAwBM,wC5BrGW;AH2/GjB;;A+B96GA;EA2BM,wC5BxGW;AH+/GjB;;A+B94GA;EAGI,kB5B8Z+C;AHi/FnD;;A+Bl5GA;EAQM,8KbjHqI;AlB+/G3I;;A+Bt5GA;EAcM,wC5B/HW;AH2gHjB;;A+Bl4GA;EACE,qBAA2D;A/Bq4G7D;;A+Bt4GA;EAKM,cAAqD;EACrD,c5BsY+E;E4BrY/E,mBAAmB;EAEnB,qB5BoY4E;AHggGlF;;A+B74GA;EAaM,wBb1E0D;Ea2E1D,0Bb3E0D;Ea4E1D,uBbxD0D;EayD1D,wBbzD0D;Ea0D1D,yB5BlLY;E4BoLZ,qB5B0X4E;EiBpjB5E,iJjBsgB+H;EiBtgB/H,yIjBsgB+H;EiBtgB/H,8KjBsgB+H;AHwjGrI;;AoBzjHI;EWkKJ;IXjKM,gBAAgB;EpB6jHpB;AACF;;A+B75GA;EA0BM,sB5BhMS;E4BiMT,sCAA4E;EAA5E,8BAA4E;A/Bu4GlF;;A+Bl6GA;EAiCM,wC5B1KW;AH+iHjB;;A+Bz3GA;EACE,qBAAqB;EACrB,WAAW;EACX,mCb7G8D;Ea8G9D,0C5BwKkC;ECvQ9B,eAtCY;E2BwIhB,gB5BkE+B;E4BjE/B,gB5BsE+B;E4BrE/B,c5BrNgB;E4BsNhB,sBAAsB;EACtB,uO5BuW+I;E4BtW/I,yB5B3NgB;EONd,sBP6OgC;E4BTlC,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B03GlB;;A+Bz4GA;EAkBI,qB5B4PsE;E4B3PtE,UAAU;EAIR,gD5B7MW;AHqkHjB;;A+B/4GA;EAgCM,c5B5OY;E4B6OZ,sB5BpPS;AHumHf;;A+Bp5GA;EAuCI,YAAY;EACZ,sB5BoIgC;E4BnIhC,sBAAsB;A/Bi3G1B;;A+B15GA;EA6CI,c5B1Pc;E4B2Pd,yB5B/Pc;AHgnHlB;;A+B/5GA;EAmDI,aAAa;A/Bg3GjB;;A+Bn6GA;EAwDI,kBAAkB;EAClB,0B5BrQc;AHonHlB;;A+B32GA;EACE,kCbxK8D;EayK9D,oB5BsHkC;E4BrHlC,uB5BqHkC;E4BpHlC,oB5BqHiC;ECjR7B,mBAtCY;AJijHlB;;A+B32GA;EACE,gCbhL8D;EaiL9D,mB5BmHiC;E4BlHjC,sB5BkHiC;E4BjHjC,kB5BkHgC;ECtR5B,kBAtCY;AJyjHlB;;A+Bt2GA;EACE,kBAAkB;EAClB,qBAAqB;EACrB,WAAW;EACX,mCbhM8D;EaiM9D,gBAAgB;A/By2GlB;;A+Bt2GA;EACE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mCbxM8D;EayM9D,SAAS;EACT,UAAU;A/By2GZ;;A+B/2GA;EASI,qB5B2KsE;E4B1KtE,gD5B1Ra;AHooHjB;;A+Bp3GA;;EAgBI,yB5B3Tc;AHoqHlB;;A+Bz3GA;EAqBM,iB5BkUQ;AHsiGd;;A+B73GA;EA0BI,0BAA0B;A/Bu2G9B;;A+Bn2GA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,OAAO;EACP,UAAU;EACV,mCbxO8D;EayO9D,yB5B6CkC;E4B3ClC,gB5BxD+B;E4ByD/B,gB5BpD+B;E4BqD/B,c5B/UgB;E4BgVhB,sB5BvVa;E4BwVb,yB5BpVgB;EONd,sBP6OgC;AHm9GpC;;A+Bn3GA;EAkBI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,UAAU;EACV,cAAc;EACd,6Bb1P4D;Ea2P5D,yB5B2BgC;E4B1BhC,gB5BpE6B;E4BqE7B,c5B/Vc;E4BgWd,iBAAiB;ETxWjB,yBnBGc;E4BuWd,oBAAoB;ErB3WpB,kCqB4WgF;A/Bq2GpF;;A+B31GA;EACE,WAAW;EACX,cbhR2B;EaiR3B,UAAU;EACV,6BAA6B;EAC7B,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B81GlB;;A+Bn2GA;EAQI,aAAa;A/B+1GjB;;A+Bv2GA;EAY8B,gE5BnWb;AHksHjB;;A+B32GA;EAa8B,gE5BpWb;AHssHjB;;A+B/2GA;EAc8B,gE5BrWb;AH0sHjB;;A+Bn3GA;EAkBI,SAAS;A/Bq2Gb;;A+Bv3GA;EAsBI,W5B2N6C;E4B1N7C,Y5B0N6C;E4BzN7C,oBAAyE;ET7YzE,yBnB8Ba;E4BiXb,S5B0N0C;EO1mB1C,mBP2mB6C;EiB1mB3C,oHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4BnHjI,wBAAgB;EAAhB,gBAAgB;A/Bo2GpB;;AoBlvHI;EWgXJ;IX/WM,wBAAgB;IAAhB,gBAAgB;EpBsvHpB;AACF;;A+Bx4GA;ETrXI,yBnB8mB2E;AHmpG/E;;A+B54GA;EAsCI,W5BoMoC;E4BnMpC,c5BoMqC;E4BnMrC,kBAAkB;EAClB,e5BmMuC;E4BlMvC,yB5B3Zc;E4B4Zd,yBAAyB;ErBjazB,mBPomBoC;AHwqGxC;;A+Bt5GA;EAiDI,W5BgM6C;E4B/L7C,Y5B+L6C;EmBtmB7C,yBnB8Ba;E4B2Yb,S5BgM0C;EO1mB1C,mBP2mB6C;EiB1mB3C,iHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4BzFjI,qBAAgB;EAAhB,gBAAgB;A/Bw2GpB;;AoBhxHI;EWgXJ;IX/WM,qBAAgB;IAAhB,gBAAgB;EpBoxHpB;AACF;;A+Bt6GA;ETrXI,yBnB8mB2E;AHirG/E;;A+B16GA;EAgEI,W5B0KoC;E4BzKpC,c5B0KqC;E4BzKrC,kBAAkB;EAClB,e5ByKuC;E4BxKvC,yB5Brbc;E4Bsbd,yBAAyB;ErB3bzB,mBPomBoC;AHssGxC;;A+Bp7GA;EA2EI,W5BsK6C;E4BrK7C,Y5BqK6C;E4BpK7C,aAAa;EACb,oB5B9D+B;E4B+D/B,mB5B/D+B;EmBrY/B,yBnB8Ba;E4Bwab,S5BmK0C;EO1mB1C,mBP2mB6C;EiB1mB3C,gHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4B5DjI,gBAAgB;A/B42GpB;;AoBjzHI;EWgXJ;IX/WM,oBAAgB;IAAhB,gBAAgB;EpBqzHpB;AACF;;A+Bv8GA;ETrXI,yBnB8mB2E;AHktG/E;;A+B38GA;EA6FI,W5B6IoC;E4B5IpC,c5B6IqC;E4B5IrC,kBAAkB;EAClB,e5B4IuC;E4B3IvC,6BAA6B;EAC7B,yBAAyB;EACzB,oBAA4C;A/Bk3GhD;;A+Br9GA;EAwGI,yB5Bzdc;EOLd,mBPomBoC;AH4uGxC;;A+B19GA;EA6GI,kBAAkB;EAClB,yB5B/dc;EOLd,mBPomBoC;AHkvGxC;;A+Bh+GA;EAoHM,yB5BneY;AHm1HlB;;A+Bp+GA;EAwHM,eAAe;A/Bg3GrB;;A+Bx+GA;EA4HM,yB5B3eY;AH21HlB;;A+B5+GA;EAgIM,eAAe;A/Bg3GrB;;A+Bh/GA;EAoIM,yB5BnfY;AHm2HlB;;A+B32GA;;;EX9fM,4GjBsgB+H;AHy2GrI;;AoB12HI;EWyfJ;;;IXxfM,gBAAgB;EpBg3HpB;AACF;;AgCx3HA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AhC23HlB;;AgCx3HA;EACE,cAAc;EACd,oB7B6qBsC;AH8sGxC;;AK13HE;E2BEE,qBAAqB;AhC43HzB;;AgCj4HA;EAUI,c7BVc;E6BWd,oBAAoB;EACpB,eAAe;AhC23HnB;;AgCn3HA;EACE,gC7BxBgB;AH84HlB;;AgCv3HA;EAII,mB7B0M6B;AH6qHjC;;AgC33HA;EAQI,6BAAgD;EtB3BhD,+BPoOgC;EOnOhC,gCPmOgC;AHgrHpC;;AKl5HE;E2B6BI,qC7BnCY;AH45HlB;;AgCr4HA;EAgBM,c7BpCY;E6BqCZ,6BAA6B;EAC7B,yBAAyB;AhCy3H/B;;AgC34HA;;EAwBI,c7B3Cc;E6B4Cd,sB7BnDW;E6BoDX,kC7BpDW;AH46Hf;;AgCl5HA;EA+BI,gB7B+K6B;EOjO7B,yBsBoD4B;EtBnD5B,0BsBmD4B;AhCu3HhC;;AgC92HA;EtBtEI,sBP6OgC;AH2sHpC;;AgCl3HA;;EAOI,W7B3EW;E6B4EX,yB7B/Ca;AH+5HjB;;AgCv2HA;EAEI,kBAAc;EAAd,cAAc;EACd,kBAAkB;AhCy2HtB;;AgCr2HA;EAEI,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,kBAAkB;AhCu2HtB;;AgC91HA;EAEI,aAAa;AhCg2HjB;;AgCl2HA;EAKI,cAAc;AhCi2HlB;;AiCr8HA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;EAC9B,oB9BiHW;AHu1Hb;;AiC98HA;;EAWI,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;AjCw8HlC;;AiCp7HA;EACE,qBAAqB;EACrB,sB9BqqB+E;E8BpqB/E,yB9BoqB+E;E8BnqB/E,kB9BiFW;ECTP,kBAtCY;E6BhChB,oBAAoB;EACpB,mBAAmB;AjCu7HrB;;AKj+HE;E4B6CE,qBAAqB;AjCw7HzB;;AiC/6HA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AjCk7HlB;;AiCv7HA;EAQI,gBAAgB;EAChB,eAAe;AjCm7HnB;;AiC57HA;EAaI,gBAAgB;EAChB,WAAW;AjCm7Hf;;AiC16HA;EACE,qBAAqB;EACrB,mB9B4lBuC;E8B3lBvC,sB9B2lBuC;AHk1GzC;;AiCj6HA;EACE,6BAAgB;EAAhB,gBAAgB;EAChB,oBAAY;EAAZ,YAAY;EAGZ,sBAAmB;EAAnB,mBAAmB;AjCk6HrB;;AiC95HA;EACE,wB9BumBwC;EC9lBpC,kBAtCY;E6B+BhB,cAAc;EACd,6BAA6B;EAC7B,6BAAuC;EvBrHrC,sBP6OgC;AH0yHpC;;AK5gIE;E4B8GE,qBAAqB;AjCk6HzB;;AiC55HA;EACE,qBAAqB;EACrB,YAAY;EACZ,aAAa;EACb,sBAAsB;EACtB,WAAW;EACX,mCAAmC;EACnC,0BAA0B;AjC+5H5B;;Acj+HI;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCw5HvB;AACF;;Act/HI;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC84HjC;EiCn6HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC84H3B;EiCt6HG;IA2BO,kBAAkB;EjC84H5B;EiCz6HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EH82GvC;EiC76HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC24HzB;EiCj7HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjC63HxB;EiCr7HG;IA4DK,aAAa;EjC43HrB;AACF;;AcrgII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjC47HvB;AACF;;Ac1hII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCk7HjC;EiCv8HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCk7H3B;EiC18HG;IA2BO,kBAAkB;EjCk7H5B;EiC78HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EHk5GvC;EiCj9HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC+6HzB;EiCr9HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCi6HxB;EiCz9HG;IA4DK,aAAa;EjCg6HrB;AACF;;AcziII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCg+HvB;AACF;;Ac9jII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCs9HjC;EiC3+HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCs9H3B;EiC9+HG;IA2BO,kBAAkB;EjCs9H5B;EiCj/HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EHs7GvC;EiCr/HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCm9HzB;EiCz/HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCq8HxB;EiC7/HG;IA4DK,aAAa;EjCo8HrB;AACF;;Ac7kII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCogIvB;AACF;;AclmII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC0/HjC;EiC/gIG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC0/H3B;EiClhIG;IA2BO,kBAAkB;EjC0/H5B;EiCrhIG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EH09GvC;EiCzhIG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCu/HzB;EiC7hIG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCy+HxB;EiCjiIG;IA4DK,aAAa;EjCw+HrB;AACF;;AiC1iIA;EAyBQ,yBAAqB;EAArB,qBAAqB;EACrB,oBAA2B;EAA3B,2BAA2B;AjCqhInC;;AiC/iIA;;EAQU,gBAAgB;EAChB,eAAe;AjC4iIzB;;AiCrjIA;EA6BU,uBAAmB;EAAnB,mBAAmB;AjC4hI7B;;AiCzjIA;EAgCY,kBAAkB;AjC6hI9B;;AiC7jIA;EAoCY,qB9BgiB6B;E8B/hB7B,oB9B+hB6B;AH8/GzC;;AiClkIA;;EA2CU,qBAAiB;EAAjB,iBAAiB;AjC4hI3B;;AiCvkIA;EA0DU,+BAAwB;EAAxB,wBAAwB;EAGxB,6BAAgB;EAAhB,gBAAgB;AjC+gI1B;;AiC5kIA;EAiEU,aAAa;AjC+gIvB;;AiClgIA;EAEI,yB9B/MW;AHmtIf;;AKptIE;E4BmNI,yB9BlNS;AHutIf;;AiC1gIA;EAWM,yB9BxNS;AH2tIf;;AK5tIE;E4B4NM,yB9B3NO;AH+tIf;;AiClhIA;EAkBQ,yB9B/NO;AHmuIf;;AiCthIA;;;;EA0BM,yB9BvOS;AH0uIf;;AiC7hIA;EA+BI,yB9B5OW;E8B6OX,gC9B7OW;AH+uIf;;AiCliIA;EAoCI,+QftNuI;AlBwtI3I;;AiCtiIA;EAwCI,yB9BrPW;AHuvIf;;AiC1iIA;EA0CM,yB9BvPS;AH2vIf;;AK5vIE;E4B2PM,yB9B1PO;AH+vIf;;AiC9/HA;EAEI,W9B7QW;AH6wIf;;AKpwIE;E4BuQI,W9BhRS;AHixIf;;AiCtgIA;EAWM,+B9BtRS;AHqxIf;;AK5wIE;E4BgRM,gC9BzRO;AHyxIf;;AiC9gIA;EAkBQ,gC9B7RO;AH6xIf;;AiClhIA;;;;EA0BM,W9BrSS;AHoyIf;;AiCzhIA;EA+BI,+B9B1SW;E8B2SX,sC9B3SW;AHyyIf;;AiC9hIA;EAoCI,qRf1QuI;AlBwwI3I;;AiCliIA;EAwCI,+B9BnTW;AHizIf;;AiCtiIA;EA0CM,W9BrTS;AHqzIf;;AK5yIE;E4B+SM,W9BxTO;AHyzIf;;AkC5zIA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,YAAY;EAEZ,qBAAqB;EACrB,sB/BJa;E+BKb,2BAA2B;EAC3B,sC/BIa;EOZX,sBP6OgC;AH0lIpC;;AkCx0IA;EAaI,eAAe;EACf,cAAc;AlC+zIlB;;AkC70IA;ExBUI,+BPoOgC;EOnOhC,gCPmOgC;AHomIpC;;AkCl1IA;ExBwBI,mCPsNgC;EOrNhC,kCPqNgC;AHymIpC;;AkCzzIA;EAGE,kBAAc;EAAd,cAAc;EAGd,eAAe;EACf,gB/BsxByC;AHkiH3C;;AkCpzIA;EACE,sB/BgxBwC;AHuiH1C;;AkCpzIA;EACE,qBAA+B;EAC/B,gBAAgB;AlCuzIlB;;AkCpzIA;EACE,gBAAgB;AlCuzIlB;;AKl2IE;E6BgDE,qBAAqB;AlCszIzB;;AkCxzIA;EAMI,oB/B+vBuC;AHujH3C;;AkC9yIA;EACE,wB/BsvByC;E+BrvBzC,gBAAgB;EAEhB,qC/B3Da;E+B4Db,6C/B5Da;AH42If;;AkCrzIA;ExBnEI,0DwB2E8E;AlCizIlF;;AkCzzIA;EAaM,aAAa;AlCgzInB;;AkC3yIA;EACE,wB/BouByC;E+BnuBzC,qC/B3Ea;E+B4Eb,0C/B5Ea;AH03If;;AkCjzIA;ExBrFI,0DQ+H4D;AlB2wIhE;;AkCtyIA;EACE,uBAAiC;EACjC,uB/BmtBwC;E+BltBxC,sBAAgC;EAChC,gBAAgB;AlCyyIlB;;AkCtyIA;EACE,uBAAiC;EACjC,sBAAgC;AlCyyIlC;;AkCryIA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,gB/B8sByC;AH0lH3C;;AkCryIA;;;EAGE,oBAAc;EAAd,cAAc;EACd,WAAW;AlCwyIb;;AkCryIA;;ExBxHI,2CQsH4D;ERrH5D,4CQqH4D;AlB6yIhE;;AkCtyIA;;ExB/GI,+CQwG4D;ERvG5D,8CQuG4D;AlBmzIhE;;AkCpyIA;EAEI,mB/BurBsD;AH+mH1D;;Ac/3II;EoBuFJ;IAMI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;IACnB,mB/BirBsD;I+BhrBtD,kB/BgrBsD;EHunHxD;EkChzIF;IAaM,gBAAY;IAAZ,YAAY;IACZ,kB/B2qBoD;I+B1qBpD,gBAAgB;IAChB,iB/ByqBoD;EH6nHxD;AACF;;AkC7xIA;EAII,mB/B2pBsD;AHkoH1D;;Acl5II;EoBiHJ;IAQI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;ElC8xIrB;EkCvyIF;IAcM,gBAAY;IAAZ,YAAY;IACZ,gBAAgB;ElC4xIpB;EkC3yIF;IAkBQ,cAAc;IACd,cAAc;ElC4xIpB;EkC/yIF;IxBxJI,0BwBiLoC;IxBhLpC,6BwBgLoC;ElC0xItC;EkCnzIF;;IA8BY,0BAA0B;ElCyxIpC;EkCvzIF;;IAmCY,6BAA6B;ElCwxIvC;EkC3zIF;IxB1II,yBwBkLmC;IxBjLnC,4BwBiLmC;ElCuxIrC;EkC/zIF;;IA6CY,yBAAyB;ElCsxInC;EkCn0IF;;IAkDY,4BAA4B;ElCqxItC;AACF;;AkCzwIA;EAEI,sB/BglBsC;AH2rH1C;;Ac77II;EoBgLJ;IAMI,uB/B6lBiC;I+B7lBjC,oB/B6lBiC;I+B7lBjC,e/B6lBiC;I+B5lBjC,2B/B6lBuC;I+B7lBvC,wB/B6lBuC;I+B7lBvC,mB/B6lBuC;I+B5lBvC,UAAU;IACV,SAAS;ElC4wIX;EkCrxIF;IAYM,qBAAqB;IACrB,WAAW;ElC4wIf;AACF;;AkCnwIA;EAEI,gBAAgB;AlCqwIpB;;AkCvwIA;EAKM,gBAAgB;ExB5OlB,6BwB6OiC;ExB5OjC,4BwB4OiC;AlCuwIrC;;AkC7wIA;ExBrPI,yBwB+P8B;ExB9P9B,0BwB8P8B;AlCwwIlC;;AkClxIA;ExB9PI,gBwB4Q0B;EACxB,mB/BnC2B;AH2yIjC;;AmC1hJA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,qBhC+hCsC;EgC9hCtC,mBhCiiCsC;EgC/hCtC,gBAAgB;EAChB,yBhCEgB;EOJd,sBP6OgC;AHkzIpC;;AmCzhJA;EAGI,oBhCqhCqC;AHqgHzC;;AmC7hJA;EAMM,qBAAqB;EACrB,qBhCihCmC;EgChhCnC,chCNY;EgCOZ,YhCshCuC;AHqgH7C;;AmCpiJA;EAoBI,0BAA0B;AnCohJ9B;;AmCxiJA;EAwBI,qBAAqB;AnCohJzB;;AmC5iJA;EA4BI,chC1Bc;AH8iJlB;;AoC3jJA;EACE,oBAAa;EAAb,aAAa;E7BGb,eAAe;EACf,gBAAgB;EGAd,sBP6OgC;AHg1IpC;;AoC5jJA;EACE,kBAAkB;EAClB,cAAc;EACd,uBjCgxBwC;EiC/wBxC,iBjCqO+B;EiCpO/B,iBjCmxBsC;EiClxBtC,cjCwBe;EiCvBf,sBjCNa;EiCOb,yBjCJgB;AHmkJlB;;AoCvkJA;EAWI,UAAU;EACV,cjCkK8D;EiCjK9D,qBAAqB;EACrB,yBjCXc;EiCYd,qBjCXc;AH2kJlB;;AoC/kJA;EAmBI,UAAU;EACV,UjC4wBiC;EiC3wBjC,gDjCSa;AHujJjB;;AoC5jJA;EAGM,cAAc;E1BChB,+BP+MgC;EO9MhC,kCP8MgC;AH+2IpC;;AoClkJA;E1BVI,gCP6NgC;EO5NhC,mCP4NgC;AHo3IpC;;AoCvkJA;EAcI,UAAU;EACV,WjCvCW;EiCwCX,yBjCXa;EiCYb,qBjCZa;AHykJjB;;AoC9kJA;EAqBI,cjCvCc;EiCwCd,oBAAoB;EAEpB,YAAY;EACZ,sBjCjDW;EiCkDX,qBjC/Cc;AH2mJlB;;AqClnJE;EACE,uBlCyxBsC;EC9pBpC,kBAtCY;EiCnFd,gBlCsO6B;AH+4IjC;;AqChnJM;E3BwBF,8BPgN+B;EO/M/B,iCP+M+B;AH64InC;;AqChnJM;E3BKF,+BP8N+B;EO7N/B,kCP6N+B;AHk5InC;;AqCloJE;EACE,uBlCuxBqC;EC5pBnC,mBAtCY;EiCnFd,gBlCuO6B;AH85IjC;;AqChoJM;E3BwBF,8BPiN+B;EOhN/B,iCPgN+B;AH45InC;;AqChoJM;E3BKF,+BP+N+B;EO9N/B,kCP8N+B;AHi6InC;;AsChpJA;EACE,qBAAqB;EACrB,qBnCw5BsC;ECv1BpC,cAAW;EkC/Db,gBnC2R+B;EmC1R/B,cAAc;EACd,kBAAkB;EAClB,mBAAmB;EACnB,wBAAwB;E5BRtB,sBP6OgC;EiB5O9B,qIjB6b6I;AH+tInJ;;AoBvpJI;EkBNJ;IlBOM,gBAAgB;EpB2pJpB;AACF;;AKxpJE;EiCGI,qBAAqB;AtCypJ3B;;AsCvqJA;EAoBI,aAAa;AtCupJjB;;AsClpJA;EACE,kBAAkB;EAClB,SAAS;AtCqpJX;;AsC9oJA;EACE,oBnC63BsC;EmC53BtC,mBnC43BsC;EOh6BpC,oBPm6BqC;AHmxHzC;;AsCzoJE;ECjDA,WpCMa;EoCLb,yBpCkCe;AH4pJjB;;AKhrJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC8rJxC;;AuCjsJU;EAQJ,UAAU;EACV,+CpCuBW;AHsqJjB;;AsCxpJE;ECjDA,WpCMa;EoCLb,yBpCWgB;AHksJlB;;AK/rJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC6sJxC;;AuChtJU;EAQJ,UAAU;EACV,iDpCAY;AH4sJlB;;AsCvqJE;ECjDA,WpCMa;EoCLb,yBpCyCe;AHmrJjB;;AK9sJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC4tJxC;;AuC/tJU;EAQJ,UAAU;EACV,+CpC8BW;AH6rJjB;;AsCtrJE;ECjDA,WpCMa;EoCLb,yBpC2Ce;AHgsJjB;;AK7tJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC2uJxC;;AuC9uJU;EAQJ,UAAU;EACV,gDpCgCW;AH0sJjB;;AsCrsJE;ECjDA,cpCegB;EoCdhB,yBpCwCe;AHktJjB;;AK5uJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvC0vJxC;;AuC7vJU;EAQJ,UAAU;EACV,+CpC6BW;AH4tJjB;;AsCptJE;ECjDA,WpCMa;EoCLb,yBpCsCe;AHmuJjB;;AK3vJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCywJxC;;AuC5wJU;EAQJ,UAAU;EACV,+CpC2BW;AH6uJjB;;AsCnuJE;ECjDA,cpCegB;EoCdhB,yBpCMgB;AHkxJlB;;AK1wJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvCwxJxC;;AuC3xJU;EAQJ,UAAU;EACV,iDpCLY;AH4xJlB;;AsClvJE;ECjDA,WpCMa;EoCLb,yBpCagB;AH0xJlB;;AKzxJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCuyJxC;;AuC1yJU;EAQJ,UAAU;EACV,8CpCEY;AHoyJlB;;AwCnzJA;EACE,kBAAoD;EACpD,mBrCqzBsC;EqCnzBtC,yBrCKgB;EOJd,qBP8O+B;AHukJnC;;Ac9vJI;E0B5DJ;IAQI,kBrC+yBoC;EHwgItC;AACF;;AwCpzJA;EACE,gBAAgB;EAChB,eAAe;E9BTb,gB8BUsB;AxCuzJ1B;;AyCl0JA;EACE,kBAAkB;EAClB,wBtCq9ByC;EsCp9BzC,mBtCq9BsC;EsCp9BtC,6BAA6C;E/BH3C,sBP6OgC;AH4lJpC;;AyCj0JA;EAEE,cAAc;AzCm0JhB;;AyC/zJA;EACE,gBtCgR+B;AHkjJjC;;AyC1zJA;EACE,mBAAsD;AzC6zJxD;;AyC9zJA;EAKI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,wBtCu7BuC;EsCt7BvC,cAAc;AzC6zJlB;;AyCnzJE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBywJlE;;A0Cn2JE;EACE,yBAAqC;A1Cs2JzC;;A0Cn2JE;EACE,cAA0B;A1Cs2J9B;;AyCj0JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBuxJlE;;A0Cj3JE;EACE,yBAAqC;A1Co3JzC;;A0Cj3JE;EACE,cAA0B;A1Co3J9B;;AyC/0JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBqyJlE;;A0C/3JE;EACE,yBAAqC;A1Ck4JzC;;A0C/3JE;EACE,cAA0B;A1Ck4J9B;;AyC71JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBmzJlE;;A0C74JE;EACE,yBAAqC;A1Cg5JzC;;A0C74JE;EACE,cAA0B;A1Cg5J9B;;AyC32JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBi0JlE;;A0C35JE;EACE,yBAAqC;A1C85JzC;;A0C35JE;EACE,cAA0B;A1C85J9B;;AyCz3JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB+0JlE;;A0Cz6JE;EACE,yBAAqC;A1C46JzC;;A0Cz6JE;EACE,cAA0B;A1C46J9B;;AyCv4JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB61JlE;;A0Cv7JE;EACE,yBAAqC;A1C07JzC;;A0Cv7JE;EACE,cAA0B;A1C07J9B;;AyCr5JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB22JlE;;A0Cr8JE;EACE,yBAAqC;A1Cw8JzC;;A0Cr8JE;EACE,cAA0B;A1Cw8J9B;;A2Ch9JE;EACE;IAAO,2BAAuC;E3Co9JhD;E2Cn9JE;IAAK,wBAAwB;E3Cs9J/B;AACF;;A2Cz9JE;EACE;IAAO,2BAAuC;E3Co9JhD;E2Cn9JE;IAAK,wBAAwB;E3Cs9J/B;AACF;;A2Cn9JA;EACE,oBAAa;EAAb,aAAa;EACb,YxC89BsC;EwC79BtC,gBAAgB;EvCoHZ,kBAtCY;EuC5EhB,yBxCJgB;EOJd,sBP6OgC;AHkvJpC;;A2Cl9JA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,gBAAgB;EAChB,WxChBa;EwCiBb,kBAAkB;EAClB,mBAAmB;EACnB,yBxCUe;EiB9BX,2BjB0+B4C;AHggIlD;;AoBr+JI;EuBOJ;IvBNM,gBAAgB;EpBy+JpB;AACF;;A2Cx9JA;ErBaE,qMAA6I;EqBX7I,0BxCw8BsC;AHmhIxC;;A2Cv9JE;EACE,0DxC08BkD;EwC18BlD,kDxC08BkD;AHghItD;;A2Cv9JM;EAJJ;IAKM,uBAAe;IAAf,eAAe;E3C29JrB;AACF;;A4CrgKA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;A5CwgKzB;;A4CrgKA;EACE,WAAO;EAAP,OAAO;A5CwgKT;;A6C1gKA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EAGtB,eAAe;EACf,gBAAgB;A7C2gKlB;;A6ClgKA;EACE,WAAW;EACX,c1CPgB;E0CQhB,mBAAmB;A7CqgKrB;;AK3gKE;EwCUE,UAAU;EACV,c1Cbc;E0Ccd,qBAAqB;EACrB,yB1CrBc;AH0hKlB;;A6C/gKA;EAcI,c1CjBc;E0CkBd,yB1CzBc;AH8hKlB;;A6C5/JA;EACE,kBAAkB;EAClB,cAAc;EACd,wB1C88ByC;E0C58BzC,sB1CzCa;E0C0Cb,sC1ChCa;AH8hKf;;A6CpgKA;EnC7BI,+BPoOgC;EOnOhC,gCPmOgC;AHk0JpC;;A6CzgKA;EnCfI,mCPsNgC;EOrNhC,kCPqNgC;AHu0JpC;;A6C9gKA;EAkBI,c1ChDc;E0CiDd,oBAAoB;EACpB,sB1CxDW;AHwjKf;;A6CphKA;EAyBI,UAAU;EACV,W1C9DW;E0C+DX,yB1ClCa;E0CmCb,qB1CnCa;AHkiKjB;;A6C3hKA;EAgCI,mBAAmB;A7C+/JvB;;A6C/hKA;EAmCM,gB1CiK2B;E0ChK3B,qB1CgK2B;AHg2JjC;;A6Cl/JI;EACE,uBAAmB;EAAnB,mBAAmB;A7Cq/JzB;;A6Ct/JI;EnCjCA,kCPsLgC;EOlMhC,0BmCmDwC;A7Cq/J5C;;A6C3/JI;EnC7CA,gCPkMgC;EOtLhC,4BmC4C0C;A7Cq/J9C;;A6ChgKI;EAeM,aAAa;A7Cq/JvB;;A6CpgKI;EAmBM,qB1C+HuB;E0C9HvB,oBAAoB;A7Cq/J9B;;A6CzgKI;EAuBQ,iB1C2HqB;E0C1HrB,sB1C0HqB;AH43JjC;;Ac/iKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CkhKvB;E6CnhKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7CihK1C;E6CvhKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CghK5C;E6C3hKE;IAeM,aAAa;E7C+gKrB;E6C9hKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7C8gK5B;E6CliKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EHo5J/B;AACF;;AcxkKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7C2iKvB;E6C5iKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7C0iK1C;E6ChjKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CyiK5C;E6CpjKE;IAeM,aAAa;E7CwiKrB;E6CvjKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CuiK5B;E6C3jKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EH66J/B;AACF;;AcjmKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CokKvB;E6CrkKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7CmkK1C;E6CzkKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CkkK5C;E6C7kKE;IAeM,aAAa;E7CikKrB;E6ChlKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CgkK5B;E6CplKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EHs8J/B;AACF;;Ac1nKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7C6lKvB;E6C9lKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7C4lK1C;E6ClmKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7C2lK5C;E6CtmKE;IAeM,aAAa;E7C0lKrB;E6CzmKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CylK5B;E6C7mKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EH+9J/B;AACF;;A6C5kKA;EAEI,qBAAqB;EACrB,oBAAoB;EnCjIpB,gBmCkIwB;A7C8kK5B;;A6CllKA;EAOM,mBAAmB;A7C+kKzB;;A6CtlKA;EAaM,sBAAsB;A7C6kK5B;;A8C1tKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmoKlE;;AKltKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6tKjD;;A8CpuKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8oKlE;;A8C1uKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmpKlE;;AKluKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6uKjD;;A8CpvKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8pKlE;;A8C1vKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmqKlE;;AKlvKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6vKjD;;A8CpwKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8qKlE;;A8C1wKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmrKlE;;AKlwKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6wKjD;;A8CpxKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8rKlE;;A8C1xKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmsKlE;;AKlxKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6xKjD;;A8CpyKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8sKlE;;A8C1yKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmtKlE;;AKlyKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6yKjD;;A8CpzKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8tKlE;;A8C1zKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmuKlE;;AKlzKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6zKjD;;A8Cp0KE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8uKlE;;A8C10KE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmvKlE;;AKl0KE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C60KjD;;A8Cp1KE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8vKlE;;A+C71KA;EACE,YAAY;E3C8HR,iBAtCY;E2CtFhB,gB5CiS+B;E4ChS/B,cAAc;EACd,W5CYa;E4CXb,yB5CCa;E4CAb,WAAW;A/Cg2Kb;;AK31KE;E0CDE,W5CMW;E4CLX,qBAAqB;A/Cg2KzB;;AK51KE;E0CCI,YAAY;A/C+1KlB;;A+Cp1KA;EACE,UAAU;EACV,6BAA6B;EAC7B,SAAS;EACT,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/Cu1KlB;;A+Cj1KA;EACE,oBAAoB;A/Co1KtB;;AgD33KA;EACE,gB7Cy4BuC;E6Cx4BvC,gBAAgB;E5C6HZ,mBAtCY;E4CpFhB,2C7CEa;E6CDb,4BAA4B;EAC5B,oC7C04BmD;E6Cz4BnD,gD7CSa;E6CRb,mCAA2B;EAA3B,2BAA2B;EAC3B,UAAU;EtCLR,sBP64BsC;AHs/I1C;;AgDx4KA;EAcI,sB7C63BsC;AHigJ1C;;AgD54KA;EAkBI,UAAU;AhD83Kd;;AgDh5KA;EAsBI,cAAc;EACd,UAAU;AhD83Kd;;AgDr5KA;EA2BI,aAAa;AhD83KjB;;AgD13KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,wB7Cy2BwC;E6Cx2BxC,c7CtBgB;E6CuBhB,2C7C7Ba;E6C8Bb,4BAA4B;EAC5B,4C7Ci3BoD;AH4gJtD;;AgD13KA;EACE,gB7Ci2BwC;AH4hJ1C;;AiDj6KA;EAEE,gBAAgB;AjDm6KlB;;AiDr6KA;EAKI,kBAAkB;EAClB,gBAAgB;AjDo6KpB;;AiD/5KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9C+pBsC;E8C9pBtC,aAAa;EACb,WAAW;EACX,YAAY;EACZ,gBAAgB;EAGhB,UAAU;AjDg6KZ;;AiDz5KA;EACE,kBAAkB;EAClB,WAAW;EACX,c9C64BuC;E8C34BvC,oBAAoB;AjD25KtB;;AiDx5KE;E7BrCI,2CjB48BoD;EiB58BpD,mCjB48BoD;EiB58BpD,oEjB48BoD;E8Cr6BtD,sC9Cm6BmD;E8Cn6BnD,8B9Cm6BmD;AHw/IvD;;AoB77KI;E6BgCF;I7B/BI,gBAAgB;EpBi8KpB;AACF;;AiD/5KE;EACE,uB9Ci6BoC;E8Cj6BpC,e9Ci6BoC;AHigJxC;;AiD95KE;EACE,8B9C85B2C;E8C95B3C,sB9C85B2C;AHmgJ/C;;AiD75KA;EACE,oBAAa;EAAb,aAAa;EACb,6B/ByE8D;AlBu1KhE;;AiDl6KA;EAKI,8B/BsE4D;E+BrE5D,gBAAgB;AjDi6KpB;;AiDv6KA;;EAWI,oBAAc;EAAd,cAAc;AjDi6KlB;;AiD56KA;EAeI,gBAAgB;AjDi6KpB;;AiD75KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,6B/BqD8D;AlB22KhE;;AiDn6KA;EAOI,cAAc;EACd,0B/BgD4D;E+B/C5D,WAAW;AjDg6Kf;;AiDz6KA;EAcI,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,YAAY;AjD+5KhB;;AiD/6KA;EAmBM,gBAAgB;AjDg6KtB;;AiDn7KA;EAuBM,aAAa;AjDg6KnB;;AiD15KA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,WAAW;EAGX,oBAAoB;EACpB,sB9C1Ga;E8C2Gb,4BAA4B;EAC5B,oC9ClGa;EOZX,qBP8O+B;E8C5HjC,UAAU;AjDy5KZ;;AiDr5KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9CojBsC;E8CnjBtC,YAAY;EACZ,aAAa;EACb,sB9CjHa;AHygLf;;AiD/5KA;EAUW,UAAU;AjDy5KrB;;AiDn6KA;EAWW,Y9C4zB2B;AHgmJtC;;AiDv5KA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,sBAA8B;EAA9B,8BAA8B;EAC9B,kB9CyzBsC;E8CxzBtC,gC9CtIgB;EOId,0CQsH4D;ERrH5D,2CQqH4D;AlBw6KhE;;AiDj6KA;EASI,kB9CozBoC;E8ClzBpC,8BAA6F;AjD25KjG;;AiDt5KA;EACE,gBAAgB;EAChB,gB9C2I+B;AH8wKjC;;AiDp5KA;EACE,kBAAkB;EAGlB,kBAAc;EAAd,cAAc;EACd,a9CuwBsC;AH8oJxC;;AiDj5KA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,kBAAyB;EAAzB,yBAAyB;EACzB,gBAAgE;EAChE,6B9CvKgB;EOkBd,8CQwG4D;ERvG5D,6CQuG4D;AlBm8KhE;;AiD55KA;EAcI,eAAwC;AjDk5K5C;;AiD74KA;EACE,kBAAkB;EAClB,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,gBAAgB;AjDg5KlB;;AcvhLI;EmCzBJ;IAuKI,gB9CmwBqC;I8ClwBrC,oBAAyC;EjD84K3C;EiDhiLF;IAsJI,+B/B3E4D;ElBw9K9D;EiDniLF;IAyJM,gC/B9E0D;ElB29K9D;EiDnhLF;IA2II,+B/BnF4D;ElB89K9D;EiDthLF;IA8IM,4B/BtF0D;ElBi+K9D;EiDn4KA;IAAY,gB9C4uB2B;EH0pJvC;AACF;;Ac7iLI;EmC0KF;;IAEE,gB9CouBqC;EHmqJvC;AACF;;AcpjLI;EmCiLF;IAAY,iB9C8tB4B;EH0qJxC;AACF;;AkDrnLA;EACE,kBAAkB;EAClB,a/CmrBsC;E+ClrBtC,cAAc;EACd,S/Cy1BmC;EgD71BnC,kMhDuRiN;EgDrRjN,kBAAkB;EAClB,gBhD+R+B;EgD9R/B,gBhDmS+B;EgDlS/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;E8C9EhB,qBAAqB;EACrB,UAAU;AlDkoLZ;;AkD7oLA;EAaW,Y/C60B2B;AHuzJtC;;AkDjpLA;EAgBI,kBAAkB;EAClB,cAAc;EACd,a/C60BqC;E+C50BrC,c/C60BqC;AHwzJzC;;AkDxpLA;EAsBM,kBAAkB;EAClB,WAAW;EACX,yBAAyB;EACzB,mBAAmB;AlDsoLzB;;AkDjoLA;EACE,iBAAgC;AlDooLlC;;AkDroLA;EAII,SAAS;AlDqoLb;;AkDzoLA;EAOM,MAAM;EACN,6BAAgE;EAChE,sB/CvBS;AH6pLf;;AkDjoLA;EACE,iB/CmzBuC;AHi1JzC;;AkDroLA;EAII,OAAO;EACP,a/C+yBqC;E+C9yBrC,c/C6yBqC;AHw1JzC;;AkD3oLA;EASM,QAAQ;EACR,oCAA2F;EAC3F,wB/CvCS;AH6qLf;;AkDjoLA;EACE,iBAAgC;AlDooLlC;;AkDroLA;EAII,MAAM;AlDqoLV;;AkDzoLA;EAOM,SAAS;EACT,6B/C4xBmC;E+C3xBnC,yB/CrDS;AH2rLf;;AkDjoLA;EACE,iB/CqxBuC;AH+2JzC;;AkDroLA;EAII,QAAQ;EACR,a/CixBqC;E+ChxBrC,c/C+wBqC;AHs3JzC;;AkD3oLA;EASM,OAAO;EACP,oC/C4wBmC;E+C3wBnC,uB/CrES;AH2sLf;;AkDjnLA;EACE,gB/C2uBuC;E+C1uBvC,uB/CgvBuC;E+C/uBvC,W/CvGa;E+CwGb,kBAAkB;EAClB,sB/C/Fa;EOZX,sBP6OgC;AHm/KpC;;AoDruLA;EACE,kBAAkB;EAClB,MAAM;EACN,OAAO;EACP,ajDirBsC;EiDhrBtC,cAAc;EACd,gBjD22BuC;EgDh3BvC,kMhDuRiN;EgDrRjN,kBAAkB;EAClB,gBhD+R+B;EgD9R/B,gBhDmS+B;EgDlS/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;EgD7EhB,qBAAqB;EACrB,sBjDNa;EiDOb,4BAA4B;EAC5B,oCjDEa;EOZX,qBP8O+B;AH+gLnC;;AoDlwLA;EAoBI,kBAAkB;EAClB,cAAc;EACd,WjD22BoC;EiD12BpC,cjD22BqC;EiD12BrC,gBjD2N+B;AHuhLnC;;AoD1wLA;EA4BM,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,yBAAyB;EACzB,mBAAmB;ApDkvLzB;;AoD7uLA;EACE,qBjD41BuC;AHo5JzC;;AoDjvLA;EAII,2BlC2F4D;AlBspLhE;;AoDrvLA;EAOM,SAAS;EACT,6BAAgE;EAChE,qCjDu1BiE;AH25JvE;;AoD3vLA;EAaM,WjD6L2B;EiD5L3B,6BAAgE;EAChE,sBjD7CS;AH+xLf;;AoD7uLA;EACE,mBjDw0BuC;AHw6JzC;;AoDjvLA;EAII,yBlCuE4D;EkCtE5D,ajDo0BqC;EiDn0BrC,YjDk0BoC;EiDj0BpC,gBAAgC;ApDivLpC;;AoDxvLA;EAUM,OAAO;EACP,oCAA2F;EAC3F,uCjDg0BiE;AHk7JvE;;AoD9vLA;EAgBM,SjDsK2B;EiDrK3B,oCAA2F;EAC3F,wBjDpES;AHszLf;;AoD7uLA;EACE,kBjDizBuC;AH+7JzC;;AoDjvLA;EAII,wBlCgD4D;AlBisLhE;;AoDrvLA;EAOM,MAAM;EACN,oCAA2F;EAC3F,wCjD4yBiE;AHs8JvE;;AoD3vLA;EAaM,QjDkJ2B;EiDjJ3B,oCAA2F;EAC3F,yBjDxFS;AH00Lf;;AoDjwLA;EAqBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,cAAc;EACd,WjDwxBoC;EiDvxBpC,oBAAsC;EACtC,WAAW;EACX,gCjD4wBuD;AHo+J3D;;AoD5uLA;EACE,oBjDixBuC;AH89JzC;;AoDhvLA;EAII,0BlCgB4D;EkCf5D,ajD6wBqC;EiD5wBrC,YjD2wBoC;EiD1wBpC,gBAAgC;ApDgvLpC;;AoDvvLA;EAUM,QAAQ;EACR,oCjDuwBmC;EiDtwBnC,sCjDywBiE;AHw+JvE;;AoD7vLA;EAgBM,UjD+G2B;EiD9G3B,oCjDiwBmC;EiDhwBnC,uBjD3HS;AH42Lf;;AoD3tLA;EACE,uBjDkuBwC;EiDjuBxC,gBAAgB;EhD3BZ,eAtCY;EgDoEhB,yBjD2tByD;EiD1tBzD,gCAAyE;E1ChJvE,0CQsH4D;ERrH5D,2CQqH4D;AlByvLhE;;AoDruLA;EAUI,aAAa;ApD+tLjB;;AoD3tLA;EACE,uBjDotBwC;EiDntBxC,cjDxJgB;AHs3LlB;;AqDz3LA;EACE,kBAAkB;ArD43LpB;;AqDz3LA;EACE,uBAAmB;EAAnB,mBAAmB;ArD43LrB;;AqDz3LA;EACE,kBAAkB;EAClB,WAAW;EACX,gBAAgB;ArD43LlB;;AsDn5LE;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtDs5Lf;;AqD93LA;EACE,kBAAkB;EAClB,aAAa;EACb,WAAW;EACX,WAAW;EACX,mBAAmB;EACnB,mCAA2B;EAA3B,2BAA2B;EjC5BvB,8CjBikCkF;EiBjkClF,sCjBikCkF;EiBjkClF,0EjBikCkF;AH61JxF;;AoBz5LI;EiCiBJ;IjChBM,gBAAgB;EpB65LpB;AACF;;AqDp4LA;;;EAGE,cAAc;ArDu4LhB;;AqDp4LA;;EAEE,mCAA2B;EAA3B,2BAA2B;ArDu4L7B;;AqDp4LA;;EAEE,oCAA4B;EAA5B,4BAA4B;ArDu4L9B;;AqD/3LA;EAEI,UAAU;EACV,4BAA4B;EAC5B,uBAAe;EAAf,eAAe;ArDi4LnB;;AqDr4LA;;;EAUI,UAAU;EACV,UAAU;ArDi4Ld;;AqD54LA;;EAgBI,UAAU;EACV,UAAU;EjCtER,2BjBgkCkC;AHw4JxC;;AoBn8LI;EiCgDJ;;IjC/CM,gBAAgB;EpBw8LpB;AACF;;AqD/3LA;;EAEE,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,UAAU;EAEV,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,qBAAuB;EAAvB,uBAAuB;EACvB,UlDk9BsC;EkDj9BtC,WlD1Fa;EkD2Fb,kBAAkB;EAClB,YlDg9BqC;EiB7iCjC,8BjB+iCgD;AHg7JtD;;AoB19LI;EiC2EJ;;IjC1EM,gBAAgB;EpB+9LpB;AACF;;AK59LE;;;EgDwFE,WlDjGW;EkDkGX,qBAAqB;EACrB,UAAU;EACV,YlDy8BmC;AHi8JvC;;AqDv4LA;EACE,OAAO;ArD04LT;;AqDr4LA;EACE,QAAQ;ArDw4LV;;AqDj4LA;;EAEE,qBAAqB;EACrB,WlDk8BuC;EkDj8BvC,YlDi8BuC;EkDh8BvC,qCAAqC;ArDo4LvC;;AqDl4LA;EACE,sNnCxFyI;AlB69L3I;;AqDn4LA;EACE,uNnC3FyI;AlBi+L3I;;AqD73LA;EACE,kBAAkB;EAClB,QAAQ;EACR,SAAS;EACT,OAAO;EACP,WAAW;EACX,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,eAAe;EAEf,iBlDw5BsC;EkDv5BtC,gBlDu5BsC;EkDt5BtC,gBAAgB;ArD+3LlB;;AqD34LA;EAeI,uBAAuB;EACvB,kBAAc;EAAd,cAAc;EACd,WlDs5BqC;EkDr5BrC,WlDs5BoC;EkDr5BpC,iBlDu5BoC;EkDt5BpC,gBlDs5BoC;EkDr5BpC,mBAAmB;EACnB,eAAe;EACf,sBlDhKW;EkDiKX,4BAA4B;EAE5B,kCAAiE;EACjE,qCAAoE;EACpE,WAAW;EjCtKT,6BjBsjC+C;AHg/JrD;;AoBjiMI;EiCqIJ;IjCpIM,gBAAgB;EpBqiMpB;AACF;;AqDl6LA;EAiCI,UAAU;ArDq4Ld;;AqD53LA;EACE,kBAAkB;EAClB,UAA2C;EAC3C,YAAY;EACZ,SAA0C;EAC1C,WAAW;EACX,iBAAiB;EACjB,oBAAoB;EACpB,WlD3La;EkD4Lb,kBAAkB;ArD+3LpB;;AuD9jMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvDkkM9B;AACF;;AuDpkMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvDkkM9B;AACF;;AuDhkMA;EACE,qBAAqB;EACrB,WpDkkC0B;EoDjkC1B,YpDikC0B;EoDhkC1B,2BAA2B;EAC3B,iCAAgD;EAChD,+BAA+B;EAE/B,kBAAkB;EAClB,sDAA8C;EAA9C,8CAA8C;AvDkkMhD;;AuD/jMA;EACE,WpD2jC4B;EoD1jC5B,YpD0jC4B;EoDzjC5B,mBpD2jC4B;AHugK9B;;AuD3jMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvD8jMrB;EuD5jMA;IACE,UAAU;EvD8jMZ;AACF;;AuDpkMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvD8jMrB;EuD5jMA;IACE,UAAU;EvD8jMZ;AACF;;AuD3jMA;EACE,qBAAqB;EACrB,WpDmiC0B;EoDliC1B,YpDkiC0B;EoDjiC1B,2BAA2B;EAC3B,8BAA8B;EAE9B,kBAAkB;EAClB,UAAU;EACV,oDAA4C;EAA5C,4CAA4C;AvD6jM9C;;AuD1jMA;EACE,WpD4hC4B;EoD3hC5B,YpD2hC4B;AHkiK9B;;AwDhnMA;EAAqB,mCAAmC;AxDonMxD;;AwDnnMA;EAAqB,8BAA8B;AxDunMnD;;AwDtnMA;EAAqB,iCAAiC;AxD0nMtD;;AwDznMA;EAAqB,iCAAiC;AxD6nMtD;;AwD5nMA;EAAqB,sCAAsC;AxDgoM3D;;AwD/nMA;EAAqB,mCAAmC;AxDmoMxD;;AyDroME;EACE,oCAAmC;AzDwoMvC;;AK9nME;;;EoDLI,oCAAgD;AzDyoMtD;;AyD/oME;EACE,oCAAmC;AzDkpMvC;;AKxoME;;;EoDLI,oCAAgD;AzDmpMtD;;AyDzpME;EACE,oCAAmC;AzD4pMvC;;AKlpME;;;EoDLI,oCAAgD;AzD6pMtD;;AyDnqME;EACE,oCAAmC;AzDsqMvC;;AK5pME;;;EoDLI,oCAAgD;AzDuqMtD;;AyD7qME;EACE,oCAAmC;AzDgrMvC;;AKtqME;;;EoDLI,oCAAgD;AzDirMtD;;AyDvrME;EACE,oCAAmC;AzD0rMvC;;AKhrME;;;EoDLI,oCAAgD;AzD2rMtD;;AyDjsME;EACE,oCAAmC;AzDosMvC;;AK1rME;;;EoDLI,oCAAgD;AzDqsMtD;;AyD3sME;EACE,oCAAmC;AzD8sMvC;;AKpsME;;;EoDLI,oCAAgD;AzD+sMtD;;A0D9sMA;EACE,iCAAmC;A1DitMrC;;A0D9sMA;EACE,wCAAwC;A1DitM1C;;A2D5tMA;EAAkB,oCAAoD;A3DguMtE;;A2D/tMA;EAAkB,wCAAwD;A3DmuM1E;;A2DluMA;EAAkB,0CAA0D;A3DsuM5E;;A2DruMA;EAAkB,2CAA2D;A3DyuM7E;;A2DxuMA;EAAkB,yCAAyD;A3D4uM3E;;A2D1uMA;EAAmB,oBAAoB;A3D8uMvC;;A2D7uMA;EAAmB,wBAAwB;A3DivM3C;;A2DhvMA;EAAmB,0BAA0B;A3DovM7C;;A2DnvMA;EAAmB,2BAA2B;A3DuvM9C;;A2DtvMA;EAAmB,yBAAyB;A3D0vM5C;;A2DvvME;EACE,gCAA+B;A3D0vMnC;;A2D3vME;EACE,gCAA+B;A3D8vMnC;;A2D/vME;EACE,gCAA+B;A3DkwMnC;;A2DnwME;EACE,gCAA+B;A3DswMnC;;A2DvwME;EACE,gCAA+B;A3D0wMnC;;A2D3wME;EACE,gCAA+B;A3D8wMnC;;A2D/wME;EACE,gCAA+B;A3DkxMnC;;A2DnxME;EACE,gCAA+B;A3DsxMnC;;A2DlxMA;EACE,6BAA+B;A3DqxMjC;;A2D9wMA;EACE,gCAA2C;A3DixM7C;;A2D9wMA;EACE,iCAAwC;A3DixM1C;;A2D9wMA;EACE,0CAAiD;EACjD,2CAAkD;A3DixMpD;;A2D9wMA;EACE,2CAAkD;EAClD,8CAAqD;A3DixMvD;;A2D9wMA;EACE,8CAAqD;EACrD,6CAAoD;A3DixMtD;;A2D9wMA;EACE,0CAAiD;EACjD,6CAAoD;A3DixMtD;;A2D9wMA;EACE,gCAA2C;A3DixM7C;;A2D9wMA;EACE,6BAA6B;A3DixM/B;;A2D9wMA;EACE,+BAAuC;A3DixMzC;;A2D9wMA;EACE,2BAA2B;A3DixM7B;;AsDz1ME;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtD41Mf;;A4Dr1MM;EAAwB,wBAA0B;A5Dy1MxD;;A4Dz1MM;EAAwB,0BAA0B;A5D61MxD;;A4D71MM;EAAwB,gCAA0B;A5Di2MxD;;A4Dj2MM;EAAwB,yBAA0B;A5Dq2MxD;;A4Dr2MM;EAAwB,yBAA0B;A5Dy2MxD;;A4Dz2MM;EAAwB,6BAA0B;A5D62MxD;;A4D72MM;EAAwB,8BAA0B;A5Di3MxD;;A4Dj3MM;EAAwB,+BAA0B;EAA1B,wBAA0B;A5Dq3MxD;;A4Dr3MM;EAAwB,sCAA0B;EAA1B,+BAA0B;A5Dy3MxD;;Acx0MI;E8CjDE;IAAwB,wBAA0B;E5D83MtD;E4D93MI;IAAwB,0BAA0B;E5Di4MtD;E4Dj4MI;IAAwB,gCAA0B;E5Do4MtD;E4Dp4MI;IAAwB,yBAA0B;E5Du4MtD;E4Dv4MI;IAAwB,yBAA0B;E5D04MtD;E4D14MI;IAAwB,6BAA0B;E5D64MtD;E4D74MI;IAAwB,8BAA0B;E5Dg5MtD;E4Dh5MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Dm5MtD;E4Dn5MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Ds5MtD;AACF;;Act2MI;E8CjDE;IAAwB,wBAA0B;E5D45MtD;E4D55MI;IAAwB,0BAA0B;E5D+5MtD;E4D/5MI;IAAwB,gCAA0B;E5Dk6MtD;E4Dl6MI;IAAwB,yBAA0B;E5Dq6MtD;E4Dr6MI;IAAwB,yBAA0B;E5Dw6MtD;E4Dx6MI;IAAwB,6BAA0B;E5D26MtD;E4D36MI;IAAwB,8BAA0B;E5D86MtD;E4D96MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Di7MtD;E4Dj7MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Do7MtD;AACF;;Acp4MI;E8CjDE;IAAwB,wBAA0B;E5D07MtD;E4D17MI;IAAwB,0BAA0B;E5D67MtD;E4D77MI;IAAwB,gCAA0B;E5Dg8MtD;E4Dh8MI;IAAwB,yBAA0B;E5Dm8MtD;E4Dn8MI;IAAwB,yBAA0B;E5Ds8MtD;E4Dt8MI;IAAwB,6BAA0B;E5Dy8MtD;E4Dz8MI;IAAwB,8BAA0B;E5D48MtD;E4D58MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D+8MtD;E4D/8MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Dk9MtD;AACF;;Acl6MI;E8CjDE;IAAwB,wBAA0B;E5Dw9MtD;E4Dx9MI;IAAwB,0BAA0B;E5D29MtD;E4D39MI;IAAwB,gCAA0B;E5D89MtD;E4D99MI;IAAwB,yBAA0B;E5Di+MtD;E4Dj+MI;IAAwB,yBAA0B;E5Do+MtD;E4Dp+MI;IAAwB,6BAA0B;E5Du+MtD;E4Dv+MI;IAAwB,8BAA0B;E5D0+MtD;E4D1+MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D6+MtD;E4D7+MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Dg/MtD;AACF;;A4Dv+MA;EAEI;IAAqB,wBAA0B;E5D0+MjD;E4D1+ME;IAAqB,0BAA0B;E5D6+MjD;E4D7+ME;IAAqB,gCAA0B;E5Dg/MjD;E4Dh/ME;IAAqB,yBAA0B;E5Dm/MjD;E4Dn/ME;IAAqB,yBAA0B;E5Ds/MjD;E4Dt/ME;IAAqB,6BAA0B;E5Dy/MjD;E4Dz/ME;IAAqB,8BAA0B;E5D4/MjD;E4D5/ME;IAAqB,+BAA0B;IAA1B,wBAA0B;E5D+/MjD;E4D//ME;IAAqB,sCAA0B;IAA1B,+BAA0B;E5DkgNjD;AACF;;A6DxhNA;EACE,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,UAAU;EACV,gBAAgB;A7D2hNlB;;A6DhiNA;EAQI,cAAc;EACd,WAAW;A7D4hNf;;A6DriNA;;;;;EAiBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,OAAO;EACP,WAAW;EACX,YAAY;EACZ,SAAS;A7D4hNb;;A6DphNE;EAEI,uBAA4F;A7DshNlG;;A6DxhNE;EAEI,mBAA4F;A7D0hNlG;;A6D5hNE;EAEI,gBAA4F;A7D8hNlG;;A6DhiNE;EAEI,iBAA4F;A7DkiNlG;;A8D3jNI;EAAgC,kCAA8B;EAA9B,8BAA8B;A9D+jNlE;;A8D9jNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9DkkNrE;;A8DjkNI;EAAgC,0CAAsC;EAAtC,sCAAsC;A9DqkN1E;;A8DpkNI;EAAgC,6CAAyC;EAAzC,yCAAyC;A9DwkN7E;;A8DtkNI;EAA8B,8BAA0B;EAA1B,0BAA0B;A9D0kN5D;;A8DzkNI;EAA8B,gCAA4B;EAA5B,4BAA4B;A9D6kN9D;;A8D5kNI;EAA8B,sCAAkC;EAAlC,kCAAkC;A9DglNpE;;A8D/kNI;EAA8B,6BAAyB;EAAzB,yBAAyB;A9DmlN3D;;A8DllNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9DslNzD;;A8DrlNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9DylNzD;;A8DxlNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9D4lN3D;;A8D3lNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9D+lN3D;;A8D7lNI;EAAoC,+BAAsC;EAAtC,sCAAsC;A9DimN9E;;A8DhmNI;EAAoC,6BAAoC;EAApC,oCAAoC;A9DomN5E;;A8DnmNI;EAAoC,gCAAkC;EAAlC,kCAAkC;A9DumN1E;;A8DtmNI;EAAoC,iCAAyC;EAAzC,yCAAyC;A9D0mNjF;;A8DzmNI;EAAoC,oCAAwC;EAAxC,wCAAwC;A9D6mNhF;;A8D3mNI;EAAiC,gCAAkC;EAAlC,kCAAkC;A9D+mNvE;;A8D9mNI;EAAiC,8BAAgC;EAAhC,gCAAgC;A9DknNrE;;A8DjnNI;EAAiC,iCAA8B;EAA9B,8BAA8B;A9DqnNnE;;A8DpnNI;EAAiC,mCAAgC;EAAhC,gCAAgC;A9DwnNrE;;A8DvnNI;EAAiC,kCAA+B;EAA/B,+BAA+B;A9D2nNpE;;A8DznNI;EAAkC,oCAAoC;EAApC,oCAAoC;A9D6nN1E;;A8D5nNI;EAAkC,kCAAkC;EAAlC,kCAAkC;A9DgoNxE;;A8D/nNI;EAAkC,qCAAgC;EAAhC,gCAAgC;A9DmoNtE;;A8DloNI;EAAkC,sCAAuC;EAAvC,uCAAuC;A9DsoN7E;;A8DroNI;EAAkC,yCAAsC;EAAtC,sCAAsC;A9DyoN5E;;A8DxoNI;EAAkC,sCAAiC;EAAjC,iCAAiC;A9D4oNvE;;A8D1oNI;EAAgC,oCAA2B;EAA3B,2BAA2B;A9D8oN/D;;A8D7oNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9DipNrE;;A8DhpNI;EAAgC,mCAA+B;EAA/B,+BAA+B;A9DopNnE;;A8DnpNI;EAAgC,sCAA6B;EAA7B,6BAA6B;A9DupNjE;;A8DtpNI;EAAgC,wCAA+B;EAA/B,+BAA+B;A9D0pNnE;;A8DzpNI;EAAgC,uCAA8B;EAA9B,8BAA8B;A9D6pNlE;;AcjpNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DwsNhE;E8DvsNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D0sNnE;E8DzsNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9D4sNxE;E8D3sNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9D8sN3E;E8D5sNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9D+sN1D;E8D9sNE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9DitN5D;E8DhtNE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9DmtNlE;E8DltNE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9DqtNzD;E8DptNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DutNvD;E8DttNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DytNvD;E8DxtNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D2tNzD;E8D1tNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D6tNzD;E8D3tNE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9D8tN5E;E8D7tNE;IAAoC,6BAAoC;IAApC,oCAAoC;E9DguN1E;E8D/tNE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9DkuNxE;E8DjuNE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9DouN/E;E8DnuNE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9DsuN9E;E8DpuNE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9DuuNrE;E8DtuNE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9DyuNnE;E8DxuNE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D2uNjE;E8D1uNE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9D6uNnE;E8D5uNE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9D+uNlE;E8D7uNE;IAAkC,oCAAoC;IAApC,oCAAoC;E9DgvNxE;E8D/uNE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9DkvNtE;E8DjvNE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9DovNpE;E8DnvNE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9DsvN3E;E8DrvNE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9DwvN1E;E8DvvNE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D0vNrE;E8DxvNE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D2vN7D;E8D1vNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D6vNnE;E8D5vNE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9D+vNjE;E8D9vNE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9DiwN/D;E8DhwNE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9DmwNjE;E8DlwNE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9DqwNhE;AACF;;Ac1vNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DizNhE;E8DhzNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DmzNnE;E8DlzNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DqzNxE;E8DpzNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DuzN3E;E8DrzNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9DwzN1D;E8DvzNE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D0zN5D;E8DzzNE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D4zNlE;E8D3zNE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9D8zNzD;E8D7zNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dg0NvD;E8D/zNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dk0NvD;E8Dj0NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Do0NzD;E8Dn0NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Ds0NzD;E8Dp0NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Du0N5E;E8Dt0NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9Dy0N1E;E8Dx0NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D20NxE;E8D10NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D60N/E;E8D50NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9D+0N9E;E8D70NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dg1NrE;E8D/0NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9Dk1NnE;E8Dj1NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9Do1NjE;E8Dn1NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9Ds1NnE;E8Dr1NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9Dw1NlE;E8Dt1NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9Dy1NxE;E8Dx1NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D21NtE;E8D11NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D61NpE;E8D51NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9D+1N3E;E8D91NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9Di2N1E;E8Dh2NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9Dm2NrE;E8Dj2NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9Do2N7D;E8Dn2NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9Ds2NnE;E8Dr2NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9Dw2NjE;E8Dv2NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D02N/D;E8Dz2NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D42NjE;E8D32NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9D82NhE;AACF;;Acn2NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9D05NhE;E8Dz5NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D45NnE;E8D35NE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9D85NxE;E8D75NE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9Dg6N3E;E8D95NE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9Di6N1D;E8Dh6NE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9Dm6N5D;E8Dl6NE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9Dq6NlE;E8Dp6NE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9Du6NzD;E8Dt6NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dy6NvD;E8Dx6NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9D26NvD;E8D16NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D66NzD;E8D56NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D+6NzD;E8D76NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Dg7N5E;E8D/6NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9Dk7N1E;E8Dj7NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9Do7NxE;E8Dn7NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9Ds7N/E;E8Dr7NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9Dw7N9E;E8Dt7NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dy7NrE;E8Dx7NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9D27NnE;E8D17NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D67NjE;E8D57NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9D+7NnE;E8D97NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9Di8NlE;E8D/7NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9Dk8NxE;E8Dj8NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9Do8NtE;E8Dn8NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9Ds8NpE;E8Dr8NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9Dw8N3E;E8Dv8NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9D08N1E;E8Dz8NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D48NrE;E8D18NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D68N7D;E8D58NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D+8NnE;E8D98NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9Di9NjE;E8Dh9NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9Dm9N/D;E8Dl9NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9Dq9NjE;E8Dp9NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9Du9NhE;AACF;;Ac58NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DmgOhE;E8DlgOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DqgOnE;E8DpgOE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DugOxE;E8DtgOE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DygO3E;E8DvgOE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9D0gO1D;E8DzgOE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D4gO5D;E8D3gOE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D8gOlE;E8D7gOE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9DghOzD;E8D/gOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DkhOvD;E8DjhOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DohOvD;E8DnhOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DshOzD;E8DrhOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DwhOzD;E8DthOE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9DyhO5E;E8DxhOE;IAAoC,6BAAoC;IAApC,oCAAoC;E9D2hO1E;E8D1hOE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D6hOxE;E8D5hOE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D+hO/E;E8D9hOE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9DiiO9E;E8D/hOE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9DkiOrE;E8DjiOE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9DoiOnE;E8DniOE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9DsiOjE;E8DriOE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9DwiOnE;E8DviOE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9D0iOlE;E8DxiOE;IAAkC,oCAAoC;IAApC,oCAAoC;E9D2iOxE;E8D1iOE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D6iOtE;E8D5iOE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D+iOpE;E8D9iOE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9DijO3E;E8DhjOE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9DmjO1E;E8DljOE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9DqjOrE;E8DnjOE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9DsjO7D;E8DrjOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DwjOnE;E8DvjOE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9D0jOjE;E8DzjOE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D4jO/D;E8D3jOE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D8jOjE;E8D7jOE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9DgkOhE;AACF;;A+D3mOI;EAAwB,sBAAsB;A/D+mOlD;;A+D9mOI;EAAwB,uBAAuB;A/DknOnD;;A+DjnOI;EAAwB,sBAAsB;A/DqnOlD;;AcjkOI;EiDtDA;IAAwB,sBAAsB;E/D4nOhD;E+D3nOE;IAAwB,uBAAuB;E/D8nOjD;E+D7nOE;IAAwB,sBAAsB;E/DgoOhD;AACF;;Ac7kOI;EiDtDA;IAAwB,sBAAsB;E/DwoOhD;E+DvoOE;IAAwB,uBAAuB;E/D0oOjD;E+DzoOE;IAAwB,sBAAsB;E/D4oOhD;AACF;;AczlOI;EiDtDA;IAAwB,sBAAsB;E/DopOhD;E+DnpOE;IAAwB,uBAAuB;E/DspOjD;E+DrpOE;IAAwB,sBAAsB;E/DwpOhD;AACF;;AcrmOI;EiDtDA;IAAwB,sBAAsB;E/DgqOhD;E+D/pOE;IAAwB,uBAAuB;E/DkqOjD;E+DjqOE;IAAwB,sBAAsB;E/DoqOhD;AACF;;AgE1qOE;EAAsB,yBAA2B;AhE8qOnD;;AgE9qOE;EAAsB,2BAA2B;AhEkrOnD;;AiEjrOE;EAAyB,2BAA8B;AjEqrOzD;;AiErrOE;EAAyB,6BAA8B;AjEyrOzD;;AiEzrOE;EAAyB,6BAA8B;AjE6rOzD;;AiE7rOE;EAAyB,0BAA8B;AjEisOzD;;AiEjsOE;EAAyB,mCAA8B;EAA9B,2BAA8B;AjEqsOzD;;AiEhsOA;EACE,eAAe;EACf,MAAM;EACN,QAAQ;EACR,OAAO;EACP,a9DoqBsC;AH+hNxC;;AiEhsOA;EACE,eAAe;EACf,QAAQ;EACR,SAAS;EACT,OAAO;EACP,a9D4pBsC;AHuiNxC;;AiE/rO8B;EAD9B;IAEI,wBAAgB;IAAhB,gBAAgB;IAChB,MAAM;IACN,a9DopBoC;EH+iNtC;AACF;;AkE7tOA;ECEE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,UAAU;EACV,YAAY;EACZ,gBAAgB;EAChB,sBAAsB;EACtB,mBAAmB;EACnB,SAAS;AnE+tOX;;AmErtOE;EAEE,gBAAgB;EAChB,WAAW;EACX,YAAY;EACZ,iBAAiB;EACjB,UAAU;EACV,mBAAmB;AnEutOvB;;AoEpvOA;EAAa,8DAAqC;ApEwvOlD;;AoEvvOA;EAAU,wDAAkC;ApE2vO5C;;AoE1vOA;EAAa,uDAAqC;ApE8vOlD;;AoE7vOA;EAAe,2BAA2B;ApEiwO1C;;AqEhwOI;EAAuB,qBAA4B;ArEowOvD;;AqEpwOI;EAAuB,qBAA4B;ArEwwOvD;;AqExwOI;EAAuB,qBAA4B;ArE4wOvD;;AqE5wOI;EAAuB,sBAA4B;ArEgxOvD;;AqEhxOI;EAAuB,sBAA4B;ArEoxOvD;;AqEpxOI;EAAuB,sBAA4B;ArEwxOvD;;AqExxOI;EAAuB,sBAA4B;ArE4xOvD;;AqE5xOI;EAAuB,sBAA4B;ArEgyOvD;;AqEhyOI;EAAuB,uBAA4B;ArEoyOvD;;AqEpyOI;EAAuB,uBAA4B;ArEwyOvD;;AqEpyOA;EAAU,0BAA0B;ArEwyOpC;;AqEvyOA;EAAU,2BAA2B;ArE2yOrC;;AqEvyOA;EAAc,2BAA2B;ArE2yOzC;;AqE1yOA;EAAc,4BAA4B;ArE8yO1C;;AqE5yOA;EAAU,uBAAuB;ArEgzOjC;;AqE/yOA;EAAU,wBAAwB;ArEmzOlC;;AsEl0OA;EAEI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,UAAU;EAEV,oBAAoB;EACpB,WAAW;EAEX,kCAAkC;AtEk0OtC;;AuEx0OQ;EAAgC,oBAA4B;AvE40OpE;;AuE30OQ;;EAEE,wBAAoC;AvE80O9C;;AuE50OQ;;EAEE,0BAAwC;AvE+0OlD;;AuE70OQ;;EAEE,2BAA0C;AvEg1OpD;;AuE90OQ;;EAEE,yBAAsC;AvEi1OhD;;AuEh2OQ;EAAgC,0BAA4B;AvEo2OpE;;AuEn2OQ;;EAEE,8BAAoC;AvEs2O9C;;AuEp2OQ;;EAEE,gCAAwC;AvEu2OlD;;AuEr2OQ;;EAEE,iCAA0C;AvEw2OpD;;AuEt2OQ;;EAEE,+BAAsC;AvEy2OhD;;AuEx3OQ;EAAgC,yBAA4B;AvE43OpE;;AuE33OQ;;EAEE,6BAAoC;AvE83O9C;;AuE53OQ;;EAEE,+BAAwC;AvE+3OlD;;AuE73OQ;;EAEE,gCAA0C;AvEg4OpD;;AuE93OQ;;EAEE,8BAAsC;AvEi4OhD;;AuEh5OQ;EAAgC,uBAA4B;AvEo5OpE;;AuEn5OQ;;EAEE,2BAAoC;AvEs5O9C;;AuEp5OQ;;EAEE,6BAAwC;AvEu5OlD;;AuEr5OQ;;EAEE,8BAA0C;AvEw5OpD;;AuEt5OQ;;EAEE,4BAAsC;AvEy5OhD;;AuEx6OQ;EAAgC,yBAA4B;AvE46OpE;;AuE36OQ;;EAEE,6BAAoC;AvE86O9C;;AuE56OQ;;EAEE,+BAAwC;AvE+6OlD;;AuE76OQ;;EAEE,gCAA0C;AvEg7OpD;;AuE96OQ;;EAEE,8BAAsC;AvEi7OhD;;AuEh8OQ;EAAgC,uBAA4B;AvEo8OpE;;AuEn8OQ;;EAEE,2BAAoC;AvEs8O9C;;AuEp8OQ;;EAEE,6BAAwC;AvEu8OlD;;AuEr8OQ;;EAEE,8BAA0C;AvEw8OpD;;AuEt8OQ;;EAEE,4BAAsC;AvEy8OhD;;AuEx9OQ;EAAgC,qBAA4B;AvE49OpE;;AuE39OQ;;EAEE,yBAAoC;AvE89O9C;;AuE59OQ;;EAEE,2BAAwC;AvE+9OlD;;AuE79OQ;;EAEE,4BAA0C;AvEg+OpD;;AuE99OQ;;EAEE,0BAAsC;AvEi+OhD;;AuEh/OQ;EAAgC,2BAA4B;AvEo/OpE;;AuEn/OQ;;EAEE,+BAAoC;AvEs/O9C;;AuEp/OQ;;EAEE,iCAAwC;AvEu/OlD;;AuEr/OQ;;EAEE,kCAA0C;AvEw/OpD;;AuEt/OQ;;EAEE,gCAAsC;AvEy/OhD;;AuExgPQ;EAAgC,0BAA4B;AvE4gPpE;;AuE3gPQ;;EAEE,8BAAoC;AvE8gP9C;;AuE5gPQ;;EAEE,gCAAwC;AvE+gPlD;;AuE7gPQ;;EAEE,iCAA0C;AvEghPpD;;AuE9gPQ;;EAEE,+BAAsC;AvEihPhD;;AuEhiPQ;EAAgC,wBAA4B;AvEoiPpE;;AuEniPQ;;EAEE,4BAAoC;AvEsiP9C;;AuEpiPQ;;EAEE,8BAAwC;AvEuiPlD;;AuEriPQ;;EAEE,+BAA0C;AvEwiPpD;;AuEtiPQ;;EAEE,6BAAsC;AvEyiPhD;;AuExjPQ;EAAgC,0BAA4B;AvE4jPpE;;AuE3jPQ;;EAEE,8BAAoC;AvE8jP9C;;AuE5jPQ;;EAEE,gCAAwC;AvE+jPlD;;AuE7jPQ;;EAEE,iCAA0C;AvEgkPpD;;AuE9jPQ;;EAEE,+BAAsC;AvEikPhD;;AuEhlPQ;EAAgC,wBAA4B;AvEolPpE;;AuEnlPQ;;EAEE,4BAAoC;AvEslP9C;;AuEplPQ;;EAEE,8BAAwC;AvEulPlD;;AuErlPQ;;EAEE,+BAA0C;AvEwlPpD;;AuEtlPQ;;EAEE,6BAAsC;AvEylPhD;;AuEjlPQ;EAAwB,2BAA2B;AvEqlP3D;;AuEplPQ;;EAEE,+BAA+B;AvEulPzC;;AuErlPQ;;EAEE,iCAAiC;AvEwlP3C;;AuEtlPQ;;EAEE,kCAAkC;AvEylP5C;;AuEvlPQ;;EAEE,gCAAgC;AvE0lP1C;;AuEzmPQ;EAAwB,0BAA2B;AvE6mP3D;;AuE5mPQ;;EAEE,8BAA+B;AvE+mPzC;;AuE7mPQ;;EAEE,gCAAiC;AvEgnP3C;;AuE9mPQ;;EAEE,iCAAkC;AvEinP5C;;AuE/mPQ;;EAEE,+BAAgC;AvEknP1C;;AuEjoPQ;EAAwB,wBAA2B;AvEqoP3D;;AuEpoPQ;;EAEE,4BAA+B;AvEuoPzC;;AuEroPQ;;EAEE,8BAAiC;AvEwoP3C;;AuEtoPQ;;EAEE,+BAAkC;AvEyoP5C;;AuEvoPQ;;EAEE,6BAAgC;AvE0oP1C;;AuEzpPQ;EAAwB,0BAA2B;AvE6pP3D;;AuE5pPQ;;EAEE,8BAA+B;AvE+pPzC;;AuE7pPQ;;EAEE,gCAAiC;AvEgqP3C;;AuE9pPQ;;EAEE,iCAAkC;AvEiqP5C;;AuE/pPQ;;EAEE,+BAAgC;AvEkqP1C;;AuEjrPQ;EAAwB,wBAA2B;AvEqrP3D;;AuEprPQ;;EAEE,4BAA+B;AvEurPzC;;AuErrPQ;;EAEE,8BAAiC;AvEwrP3C;;AuEtrPQ;;EAEE,+BAAkC;AvEyrP5C;;AuEvrPQ;;EAEE,6BAAgC;AvE0rP1C;;AuEprPI;EAAmB,uBAAuB;AvEwrP9C;;AuEvrPI;;EAEE,2BAA2B;AvE0rPjC;;AuExrPI;;EAEE,6BAA6B;AvE2rPnC;;AuEzrPI;;EAEE,8BAA8B;AvE4rPpC;;AuE1rPI;;EAEE,4BAA4B;AvE6rPlC;;ActsPI;EyDlDI;IAAgC,oBAA4B;EvE6vPlE;EuE5vPM;;IAEE,wBAAoC;EvE8vP5C;EuE5vPM;;IAEE,0BAAwC;EvE8vPhD;EuE5vPM;;IAEE,2BAA0C;EvE8vPlD;EuE5vPM;;IAEE,yBAAsC;EvE8vP9C;EuE7wPM;IAAgC,0BAA4B;EvEgxPlE;EuE/wPM;;IAEE,8BAAoC;EvEixP5C;EuE/wPM;;IAEE,gCAAwC;EvEixPhD;EuE/wPM;;IAEE,iCAA0C;EvEixPlD;EuE/wPM;;IAEE,+BAAsC;EvEixP9C;EuEhyPM;IAAgC,yBAA4B;EvEmyPlE;EuElyPM;;IAEE,6BAAoC;EvEoyP5C;EuElyPM;;IAEE,+BAAwC;EvEoyPhD;EuElyPM;;IAEE,gCAA0C;EvEoyPlD;EuElyPM;;IAEE,8BAAsC;EvEoyP9C;EuEnzPM;IAAgC,uBAA4B;EvEszPlE;EuErzPM;;IAEE,2BAAoC;EvEuzP5C;EuErzPM;;IAEE,6BAAwC;EvEuzPhD;EuErzPM;;IAEE,8BAA0C;EvEuzPlD;EuErzPM;;IAEE,4BAAsC;EvEuzP9C;EuEt0PM;IAAgC,yBAA4B;EvEy0PlE;EuEx0PM;;IAEE,6BAAoC;EvE00P5C;EuEx0PM;;IAEE,+BAAwC;EvE00PhD;EuEx0PM;;IAEE,gCAA0C;EvE00PlD;EuEx0PM;;IAEE,8BAAsC;EvE00P9C;EuEz1PM;IAAgC,uBAA4B;EvE41PlE;EuE31PM;;IAEE,2BAAoC;EvE61P5C;EuE31PM;;IAEE,6BAAwC;EvE61PhD;EuE31PM;;IAEE,8BAA0C;EvE61PlD;EuE31PM;;IAEE,4BAAsC;EvE61P9C;EuE52PM;IAAgC,qBAA4B;EvE+2PlE;EuE92PM;;IAEE,yBAAoC;EvEg3P5C;EuE92PM;;IAEE,2BAAwC;EvEg3PhD;EuE92PM;;IAEE,4BAA0C;EvEg3PlD;EuE92PM;;IAEE,0BAAsC;EvEg3P9C;EuE/3PM;IAAgC,2BAA4B;EvEk4PlE;EuEj4PM;;IAEE,+BAAoC;EvEm4P5C;EuEj4PM;;IAEE,iCAAwC;EvEm4PhD;EuEj4PM;;IAEE,kCAA0C;EvEm4PlD;EuEj4PM;;IAEE,gCAAsC;EvEm4P9C;EuEl5PM;IAAgC,0BAA4B;EvEq5PlE;EuEp5PM;;IAEE,8BAAoC;EvEs5P5C;EuEp5PM;;IAEE,gCAAwC;EvEs5PhD;EuEp5PM;;IAEE,iCAA0C;EvEs5PlD;EuEp5PM;;IAEE,+BAAsC;EvEs5P9C;EuEr6PM;IAAgC,wBAA4B;EvEw6PlE;EuEv6PM;;IAEE,4BAAoC;EvEy6P5C;EuEv6PM;;IAEE,8BAAwC;EvEy6PhD;EuEv6PM;;IAEE,+BAA0C;EvEy6PlD;EuEv6PM;;IAEE,6BAAsC;EvEy6P9C;EuEx7PM;IAAgC,0BAA4B;EvE27PlE;EuE17PM;;IAEE,8BAAoC;EvE47P5C;EuE17PM;;IAEE,gCAAwC;EvE47PhD;EuE17PM;;IAEE,iCAA0C;EvE47PlD;EuE17PM;;IAEE,+BAAsC;EvE47P9C;EuE38PM;IAAgC,wBAA4B;EvE88PlE;EuE78PM;;IAEE,4BAAoC;EvE+8P5C;EuE78PM;;IAEE,8BAAwC;EvE+8PhD;EuE78PM;;IAEE,+BAA0C;EvE+8PlD;EuE78PM;;IAEE,6BAAsC;EvE+8P9C;EuEv8PM;IAAwB,2BAA2B;EvE08PzD;EuEz8PM;;IAEE,+BAA+B;EvE28PvC;EuEz8PM;;IAEE,iCAAiC;EvE28PzC;EuEz8PM;;IAEE,kCAAkC;EvE28P1C;EuEz8PM;;IAEE,gCAAgC;EvE28PxC;EuE19PM;IAAwB,0BAA2B;EvE69PzD;EuE59PM;;IAEE,8BAA+B;EvE89PvC;EuE59PM;;IAEE,gCAAiC;EvE89PzC;EuE59PM;;IAEE,iCAAkC;EvE89P1C;EuE59PM;;IAEE,+BAAgC;EvE89PxC;EuE7+PM;IAAwB,wBAA2B;EvEg/PzD;EuE/+PM;;IAEE,4BAA+B;EvEi/PvC;EuE/+PM;;IAEE,8BAAiC;EvEi/PzC;EuE/+PM;;IAEE,+BAAkC;EvEi/P1C;EuE/+PM;;IAEE,6BAAgC;EvEi/PxC;EuEhgQM;IAAwB,0BAA2B;EvEmgQzD;EuElgQM;;IAEE,8BAA+B;EvEogQvC;EuElgQM;;IAEE,gCAAiC;EvEogQzC;EuElgQM;;IAEE,iCAAkC;EvEogQ1C;EuElgQM;;IAEE,+BAAgC;EvEogQxC;EuEnhQM;IAAwB,wBAA2B;EvEshQzD;EuErhQM;;IAEE,4BAA+B;EvEuhQvC;EuErhQM;;IAEE,8BAAiC;EvEuhQzC;EuErhQM;;IAEE,+BAAkC;EvEuhQ1C;EuErhQM;;IAEE,6BAAgC;EvEuhQxC;EuEjhQE;IAAmB,uBAAuB;EvEohQ5C;EuEnhQE;;IAEE,2BAA2B;EvEqhQ/B;EuEnhQE;;IAEE,6BAA6B;EvEqhQjC;EuEnhQE;;IAEE,8BAA8B;EvEqhQlC;EuEnhQE;;IAEE,4BAA4B;EvEqhQhC;AACF;;Ac/hQI;EyDlDI;IAAgC,oBAA4B;EvEslQlE;EuErlQM;;IAEE,wBAAoC;EvEulQ5C;EuErlQM;;IAEE,0BAAwC;EvEulQhD;EuErlQM;;IAEE,2BAA0C;EvEulQlD;EuErlQM;;IAEE,yBAAsC;EvEulQ9C;EuEtmQM;IAAgC,0BAA4B;EvEymQlE;EuExmQM;;IAEE,8BAAoC;EvE0mQ5C;EuExmQM;;IAEE,gCAAwC;EvE0mQhD;EuExmQM;;IAEE,iCAA0C;EvE0mQlD;EuExmQM;;IAEE,+BAAsC;EvE0mQ9C;EuEznQM;IAAgC,yBAA4B;EvE4nQlE;EuE3nQM;;IAEE,6BAAoC;EvE6nQ5C;EuE3nQM;;IAEE,+BAAwC;EvE6nQhD;EuE3nQM;;IAEE,gCAA0C;EvE6nQlD;EuE3nQM;;IAEE,8BAAsC;EvE6nQ9C;EuE5oQM;IAAgC,uBAA4B;EvE+oQlE;EuE9oQM;;IAEE,2BAAoC;EvEgpQ5C;EuE9oQM;;IAEE,6BAAwC;EvEgpQhD;EuE9oQM;;IAEE,8BAA0C;EvEgpQlD;EuE9oQM;;IAEE,4BAAsC;EvEgpQ9C;EuE/pQM;IAAgC,yBAA4B;EvEkqQlE;EuEjqQM;;IAEE,6BAAoC;EvEmqQ5C;EuEjqQM;;IAEE,+BAAwC;EvEmqQhD;EuEjqQM;;IAEE,gCAA0C;EvEmqQlD;EuEjqQM;;IAEE,8BAAsC;EvEmqQ9C;EuElrQM;IAAgC,uBAA4B;EvEqrQlE;EuEprQM;;IAEE,2BAAoC;EvEsrQ5C;EuEprQM;;IAEE,6BAAwC;EvEsrQhD;EuEprQM;;IAEE,8BAA0C;EvEsrQlD;EuEprQM;;IAEE,4BAAsC;EvEsrQ9C;EuErsQM;IAAgC,qBAA4B;EvEwsQlE;EuEvsQM;;IAEE,yBAAoC;EvEysQ5C;EuEvsQM;;IAEE,2BAAwC;EvEysQhD;EuEvsQM;;IAEE,4BAA0C;EvEysQlD;EuEvsQM;;IAEE,0BAAsC;EvEysQ9C;EuExtQM;IAAgC,2BAA4B;EvE2tQlE;EuE1tQM;;IAEE,+BAAoC;EvE4tQ5C;EuE1tQM;;IAEE,iCAAwC;EvE4tQhD;EuE1tQM;;IAEE,kCAA0C;EvE4tQlD;EuE1tQM;;IAEE,gCAAsC;EvE4tQ9C;EuE3uQM;IAAgC,0BAA4B;EvE8uQlE;EuE7uQM;;IAEE,8BAAoC;EvE+uQ5C;EuE7uQM;;IAEE,gCAAwC;EvE+uQhD;EuE7uQM;;IAEE,iCAA0C;EvE+uQlD;EuE7uQM;;IAEE,+BAAsC;EvE+uQ9C;EuE9vQM;IAAgC,wBAA4B;EvEiwQlE;EuEhwQM;;IAEE,4BAAoC;EvEkwQ5C;EuEhwQM;;IAEE,8BAAwC;EvEkwQhD;EuEhwQM;;IAEE,+BAA0C;EvEkwQlD;EuEhwQM;;IAEE,6BAAsC;EvEkwQ9C;EuEjxQM;IAAgC,0BAA4B;EvEoxQlE;EuEnxQM;;IAEE,8BAAoC;EvEqxQ5C;EuEnxQM;;IAEE,gCAAwC;EvEqxQhD;EuEnxQM;;IAEE,iCAA0C;EvEqxQlD;EuEnxQM;;IAEE,+BAAsC;EvEqxQ9C;EuEpyQM;IAAgC,wBAA4B;EvEuyQlE;EuEtyQM;;IAEE,4BAAoC;EvEwyQ5C;EuEtyQM;;IAEE,8BAAwC;EvEwyQhD;EuEtyQM;;IAEE,+BAA0C;EvEwyQlD;EuEtyQM;;IAEE,6BAAsC;EvEwyQ9C;EuEhyQM;IAAwB,2BAA2B;EvEmyQzD;EuElyQM;;IAEE,+BAA+B;EvEoyQvC;EuElyQM;;IAEE,iCAAiC;EvEoyQzC;EuElyQM;;IAEE,kCAAkC;EvEoyQ1C;EuElyQM;;IAEE,gCAAgC;EvEoyQxC;EuEnzQM;IAAwB,0BAA2B;EvEszQzD;EuErzQM;;IAEE,8BAA+B;EvEuzQvC;EuErzQM;;IAEE,gCAAiC;EvEuzQzC;EuErzQM;;IAEE,iCAAkC;EvEuzQ1C;EuErzQM;;IAEE,+BAAgC;EvEuzQxC;EuEt0QM;IAAwB,wBAA2B;EvEy0QzD;EuEx0QM;;IAEE,4BAA+B;EvE00QvC;EuEx0QM;;IAEE,8BAAiC;EvE00QzC;EuEx0QM;;IAEE,+BAAkC;EvE00Q1C;EuEx0QM;;IAEE,6BAAgC;EvE00QxC;EuEz1QM;IAAwB,0BAA2B;EvE41QzD;EuE31QM;;IAEE,8BAA+B;EvE61QvC;EuE31QM;;IAEE,gCAAiC;EvE61QzC;EuE31QM;;IAEE,iCAAkC;EvE61Q1C;EuE31QM;;IAEE,+BAAgC;EvE61QxC;EuE52QM;IAAwB,wBAA2B;EvE+2QzD;EuE92QM;;IAEE,4BAA+B;EvEg3QvC;EuE92QM;;IAEE,8BAAiC;EvEg3QzC;EuE92QM;;IAEE,+BAAkC;EvEg3Q1C;EuE92QM;;IAEE,6BAAgC;EvEg3QxC;EuE12QE;IAAmB,uBAAuB;EvE62Q5C;EuE52QE;;IAEE,2BAA2B;EvE82Q/B;EuE52QE;;IAEE,6BAA6B;EvE82QjC;EuE52QE;;IAEE,8BAA8B;EvE82QlC;EuE52QE;;IAEE,4BAA4B;EvE82QhC;AACF;;Acx3QI;EyDlDI;IAAgC,oBAA4B;EvE+6QlE;EuE96QM;;IAEE,wBAAoC;EvEg7Q5C;EuE96QM;;IAEE,0BAAwC;EvEg7QhD;EuE96QM;;IAEE,2BAA0C;EvEg7QlD;EuE96QM;;IAEE,yBAAsC;EvEg7Q9C;EuE/7QM;IAAgC,0BAA4B;EvEk8QlE;EuEj8QM;;IAEE,8BAAoC;EvEm8Q5C;EuEj8QM;;IAEE,gCAAwC;EvEm8QhD;EuEj8QM;;IAEE,iCAA0C;EvEm8QlD;EuEj8QM;;IAEE,+BAAsC;EvEm8Q9C;EuEl9QM;IAAgC,yBAA4B;EvEq9QlE;EuEp9QM;;IAEE,6BAAoC;EvEs9Q5C;EuEp9QM;;IAEE,+BAAwC;EvEs9QhD;EuEp9QM;;IAEE,gCAA0C;EvEs9QlD;EuEp9QM;;IAEE,8BAAsC;EvEs9Q9C;EuEr+QM;IAAgC,uBAA4B;EvEw+QlE;EuEv+QM;;IAEE,2BAAoC;EvEy+Q5C;EuEv+QM;;IAEE,6BAAwC;EvEy+QhD;EuEv+QM;;IAEE,8BAA0C;EvEy+QlD;EuEv+QM;;IAEE,4BAAsC;EvEy+Q9C;EuEx/QM;IAAgC,yBAA4B;EvE2/QlE;EuE1/QM;;IAEE,6BAAoC;EvE4/Q5C;EuE1/QM;;IAEE,+BAAwC;EvE4/QhD;EuE1/QM;;IAEE,gCAA0C;EvE4/QlD;EuE1/QM;;IAEE,8BAAsC;EvE4/Q9C;EuE3gRM;IAAgC,uBAA4B;EvE8gRlE;EuE7gRM;;IAEE,2BAAoC;EvE+gR5C;EuE7gRM;;IAEE,6BAAwC;EvE+gRhD;EuE7gRM;;IAEE,8BAA0C;EvE+gRlD;EuE7gRM;;IAEE,4BAAsC;EvE+gR9C;EuE9hRM;IAAgC,qBAA4B;EvEiiRlE;EuEhiRM;;IAEE,yBAAoC;EvEkiR5C;EuEhiRM;;IAEE,2BAAwC;EvEkiRhD;EuEhiRM;;IAEE,4BAA0C;EvEkiRlD;EuEhiRM;;IAEE,0BAAsC;EvEkiR9C;EuEjjRM;IAAgC,2BAA4B;EvEojRlE;EuEnjRM;;IAEE,+BAAoC;EvEqjR5C;EuEnjRM;;IAEE,iCAAwC;EvEqjRhD;EuEnjRM;;IAEE,kCAA0C;EvEqjRlD;EuEnjRM;;IAEE,gCAAsC;EvEqjR9C;EuEpkRM;IAAgC,0BAA4B;EvEukRlE;EuEtkRM;;IAEE,8BAAoC;EvEwkR5C;EuEtkRM;;IAEE,gCAAwC;EvEwkRhD;EuEtkRM;;IAEE,iCAA0C;EvEwkRlD;EuEtkRM;;IAEE,+BAAsC;EvEwkR9C;EuEvlRM;IAAgC,wBAA4B;EvE0lRlE;EuEzlRM;;IAEE,4BAAoC;EvE2lR5C;EuEzlRM;;IAEE,8BAAwC;EvE2lRhD;EuEzlRM;;IAEE,+BAA0C;EvE2lRlD;EuEzlRM;;IAEE,6BAAsC;EvE2lR9C;EuE1mRM;IAAgC,0BAA4B;EvE6mRlE;EuE5mRM;;IAEE,8BAAoC;EvE8mR5C;EuE5mRM;;IAEE,gCAAwC;EvE8mRhD;EuE5mRM;;IAEE,iCAA0C;EvE8mRlD;EuE5mRM;;IAEE,+BAAsC;EvE8mR9C;EuE7nRM;IAAgC,wBAA4B;EvEgoRlE;EuE/nRM;;IAEE,4BAAoC;EvEioR5C;EuE/nRM;;IAEE,8BAAwC;EvEioRhD;EuE/nRM;;IAEE,+BAA0C;EvEioRlD;EuE/nRM;;IAEE,6BAAsC;EvEioR9C;EuEznRM;IAAwB,2BAA2B;EvE4nRzD;EuE3nRM;;IAEE,+BAA+B;EvE6nRvC;EuE3nRM;;IAEE,iCAAiC;EvE6nRzC;EuE3nRM;;IAEE,kCAAkC;EvE6nR1C;EuE3nRM;;IAEE,gCAAgC;EvE6nRxC;EuE5oRM;IAAwB,0BAA2B;EvE+oRzD;EuE9oRM;;IAEE,8BAA+B;EvEgpRvC;EuE9oRM;;IAEE,gCAAiC;EvEgpRzC;EuE9oRM;;IAEE,iCAAkC;EvEgpR1C;EuE9oRM;;IAEE,+BAAgC;EvEgpRxC;EuE/pRM;IAAwB,wBAA2B;EvEkqRzD;EuEjqRM;;IAEE,4BAA+B;EvEmqRvC;EuEjqRM;;IAEE,8BAAiC;EvEmqRzC;EuEjqRM;;IAEE,+BAAkC;EvEmqR1C;EuEjqRM;;IAEE,6BAAgC;EvEmqRxC;EuElrRM;IAAwB,0BAA2B;EvEqrRzD;EuEprRM;;IAEE,8BAA+B;EvEsrRvC;EuEprRM;;IAEE,gCAAiC;EvEsrRzC;EuEprRM;;IAEE,iCAAkC;EvEsrR1C;EuEprRM;;IAEE,+BAAgC;EvEsrRxC;EuErsRM;IAAwB,wBAA2B;EvEwsRzD;EuEvsRM;;IAEE,4BAA+B;EvEysRvC;EuEvsRM;;IAEE,8BAAiC;EvEysRzC;EuEvsRM;;IAEE,+BAAkC;EvEysR1C;EuEvsRM;;IAEE,6BAAgC;EvEysRxC;EuEnsRE;IAAmB,uBAAuB;EvEssR5C;EuErsRE;;IAEE,2BAA2B;EvEusR/B;EuErsRE;;IAEE,6BAA6B;EvEusRjC;EuErsRE;;IAEE,8BAA8B;EvEusRlC;EuErsRE;;IAEE,4BAA4B;EvEusRhC;AACF;;AcjtRI;EyDlDI;IAAgC,oBAA4B;EvEwwRlE;EuEvwRM;;IAEE,wBAAoC;EvEywR5C;EuEvwRM;;IAEE,0BAAwC;EvEywRhD;EuEvwRM;;IAEE,2BAA0C;EvEywRlD;EuEvwRM;;IAEE,yBAAsC;EvEywR9C;EuExxRM;IAAgC,0BAA4B;EvE2xRlE;EuE1xRM;;IAEE,8BAAoC;EvE4xR5C;EuE1xRM;;IAEE,gCAAwC;EvE4xRhD;EuE1xRM;;IAEE,iCAA0C;EvE4xRlD;EuE1xRM;;IAEE,+BAAsC;EvE4xR9C;EuE3yRM;IAAgC,yBAA4B;EvE8yRlE;EuE7yRM;;IAEE,6BAAoC;EvE+yR5C;EuE7yRM;;IAEE,+BAAwC;EvE+yRhD;EuE7yRM;;IAEE,gCAA0C;EvE+yRlD;EuE7yRM;;IAEE,8BAAsC;EvE+yR9C;EuE9zRM;IAAgC,uBAA4B;EvEi0RlE;EuEh0RM;;IAEE,2BAAoC;EvEk0R5C;EuEh0RM;;IAEE,6BAAwC;EvEk0RhD;EuEh0RM;;IAEE,8BAA0C;EvEk0RlD;EuEh0RM;;IAEE,4BAAsC;EvEk0R9C;EuEj1RM;IAAgC,yBAA4B;EvEo1RlE;EuEn1RM;;IAEE,6BAAoC;EvEq1R5C;EuEn1RM;;IAEE,+BAAwC;EvEq1RhD;EuEn1RM;;IAEE,gCAA0C;EvEq1RlD;EuEn1RM;;IAEE,8BAAsC;EvEq1R9C;EuEp2RM;IAAgC,uBAA4B;EvEu2RlE;EuEt2RM;;IAEE,2BAAoC;EvEw2R5C;EuEt2RM;;IAEE,6BAAwC;EvEw2RhD;EuEt2RM;;IAEE,8BAA0C;EvEw2RlD;EuEt2RM;;IAEE,4BAAsC;EvEw2R9C;EuEv3RM;IAAgC,qBAA4B;EvE03RlE;EuEz3RM;;IAEE,yBAAoC;EvE23R5C;EuEz3RM;;IAEE,2BAAwC;EvE23RhD;EuEz3RM;;IAEE,4BAA0C;EvE23RlD;EuEz3RM;;IAEE,0BAAsC;EvE23R9C;EuE14RM;IAAgC,2BAA4B;EvE64RlE;EuE54RM;;IAEE,+BAAoC;EvE84R5C;EuE54RM;;IAEE,iCAAwC;EvE84RhD;EuE54RM;;IAEE,kCAA0C;EvE84RlD;EuE54RM;;IAEE,gCAAsC;EvE84R9C;EuE75RM;IAAgC,0BAA4B;EvEg6RlE;EuE/5RM;;IAEE,8BAAoC;EvEi6R5C;EuE/5RM;;IAEE,gCAAwC;EvEi6RhD;EuE/5RM;;IAEE,iCAA0C;EvEi6RlD;EuE/5RM;;IAEE,+BAAsC;EvEi6R9C;EuEh7RM;IAAgC,wBAA4B;EvEm7RlE;EuEl7RM;;IAEE,4BAAoC;EvEo7R5C;EuEl7RM;;IAEE,8BAAwC;EvEo7RhD;EuEl7RM;;IAEE,+BAA0C;EvEo7RlD;EuEl7RM;;IAEE,6BAAsC;EvEo7R9C;EuEn8RM;IAAgC,0BAA4B;EvEs8RlE;EuEr8RM;;IAEE,8BAAoC;EvEu8R5C;EuEr8RM;;IAEE,gCAAwC;EvEu8RhD;EuEr8RM;;IAEE,iCAA0C;EvEu8RlD;EuEr8RM;;IAEE,+BAAsC;EvEu8R9C;EuEt9RM;IAAgC,wBAA4B;EvEy9RlE;EuEx9RM;;IAEE,4BAAoC;EvE09R5C;EuEx9RM;;IAEE,8BAAwC;EvE09RhD;EuEx9RM;;IAEE,+BAA0C;EvE09RlD;EuEx9RM;;IAEE,6BAAsC;EvE09R9C;EuEl9RM;IAAwB,2BAA2B;EvEq9RzD;EuEp9RM;;IAEE,+BAA+B;EvEs9RvC;EuEp9RM;;IAEE,iCAAiC;EvEs9RzC;EuEp9RM;;IAEE,kCAAkC;EvEs9R1C;EuEp9RM;;IAEE,gCAAgC;EvEs9RxC;EuEr+RM;IAAwB,0BAA2B;EvEw+RzD;EuEv+RM;;IAEE,8BAA+B;EvEy+RvC;EuEv+RM;;IAEE,gCAAiC;EvEy+RzC;EuEv+RM;;IAEE,iCAAkC;EvEy+R1C;EuEv+RM;;IAEE,+BAAgC;EvEy+RxC;EuEx/RM;IAAwB,wBAA2B;EvE2/RzD;EuE1/RM;;IAEE,4BAA+B;EvE4/RvC;EuE1/RM;;IAEE,8BAAiC;EvE4/RzC;EuE1/RM;;IAEE,+BAAkC;EvE4/R1C;EuE1/RM;;IAEE,6BAAgC;EvE4/RxC;EuE3gSM;IAAwB,0BAA2B;EvE8gSzD;EuE7gSM;;IAEE,8BAA+B;EvE+gSvC;EuE7gSM;;IAEE,gCAAiC;EvE+gSzC;EuE7gSM;;IAEE,iCAAkC;EvE+gS1C;EuE7gSM;;IAEE,+BAAgC;EvE+gSxC;EuE9hSM;IAAwB,wBAA2B;EvEiiSzD;EuEhiSM;;IAEE,4BAA+B;EvEkiSvC;EuEhiSM;;IAEE,8BAAiC;EvEkiSzC;EuEhiSM;;IAEE,+BAAkC;EvEkiS1C;EuEhiSM;;IAEE,6BAAgC;EvEkiSxC;EuE5hSE;IAAmB,uBAAuB;EvE+hS5C;EuE9hSE;;IAEE,2BAA2B;EvEgiS/B;EuE9hSE;;IAEE,6BAA6B;EvEgiSjC;EuE9hSE;;IAEE,8BAA8B;EvEgiSlC;EuE9hSE;;IAEE,4BAA4B;EvEgiShC;AACF;;AwEhmSA;EAAkB,4GAA8C;AxEomShE;;AwEhmSA;EAAiB,8BAA8B;AxEomS/C;;AwEnmSA;EAAiB,8BAA8B;AxEumS/C;;AwEtmSA;EAAiB,8BAA8B;AxE0mS/C;;AwEzmSA;ECTE,gBAAgB;EAChB,uBAAuB;EACvB,mBAAmB;AzEsnSrB;;AwEvmSI;EAAwB,2BAA2B;AxE2mSvD;;AwE1mSI;EAAwB,4BAA4B;AxE8mSxD;;AwE7mSI;EAAwB,6BAA6B;AxEinSzD;;Ac5kSI;E0DvCA;IAAwB,2BAA2B;ExEwnSrD;EwEvnSE;IAAwB,4BAA4B;ExE0nStD;EwEznSE;IAAwB,6BAA6B;ExE4nSvD;AACF;;AcxlSI;E0DvCA;IAAwB,2BAA2B;ExEooSrD;EwEnoSE;IAAwB,4BAA4B;ExEsoStD;EwEroSE;IAAwB,6BAA6B;ExEwoSvD;AACF;;AcpmSI;E0DvCA;IAAwB,2BAA2B;ExEgpSrD;EwE/oSE;IAAwB,4BAA4B;ExEkpStD;EwEjpSE;IAAwB,6BAA6B;ExEopSvD;AACF;;AchnSI;E0DvCA;IAAwB,2BAA2B;ExE4pSrD;EwE3pSE;IAAwB,4BAA4B;ExE8pStD;EwE7pSE;IAAwB,6BAA6B;ExEgqSvD;AACF;;AwE3pSA;EAAmB,oCAAoC;AxE+pSvD;;AwE9pSA;EAAmB,oCAAoC;AxEkqSvD;;AwEjqSA;EAAmB,qCAAqC;AxEqqSxD;;AwEjqSA;EAAuB,2BAA0C;AxEqqSjE;;AwEpqSA;EAAuB,+BAA4C;AxEwqSnE;;AwEvqSA;EAAuB,2BAA2C;AxE2qSlE;;AwE1qSA;EAAuB,2BAAyC;AxE8qShE;;AwE7qSA;EAAuB,8BAA2C;AxEirSlE;;AwEhrSA;EAAuB,6BAA6B;AxEorSpD;;AwEhrSA;EAAc,sBAAwB;AxEorStC;;A0E3tSE;EACE,yBAAwB;A1E8tS5B;;AKptSE;EqELM,yBAA0E;A1E6tSlF;;A0EnuSE;EACE,yBAAwB;A1EsuS5B;;AK5tSE;EqELM,yBAA0E;A1EquSlF;;A0E3uSE;EACE,yBAAwB;A1E8uS5B;;AKpuSE;EqELM,yBAA0E;A1E6uSlF;;A0EnvSE;EACE,yBAAwB;A1EsvS5B;;AK5uSE;EqELM,yBAA0E;A1EqvSlF;;A0E3vSE;EACE,yBAAwB;A1E8vS5B;;AKpvSE;EqELM,yBAA0E;A1E6vSlF;;A0EnwSE;EACE,yBAAwB;A1EswS5B;;AK5vSE;EqELM,yBAA0E;A1EqwSlF;;A0E3wSE;EACE,yBAAwB;A1E8wS5B;;AKpwSE;EqELM,yBAA0E;A1E6wSlF;;A0EnxSE;EACE,yBAAwB;A1EsxS5B;;AK5wSE;EqELM,yBAA0E;A1EqxSlF;;AwE9uSA;EAAa,yBAA6B;AxEkvS1C;;AwEjvSA;EAAc,yBAA6B;AxEqvS3C;;AwEnvSA;EAAiB,oCAAkC;AxEuvSnD;;AwEtvSA;EAAiB,0CAAkC;AxE0vSnD;;AwEtvSA;EGvDE,WAAW;EACX,kBAAkB;EAClB,iBAAiB;EACjB,6BAA6B;EAC7B,SAAS;A3EizSX;;AwE1vSA;EAAwB,gCAAgC;AxE8vSxD;;AwE5vSA;EACE,iCAAiC;EACjC,oCAAoC;AxE+vStC;;AwE1vSA;EAAc,yBAAyB;AxE8vSvC;;A4E/zSA;EACE,8BAA8B;A5Ek0ShC;;A4E/zSA;EACE,6BAA6B;A5Ek0S/B;;A6El0SE;E3EOF;;;I2EDM,4BAA4B;IAE5B,2BAA2B;E7Ek0S/B;E6E/zSE;IAEI,0BAA0B;E7Eg0ShC;E6EvzSE;IACE,6BAA6B;E7EyzSjC;EE3nSF;I2E/KM,gCAAgC;E7E6ySpC;E6E3ySE;;IAEE,yB1EzCY;I0E0CZ,wBAAwB;E7E6yS5B;E6ErySE;IACE,2BAA2B;E7EuyS/B;E6EpySE;;IAEE,wBAAwB;E7EsyS5B;E6EnySE;;;IAGE,UAAU;IACV,SAAS;E7EqySb;E6ElySE;;IAEE,uBAAuB;E7EoyS3B;E6E5xSE;IACE,Q1E4hCgC;EHkwQpC;EE10SF;I2E+CM,2BAA2C;E7E8xS/C;EYp3SA;IiEyFI,2BAA2C;E7E8xS/C;EiC52SF;I4CmFM,aAAa;E7E4xSjB;EsC33SF;IuCkGM,sB1EtFS;EHk3Sb;EgB/3SF;I6DuGM,oCAAoC;E7E2xSxC;E6E5xSE;;IAKI,iCAAmC;E7E2xSzC;EgB91SF;;I6D0EQ,oCAAsC;E7EwxS5C;EgB7wSF;I6DNM,cAAc;E7EsxSlB;EiB54SA;;;;I4D4HM,qB1EvHU;EH64ShB;EgBxySF;I6DuBM,cAAc;IACd,qB1E7HY;EHi5ShB;AACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-wrap: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n font-size: 1rem;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input[disabled] ~ .form-check-label,\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n cursor: pointer;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 0%;\n min-width: 0;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1;\n width: 1rem;\n height: 1.25rem;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input[disabled] ~ .custom-file-label,\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: 1.4rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar .container,\n.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n flex: 1 1 auto;\n min-height: 1px;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0;\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n display: flex;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n flex: 1 0 0%;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:last-of-type) {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card > .card-header {\n border-radius: 0;\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 3;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n\n.list-group-horizontal .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n.list-group-horizontal .list-group-item.active {\n margin-top: 0;\n}\n\n.list-group-horizontal .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n\n.list-group-horizontal .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n.list-group-flush .list-group-item {\n border-right-width: 0;\n border-left-width: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:first-child {\n border-top-width: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc(-0.5rem - 1px);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc(-0.5rem - 1px);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-break: break-word !important;\n overflow-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { @include font-size($h1-font-size); }\nh2, .h2 { @include font-size($h2-font-size); }\nh3, .h3 { @include font-size($h3-font-size); }\nh4, .h4 { @include font-size($h4-font-size); }\nh5, .h5 { @include font-size($h5-font-size); }\nh6, .h6 { @include font-size($h6-font-size); }\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n @include font-size($display1-size);\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n @include font-size($display2-size);\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n @include font-size($display3-size);\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n @include font-size($display4-size);\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n @include font-size($small-font-size);\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size(90%);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n @include font-size($blockquote-font-size);\n}\n\n.blockquote-footer {\n display: block;\n @include font-size($blockquote-small-font-size);\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled() {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid();\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid();\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid() {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n @include deprecate(\"`img-retina()`\", \"v4.3.0\", \"v5\");\n}\n","// stylelint-disable property-blacklist\n// Single side border-radius\n\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-top-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n }\n}\n\n@mixin border-top-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-right-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-left-radius($radius) {\n @if $enable-rounded {\n border-bottom-left-radius: $radius;\n }\n}\n","// Inline code\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n @include font-size(100%);\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover() {\n color: $table-hover-color;\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover() {\n color: $table-dark-hover-color;\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover() {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// Bootstrap functions\n//\n// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.\n\n// Ascending\n// Used to evaluate Sass maps like our grid breakpoints.\n@mixin _assert-ascending($map, $map-name) {\n $prev-key: null;\n $prev-num: null;\n @each $key, $num in $map {\n @if $prev-num == null or unit($num) == \"%\" or unit($prev-num) == \"%\" {\n // Do nothing\n } @else if not comparable($prev-num, $num) {\n @warn \"Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n } @else if $prev-num >= $num {\n @warn \"Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n }\n $prev-key: $key;\n $prev-num: $num;\n }\n}\n\n// Starts at zero\n// Used to ensure the min-width of the lowest breakpoint starts at 0.\n@mixin _assert-starts-at-zero($map, $map-name: \"$grid-breakpoints\") {\n $values: map-values($map);\n $first-value: nth($values, 1);\n @if $first-value != 0 {\n @warn \"First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.\";\n }\n}\n\n// Replace `$search` with `$replace` in `$string`\n// Used on our SVG icon backgrounds for custom forms.\n//\n// @author Hugo Giraudel\n// @param {String} $string - Initial string\n// @param {String} $search - Substring to replace\n// @param {String} $replace ('') - New value\n// @return {String} - Updated string\n@function str-replace($string, $search, $replace: \"\") {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n// See https://codepen.io/kevinweber/pen/dXWoRw\n@function escape-svg($string) {\n @if str-index($string, \"data:image/svg+xml\") {\n @each $char, $encoded in $escaped-characters {\n $string: str-replace($string, $char, $encoded);\n }\n }\n\n @return $string;\n}\n\n// Color contrast\n@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {\n $r: red($color);\n $g: green($color);\n $b: blue($color);\n\n $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;\n\n @if ($yiq >= $yiq-contrasted-threshold) {\n @return $dark;\n } @else {\n @return $light;\n }\n}\n\n// Retrieve color Sass maps\n@function color($key: \"blue\") {\n @return map-get($colors, $key);\n}\n\n@function theme-color($key: \"primary\") {\n @return map-get($theme-colors, $key);\n}\n\n@function gray($key: \"100\") {\n @return map-get($grays, $key);\n}\n\n// Request a theme color level\n@function theme-color-level($color-name: \"primary\", $level: 0) {\n $color: theme-color($color-name);\n $color-base: if($level > 0, $black, $white);\n $level: abs($level);\n\n @return mix($color-base, $color, $level * $theme-color-interval);\n}\n\n// Return valid calc\n@function add($value1, $value2, $return-calc: true) {\n @if $value1 == null {\n @return $value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 + $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(\" + \") + $value2);\n}\n\n@function subtract($value1, $value2, $return-calc: true) {\n @if $value1 == null and $value2 == null {\n @return null;\n }\n\n @if $value1 == null {\n @return -$value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 - $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(\" - \") + $value2);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: add($input-padding-y, $input-border-width);\n padding-bottom: add($input-padding-y, $input-border-width);\n margin-bottom: 0; // Override the `
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const hrefAttr = element.getAttribute('href')\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''\n }\n\n try {\n return document.querySelector(selector) ? selector : null\n } catch (err) {\n return null\n }\n },\n\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n },\n\n jQueryDetection() {\n if (typeof $ === 'undefined') {\n throw new TypeError('Bootstrap\\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\\'s JavaScript.')\n }\n\n const version = $.fn.jquery.split(' ')[0].split('.')\n const minMajor = 1\n const ltMajor = 2\n const minMinor = 9\n const minPatch = 1\n const maxMajor = 4\n\n if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {\n throw new Error('Bootstrap\\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0')\n }\n }\n}\n\nUtil.jQueryDetection()\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n}\n\nconst Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n}\n\nconst Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLES : '[data-toggle=\"buttons\"]',\n DATA_TOGGLE : '[data-toggle=\"button\"]',\n DATA_TOGGLES_BUTTONS : '[data-toggle=\"buttons\"] .btn',\n INPUT : 'input:not([type=\"hidden\"])',\n ACTIVE : '.active',\n BUTTON : '.btn'\n}\n\nconst Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLES\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(Selector.INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(Selector.ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n } else if (input.type === 'checkbox') {\n if (this._element.tagName === 'LABEL' && input.checked === this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n }\n } else {\n // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input\n triggerChangeEvent = false\n }\n\n if (triggerChangeEvent) {\n input.checked = !this._element.classList.contains(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) {\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)[0]\n }\n\n if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) {\n event.preventDefault() // work around Firefox bug #1540995\n } else {\n const inputBtn = button.querySelector(Selector.INPUT)\n\n if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) {\n event.preventDefault() // work around Firefox bug #1540995\n return\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n }\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n$(window).on(Event.LOAD_DATA_API, () => {\n // ensure correct active class is set to match the controls' actual values/states\n\n // find all checkboxes/readio buttons inside data-toggle groups\n let buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLES_BUTTONS))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n const input = button.querySelector(Selector.INPUT)\n if (input.checked || input.hasAttribute('checked')) {\n button.classList.add(ClassName.ACTIVE)\n } else {\n button.classList.remove(ClassName.ACTIVE)\n }\n }\n\n // find all button toggles\n buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n if (button.getAttribute('aria-pressed') === 'true') {\n button.classList.add(ClassName.ACTIVE)\n } else {\n button.classList.remove(ClassName.ACTIVE)\n }\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n}\n\nconst Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHSTART : `touchstart${EVENT_KEY}`,\n TOUCHMOVE : `touchmove${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n POINTERDOWN : `pointerdown${EVENT_KEY}`,\n POINTERUP : `pointerup${EVENT_KEY}`,\n DRAG_START : `dragstart${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item',\n POINTER_EVENT : 'pointer-event'\n}\n\nconst Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n ITEM_IMG : '.carousel-item img',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n}\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(Selector.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n this.touchDeltaX = 0\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n }\n\n if (this._config.touch) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())\n if (this._pointerEvent) {\n $(this._element).on(Event.POINTERDOWN, (event) => start(event))\n $(this._element).on(Event.POINTERUP, (event) => end(event))\n\n this._element.classList.add(ClassName.POINTER_EVENT)\n } else {\n $(this._element).on(Event.TOUCHSTART, (event) => start(event))\n $(this._element).on(Event.TOUCHMOVE, (event) => move(event))\n $(this._element).on(Event.TOUCHEND, (event) => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))\n $(indicators)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = nextElementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval && _config.ride) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(Event.LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n}\n\nconst Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n}\n\nconst Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(ClassName.COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = document.querySelector(this._config.parent)\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n const children = [].slice.call(parent.querySelectorAll(selector))\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n}\n\nconst Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n}\n\nconst AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n}\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic',\n popperConfig : null\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string',\n popperConfig : '(null|object)'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n this.show(true)\n }\n\n show(usePopper = false) {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar && usePopper) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\n\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(Selector.MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this._config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets, this._element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this._config.offset\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: this._getOffset(),\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n\n return {\n ...popperConfig,\n ...this._config.popperConfig\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n if (context._popper) {\n context._popper.destroy()\n }\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive && event.which === ESCAPE_KEYCODE) {\n return\n }\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = parent.querySelector(Selector.DATA_TOGGLE)\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))\n .filter((item) => $(item).is(':visible'))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDE_PREVENTED : `hidePrevented${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SCROLLABLE : 'modal-dialog-scrollable',\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show',\n STATIC : 'modal-static'\n}\n\nconst Selector = {\n DIALOG : '.modal-dialog',\n MODAL_BODY : '.modal-body',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(Selector.DIALOG)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\n\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n $(document).off(Event.FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _triggerBackdropTransition() {\n if (this._config.backdrop === 'static') {\n const hideEventPrevented = $.Event(Event.HIDE_PREVENTED)\n\n $(this._element).trigger(hideEventPrevented)\n if (hideEventPrevented.defaultPrevented) {\n return\n }\n\n this._element.classList.add(ClassName.STATIC)\n\n const modalTransitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element).one(Util.TRANSITION_END, () => {\n this._element.classList.remove(ClassName.STATIC)\n })\n .emulateTransitionEnd(modalTransitionDuration)\n this._element.focus()\n } else {\n this.hide()\n }\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(ClassName.FADE)\n const modalBody = this._dialog ? this._dialog.querySelector(Selector.MODAL_BODY) : null\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n\n if ($(this._dialog).hasClass(ClassName.SCROLLABLE) && modalBody) {\n modalBody.scrollTop = 0\n } else {\n this._element.scrollTop = 0\n }\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n this._triggerBackdropTransition()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(ClassName.OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): tools/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst uriAttrs = [\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n]\n\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultWhitelist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n\n/**\n * A pattern that recognizes a commonly useful subset of URLs that are safe.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi\n\n/**\n * A pattern that matches safe data URLs. Only matches image, video and audio types.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i\n\nfunction allowedAttribute(attr, allowedAttributeList) {\n const attrName = attr.nodeName.toLowerCase()\n\n if (allowedAttributeList.indexOf(attrName) !== -1) {\n if (uriAttrs.indexOf(attrName) !== -1) {\n return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))\n }\n\n return true\n }\n\n const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)\n\n // Check if a regular expression validates the attribute.\n for (let i = 0, l = regExp.length; i < l; i++) {\n if (attrName.match(regExp[i])) {\n return true\n }\n }\n\n return false\n}\n\nexport function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {\n if (unsafeHtml.length === 0) {\n return unsafeHtml\n }\n\n if (sanitizeFn && typeof sanitizeFn === 'function') {\n return sanitizeFn(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const whitelistKeys = Object.keys(whiteList)\n const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))\n\n for (let i = 0, len = elements.length; i < len; i++) {\n const el = elements[i]\n const elName = el.nodeName.toLowerCase()\n\n if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {\n el.parentNode.removeChild(el)\n\n continue\n }\n\n const attributeList = [].slice.call(el.attributes)\n const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])\n\n attributeList.forEach((attr) => {\n if (!allowedAttribute(attr, whitelistedAttributes)) {\n el.removeAttribute(attr.nodeName)\n }\n })\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n DefaultWhitelist,\n sanitizeHtml\n} from './tools/sanitizer'\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\nconst DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']\n\nconst DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string|function)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)',\n sanitize : 'boolean',\n sanitizeFn : '(null|function)',\n whiteList : 'object',\n popperConfig : '(null|object)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '
' +\n '
' +\n '
',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent',\n sanitize : true,\n sanitizeFn : null,\n whiteList : DefaultWhitelist,\n popperConfig : null\n}\n\nconst HoverState = {\n SHOW : 'show',\n OUT : 'out'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n}\n\nconst Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n}\n\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler)\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (this.config.html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n\n return\n }\n\n if (this.config.html) {\n if (this.config.sanitize) {\n content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)\n }\n\n $element.html(content)\n } else {\n $element.text(content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getPopperConfig(attachment) {\n const defaultBsConfig = {\n placement: attachment,\n modifiers: {\n offset: this._getOffset(),\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n }\n\n return {\n ...defaultBsConfig,\n ...this.config.popperConfig\n }\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this.config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this.config.offset(data.offsets, this.element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this.config.offset\n }\n\n return offset\n }\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container)\n }\n\n return $(document).find(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n })\n\n this._hideModalHandler = () => {\n if (this.element) {\n this.hide()\n }\n }\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n this._hideModalHandler\n )\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n const dataAttributes = $(this.element).data()\n\n Object.keys(dataAttributes)\n .forEach((dataAttr) => {\n if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {\n delete dataAttributes[dataAttr]\n }\n })\n\n config = {\n ...this.constructor.Default,\n ...dataAttributes,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n if (config.sanitize) {\n config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const popperInstance = popperData.instance\n this.tip = popperInstance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
' +\n '
' +\n '

' +\n '
'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n}\n\nconst Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n}\n\nconst OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n const offsetLength = this._offsets.length\n for (let i = offsetLength; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both
    and