├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .fantomasignore ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── fsdocs-gh-pages.yml │ └── publish.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.md ├── README.md ├── StringBuffer.sln ├── StringBuffer.sln.DotSettings ├── build.cmd ├── build.sh ├── build ├── Changelog.fs ├── FsDocs.fs ├── build.fs ├── build.fsproj └── paket.references ├── docsSrc ├── _menu-item_template.html ├── _menu_template.html ├── _template.html ├── content │ ├── fsdocs-custom.css │ ├── fsdocs-dark.css │ ├── fsdocs-light.css │ ├── fsdocs-main.css │ ├── navbar-fixed-left.css │ └── theme-toggle.js └── index.md ├── global.json ├── paket.dependencies ├── paket.lock ├── src ├── Directory.Build.props └── StringBuffer │ ├── AssemblyInfo.fs │ ├── StringBuffer.fs │ ├── StringBuffer.fsproj │ └── paket.references └── tests ├── Directory.Build.props └── StringBuffer.Tests ├── Main.fs ├── StringBuffer.Tests.fsproj ├── Tests.fs └── paket.references /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-reportgenerator-globaltool": { 6 | "version": "5.1.18", 7 | "commands": [ 8 | "reportgenerator" 9 | ] 10 | }, 11 | "paket": { 12 | "version": "7.2.0", 13 | "commands": [ 14 | "paket" 15 | ] 16 | }, 17 | "fsharp-analyzers": { 18 | "version": "0.11.0", 19 | "commands": [ 20 | "fsharp-analyzers" 21 | ] 22 | }, 23 | "fantomas": { 24 | "version": "6.0.1", 25 | "commands": [ 26 | "fantomas" 27 | ] 28 | }, 29 | "fsdocs-tool": { 30 | "version": "18.1.0", 31 | "commands": [ 32 | "fsdocs" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian version (use bullseye on local arm64/Apple Silicon): bookworm, bullseye, buster 2 | ARG VARIANT="bookworm" 3 | FROM buildpack-deps:${VARIANT}-curl 4 | 5 | 6 | ENV \ 7 | # Enable detection of running in a container 8 | DOTNET_RUNNING_IN_CONTAINER=true \ 9 | DOTNET_ROOT=/usr/share/dotnet/ \ 10 | DOTNET_NOLOGO=true \ 11 | DOTNET_CLI_TELEMETRY_OPTOUT=false\ 12 | DOTNET_USE_POLLING_FILE_WATCHER=true 13 | 14 | 15 | # [Optional] Uncomment this section to install additional OS packages. 16 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 17 | # && apt-get -y install --no-install-recommends 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotnet", 3 | // Set the build context one level higher so we can grab metadata like global.json 4 | "context": "..", 5 | "dockerFile": "Dockerfile", 6 | "forwardPorts": [ 7 | 0 8 | ], 9 | "features": { 10 | // https://github.com/devcontainers/features/blob/main/src/common-utils/README.md 11 | "ghcr.io/devcontainers/features/common-utils:2": { 12 | "installZsh": true, 13 | "installOhMyZshConfig": true, 14 | "configureZshAsDefaultShell": true, 15 | "username": "vscode", 16 | "userUid": "1000", 17 | "userGid": "1000", 18 | "upgradePackages": true 19 | }, 20 | // https://github.com/devcontainers/features/blob/main/src/github-cli/README.md 21 | "ghcr.io/devcontainers/features/github-cli:1": {}, 22 | // https://github.com/devcontainers-contrib/features/blob/main/src/starship/README.md 23 | "ghcr.io/devcontainers-contrib/features/starship:1": {}, 24 | // https://github.com/devcontainers/features/blob/main/src/dotnet/README.md 25 | "ghcr.io/devcontainers/features/dotnet:2": { 26 | "version": "7.0", 27 | "additionalVersions": "6.0" 28 | } 29 | }, 30 | "overrideFeatureInstallOrder": [ 31 | "ghcr.io/devcontainers/features/common-utils", 32 | "ghcr.io/devcontainers/features/github-cli", 33 | "ghcr.io/devcontainers-contrib/features/starship", 34 | "ghcr.io/devcontainers/features/dotnet" 35 | ], 36 | "customizations": { 37 | "vscode": { 38 | // Add the IDs of extensions you want installed when the container is created. 39 | "extensions": [ 40 | "ms-dotnettools.csharp", 41 | "Ionide.Ionide-fsharp", 42 | "tintoy.msbuild-project-tools", 43 | "ionide.ionide-paket", 44 | "usernamehw.errorlens", 45 | "alefragnani.Bookmarks", 46 | "oderwat.indent-rainbow", 47 | "vscode-icons-team.vscode-icons", 48 | "EditorConfig.EditorConfig", 49 | "ms-azuretools.vscode-docker", 50 | "GitHub.vscode-pull-request-github", 51 | "github.vscode-github-actions" 52 | ], 53 | "settings": { 54 | "terminal.integrated.defaultProfile.linux": "zsh", 55 | "csharp.suppressDotnetInstallWarning": true 56 | } 57 | } 58 | }, 59 | "remoteUser": "vscode", 60 | "containerUser": "vscode", 61 | "containerEnv": { 62 | // Expose the local environment variable to the container 63 | // They are used for releasing and publishing from the container 64 | "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" 65 | }, 66 | "onCreateCommand": { 67 | "enable-starship": "echo 'eval \"$(starship init zsh)\"' >> ~/.zshrc" 68 | }, 69 | "postAttachCommand": { 70 | "restore": "dotnet tool restore && dotnet restore" 71 | }, 72 | "waitFor": "updateContentCommand" 73 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | trim_trailing_whitespace = true 15 | end_of_line = lf 16 | 17 | [*.{fs,fsi,fsx,config}] 18 | # https://fsprojects.github.io/fantomas/docs/end-users/Configuration.html 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | max_line_length=100 22 | fsharp_multiline_bracket_style = stroustrup 23 | fsharp_keep_max_number_of_blank_lines=2 24 | fsharp_max_array_or_list_number_of_items=1 25 | fsharp_array_or_list_multiline_formatter=number_of_items 26 | fsharp_max_infix_operator_expression=10 27 | fsharp_multi_line_lambda_closing_newline=true 28 | 29 | # Visual Studio Solution Files 30 | [*.sln] 31 | indent_style = tab 32 | 33 | # XML project files 34 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,sfproj}] 35 | indent_size = 2 36 | 37 | # XML config files 38 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 39 | indent_size = 2 40 | 41 | # Markdown Files 42 | [*.{md,mdx}] 43 | trim_trailing_whitespace = false 44 | 45 | # Bash Files 46 | [*.{sh}] 47 | end_of_line = lf 48 | 49 | # Batch Files 50 | [*.{cmd,bat}] 51 | end_of_line = crlf 52 | 53 | # Powershell Files 54 | [*.{ps1, psm1}] 55 | end_of_line = crlf 56 | 57 | # Paket files 58 | [paket.*] 59 | trim_trailing_whitespace = true 60 | indent_size = 2 61 | 62 | [*.paket.references] 63 | trim_trailing_whitespace = true 64 | indent_size = 2 65 | 66 | 67 | # YAML Files 68 | [*.{yml,yaml}] 69 | indent_size = 2 70 | indent_style = space 71 | -------------------------------------------------------------------------------- /.fantomasignore: -------------------------------------------------------------------------------- 1 | # Ignore AssemblyInfo files 2 | AssemblyInfo.fs 3 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This file contains a list of git hashes of revisions to be ignored by git 2 | # These revisions are considered "unimportant" in 3 | # that they are unlikely to be what you are interested in when blaming. 4 | # Like formatting with Fantomas 5 | # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 6 | # Add formatting commits here 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | *.sh text eol=lf 16 | 17 | # Standard to msysgit 18 | *.doc diff=astextplain 19 | *.DOC diff=astextplain 20 | *.docx diff=astextplain 21 | *.DOCX diff=astextplain 22 | *.dot diff=astextplain 23 | *.DOT diff=astextplain 24 | *.pdf diff=astextplain 25 | *.PDF diff=astextplain 26 | *.rtf diff=astextplain 27 | *.RTF diff=astextplain 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing GitHub repo, or reproduction steps 10 | 11 | Please add either a failing test, a GitHub repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to StringBuffer? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | configuration: [Debug, Release] 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup necessary dotnet SDKs 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | global-json-file: global.json 25 | dotnet-version: | 26 | 6.x 27 | 7.x 28 | 29 | - name: Build via Bash 30 | if: runner.os != 'Windows' 31 | run: | 32 | chmod +x ./build.sh 33 | ./build.sh 34 | env: 35 | CI: true 36 | CONFIGURATION: ${{ matrix.configuration }} 37 | ENABLE_COVERAGE: true 38 | - name: Build via Windows 39 | if: runner.os == 'Windows' 40 | run: ./build.cmd 41 | env: 42 | CI: true 43 | CONFIGURATION: ${{ matrix.configuration }} 44 | ENABLE_COVERAGE: true 45 | # Builds the project in a dev container 46 | build-devcontainer: 47 | runs-on: ubuntu-latest 48 | steps: 49 | 50 | - uses: actions/checkout@v3 51 | 52 | - name: Build and run dev container task 53 | uses: devcontainers/ci@v0.3 54 | with: 55 | runCmd: | 56 | chmod +x ./build.sh 57 | ./build.sh 58 | -------------------------------------------------------------------------------- /.github/workflows/fsdocs-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v3 32 | - name: Setup necessary dotnet SDKs 33 | uses: actions/setup-dotnet@v3 34 | with: 35 | global-json-file: global.json 36 | dotnet-version: | 37 | 6.x 38 | - name: Build Docs 39 | run: | 40 | chmod +x ./build.sh 41 | ./build.sh builddocs 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v1 45 | with: 46 | path: docs/ 47 | 48 | # Deployment job 49 | deploy: 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | runs-on: ubuntu-latest 54 | needs: build 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v2 59 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: .NET Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | env: 8 | CONFIGURATION: Release 9 | jobs: 10 | build: 11 | # Sets permissions of the GITHUB_TOKEN to allow release creating 12 | permissions: 13 | contents: write 14 | environment: 15 | name: nuget 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup necessary dotnet SDKs 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | global-json-file: global.json 23 | dotnet-version: | 24 | 6.x 25 | - name: publish 26 | env: 27 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | FAKE_DETAILED_ERRORS: true 30 | ENABLE_COVERAGE: false # AltCover doesn't work with Release builds, reports lower coverage than actual 31 | run: | 32 | chmod +x ./build.sh 33 | ./build.sh Publish 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | packages/ 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | TestResults.xml 255 | 256 | # NuGet packages distributables 257 | dist/ 258 | 259 | # Ionide cache 260 | .ionide/ 261 | 262 | # Test coverage files 263 | coverage.xml 264 | coverage.*.xml 265 | 266 | # Paket tool store 267 | .paket/.store 268 | .paket/paket 269 | 270 | .fake 271 | .ionide 272 | 273 | # fsdocs generated 274 | tmp/ 275 | temp/ 276 | .fsdocs 277 | docs/ 278 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) 241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) 242 | 243 | 244 | %(PaketReferencesFileLinesInfo.PackageVersion) 245 | All 246 | runtime 247 | $(ExcludeAssets);contentFiles 248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive 249 | true 250 | true 251 | 252 | 253 | 254 | 255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 266 | 267 | 268 | %(PaketCliToolFileLinesInfo.PackageVersion) 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | 280 | 281 | false 282 | 283 | 284 | 285 | 286 | 287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 288 | 289 | 290 | 291 | 292 | 293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 294 | true 295 | false 296 | true 297 | false 298 | true 299 | false 300 | true 301 | false 302 | true 303 | false 304 | true 305 | $(PaketIntermediateOutputPath)\$(Configuration) 306 | $(PaketIntermediateOutputPath) 307 | 308 | 309 | 310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 370 | 371 | 420 | 421 | 466 | 467 | 511 | 512 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionide.ionide-paket", 4 | "ionide.ionide-fsharp", 5 | "ionide.ionide-fake", 6 | "ms-dotnettools.csharp", 7 | "editorConfig.editorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore", 3 | "FSharp.enableAnalyzers": false, 4 | "FSharp.analyzersPath": [ 5 | "./packages/analyzers" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.2] - 2024-11-15 9 | 10 | ### Fixed 11 | - `indent` computation expression now directly implements methods such as `_.Zero()` instead of relying on member being visible from base computation expression builder 12 | 13 | ## [1.0.1] - 2024-03-29 14 | 15 | ### Fixed 16 | - Updated README 17 | 18 | ## [1.0.0] - 2024-03-29 19 | 20 | First release 21 | 22 | ### Added 23 | - Initial implementation, unit tests, docs 24 | 25 | [Unreleased]: https://github.com/simon-reynolds/StringBuffer//compare/v1.0.2...HEAD 26 | [1.0.2]: https://github.com/simon-reynolds/StringBuffer//compare/v1.0.1...v1.0.2 27 | [1.0.1]: https://github.com/simon-reynolds/StringBuffer//compare/v1.0.0...v1.0.1 28 | [1.0.0]: https://github.com/simon-reynolds/StringBuffer/releases/tag/v1.0.0 29 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | f#, fsharp 10 | https://github.com/simon-reynolds/StringBuffer 11 | false 12 | LICENSE.md 13 | README.md 14 | git 15 | simon-reynolds 16 | https://github.com/simon-reynolds/StringBuffer 17 | 18 | 19 | false 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | <_BuildProjBaseIntermediateOutputPath>$(MSBuildThisFileDirectory)build/obj/ 9 | <_DotnetToolManifestFile>$(MSBuildThisFileDirectory).config/dotnet-tools.json 10 | <_DotnetToolRestoreOutputFile>$(_BuildProjBaseIntermediateOutputPath)/dotnet-tool-restore-$(NETCoreSdkVersion)-$(OS) 11 | <_DotnetFantomasOutputFile>$(BaseIntermediateOutputPath)dotnet-fantomas-msbuild-$(NETCoreSdkVersion)-$(OS) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StringBuffer 2 | 3 | An F# computation expression for writing code with code 4 | 5 | --- 6 | 7 | ## Builds 8 | 9 | GitHub Actions | 10 | :---: | 11 | [![GitHub Actions](https://github.com/simon-reynolds/StringBuffer/actions/workflows/build.yml/badge.svg)](https://github.com/simon-reynolds/StringBuffer/actions/workflows/build.yml) 12 | [![Build History](https://buildstats.info/github/chart/simon-reynolds/StringBuffer)](https://github.com/simon-reynolds/StringBuffer/actions?query=branch%3Amain) | 13 | 14 | ## NuGet 15 | 16 | Package | Stable | Prerelease 17 | --- | --- | --- 18 | StringBuffer | [![NuGet Badge](https://buildstats.info/nuget/StringBuffer)](https://www.nuget.org/packages/StringBuffer/) | [![NuGet Badge](https://buildstats.info/nuget/StringBuffer?includePreReleases=true)](https://www.nuget.org/packages/StringBuffer/) 19 | 20 | --- 21 | 22 | ### Using 23 | 24 | Install the latest version from [https://www.nuget.org/packages/StringBuffer/](https://www.nuget.org/packages/StringBuffer/) 25 | 26 | Then use `stringBuffer` and `indent` to write your code that writes code. 27 | 28 | ```fsharp 29 | stringBuffer { 30 | "let square x =" 31 | indent { 32 | "x * x" 33 | } 34 | } 35 | ``` 36 | 37 | `stringBuffer` and `indent` can both be nested in each other and will correctly produce output indented as expected. 38 | 39 | ```fsharp 40 | let namespaces = seq { 41 | "System" 42 | "System.IO" 43 | } 44 | 45 | let formattedCode = stringBuffer { 46 | namespaces |> Seq.map (fun ns -> "open " + ns) 47 | 48 | "" 49 | "module MyModule" 50 | indent { 51 | "let LifeUniverseAndEverything () =" 52 | indent { 53 | "42" 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ```fsharp 60 | open System 61 | open System.IO 62 | 63 | module MyModule 64 | let LifeUniverseAndEverything () = 65 | 42 66 | ``` 67 | 68 | ### Developing 69 | 70 | Make sure the following **requirements** are installed on your system: 71 | 72 | - [dotnet SDK](https://www.microsoft.com/net/download/core) 6.0 or higher 73 | 74 | or 75 | 76 | - [VSCode Dev Container](https://code.visualstudio.com/docs/remote/containers) 77 | 78 | -------------------------------------------------------------------------------- /StringBuffer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "StringBuffer", "src\StringBuffer\StringBuffer.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "StringBuffer.Tests", "tests\StringBuffer.Tests\StringBuffer.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj", "{40D2259D-991D-44C4-B45D-C88CE0710C23}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|x64 32 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|x64 33 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|x86 34 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|x86 35 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|x64 38 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|x64 39 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|x86 40 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|x86 41 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|x64 44 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|x64 45 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|x86 46 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|x86 47 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|x64 50 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|x64 51 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|x86 52 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|x86 53 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|x64.ActiveCfg = Debug|Any CPU 56 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|x64.Build.0 = Debug|Any CPU 57 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|x86.ActiveCfg = Debug|Any CPU 58 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Debug|x86.Build.0 = Debug|Any CPU 59 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|x64.ActiveCfg = Release|Any CPU 62 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|x64.Build.0 = Release|Any CPU 63 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|x86.ActiveCfg = Release|Any CPU 64 | {40D2259D-991D-44C4-B45D-C88CE0710C23}.Release|x86.Build.0 = Release|Any CPU 65 | EndGlobalSection 66 | GlobalSection(NestedProjects) = preSolution 67 | {5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 68 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /StringBuffer.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | dotnet run --project ./build/build.fsproj -- -t %* 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | FAKE_DETAILED_ERRORS=true dotnet run --project ./build/build.fsproj -- -t "$@" 7 | -------------------------------------------------------------------------------- /build/Changelog.fs: -------------------------------------------------------------------------------- 1 | module Changelog 2 | 3 | open System 4 | open Fake.Core 5 | open Fake.IO 6 | 7 | let isEmptyChange = 8 | function 9 | | Changelog.Change.Added s 10 | | Changelog.Change.Changed s 11 | | Changelog.Change.Deprecated s 12 | | Changelog.Change.Fixed s 13 | | Changelog.Change.Removed s 14 | | Changelog.Change.Security s 15 | | Changelog.Change.Custom(_, s) -> String.IsNullOrWhiteSpace s.CleanedText 16 | 17 | let tagFromVersionNumber versionNumber = sprintf "v%s" versionNumber 18 | 19 | let failOnEmptyChangelog (latestEntry: Changelog.ChangelogEntry) = 20 | let isEmpty = 21 | (latestEntry.Changes 22 | |> Seq.forall isEmptyChange) 23 | || latestEntry.Changes 24 | |> Seq.isEmpty 25 | 26 | if isEmpty then 27 | failwith 28 | "No changes in CHANGELOG. Please add your changes under a heading specified in https://keepachangelog.com/" 29 | 30 | let mkLinkReference 31 | (newVersion: SemVerInfo) 32 | (changelog: Changelog.Changelog) 33 | (gitHubRepoUrl: string) 34 | = 35 | if 36 | changelog.Entries 37 | |> List.isEmpty 38 | then 39 | // No actual changelog entries yet: link reference will just point to the Git tag 40 | sprintf 41 | "[%s]: %s/releases/tag/%s" 42 | newVersion.AsString 43 | (gitHubRepoUrl.TrimEnd('/')) 44 | (tagFromVersionNumber newVersion.AsString) 45 | else 46 | let versionTuple version = 47 | (version.Major, version.Minor, version.Patch) 48 | // Changelog entries come already sorted, most-recent first, by the Changelog module 49 | let prevEntry = 50 | changelog.Entries 51 | |> List.skipWhile (fun entry -> 52 | entry.SemVer.PreRelease.IsSome 53 | || versionTuple entry.SemVer = versionTuple newVersion 54 | ) 55 | |> List.tryHead 56 | 57 | let linkTarget = 58 | match prevEntry with 59 | | Some entry -> 60 | sprintf 61 | "%s/compare/%s...%s" 62 | gitHubRepoUrl 63 | (tagFromVersionNumber entry.SemVer.AsString) 64 | (tagFromVersionNumber newVersion.AsString) 65 | | None -> 66 | sprintf 67 | "%s/releases/tag/%s" 68 | gitHubRepoUrl 69 | (tagFromVersionNumber newVersion.AsString) 70 | 71 | sprintf "[%s]: %s" newVersion.AsString linkTarget 72 | 73 | let mkReleaseNotes changelog (latestEntry: Changelog.ChangelogEntry) gitHubRepoUrl = 74 | let linkReference = mkLinkReference latestEntry.SemVer changelog gitHubRepoUrl 75 | 76 | if String.isNullOrEmpty linkReference then 77 | latestEntry.ToString() 78 | else 79 | // Add link reference target to description before building release notes, since in main changelog file it's at the bottom of the file 80 | let description = 81 | match latestEntry.Description with 82 | | None -> linkReference 83 | | Some desc when desc.Contains(linkReference) -> desc 84 | | Some desc -> sprintf "%s\n\n%s" (desc.Trim()) linkReference 85 | 86 | { 87 | latestEntry with 88 | Description = Some description 89 | } 90 | .ToString() 91 | 92 | let getVersionNumber envVarName ctx = 93 | let args = ctx.Context.Arguments 94 | 95 | let verArg = 96 | args 97 | |> List.tryHead 98 | |> Option.defaultWith (fun () -> Environment.environVarOrDefault envVarName "") 99 | 100 | if SemVer.isValid verArg then 101 | verArg 102 | elif 103 | verArg.StartsWith("v") 104 | && SemVer.isValid verArg.[1..] 105 | then 106 | let target = ctx.Context.FinalTarget 107 | 108 | Trace.traceImportantfn 109 | "Please specify a version number without leading 'v' next time, e.g. \"./build.sh %s %s\" rather than \"./build.sh %s %s\"" 110 | target 111 | verArg.[1..] 112 | target 113 | verArg 114 | 115 | verArg.[1..] 116 | elif String.isNullOrEmpty verArg then 117 | let target = ctx.Context.FinalTarget 118 | 119 | Trace.traceErrorfn 120 | "Please specify a version number, either at the command line (\"./build.sh %s 1.0.0\") or in the %s environment variable" 121 | target 122 | envVarName 123 | 124 | failwith "No version number found" 125 | else 126 | Trace.traceErrorfn 127 | "Please specify a valid version number: %A could not be recognized as a version number" 128 | verArg 129 | 130 | failwith "Invalid version number" 131 | 132 | let mutable changelogBackupFilename = "" 133 | 134 | let updateChangelog changelogPath (changelog: Fake.Core.Changelog.Changelog) gitHubRepoUrl ctx = 135 | 136 | let verStr = 137 | ctx 138 | |> getVersionNumber "RELEASE_VERSION" 139 | 140 | let description, unreleasedChanges = 141 | match changelog.Unreleased with 142 | | None -> None, [] 143 | | Some u -> u.Description, u.Changes 144 | 145 | let newVersion = SemVer.parse verStr 146 | 147 | changelog.Entries 148 | |> List.tryFind (fun entry -> entry.SemVer = newVersion) 149 | |> Option.iter (fun entry -> 150 | Trace.traceErrorfn 151 | "Version %s already exists in %s, released on %s" 152 | verStr 153 | changelogPath 154 | (if entry.Date.IsSome then 155 | entry.Date.Value.ToString("yyyy-MM-dd") 156 | else 157 | "(no date specified)") 158 | 159 | failwith "Can't release with a duplicate version number" 160 | ) 161 | 162 | changelog.Entries 163 | |> List.tryFind (fun entry -> entry.SemVer > newVersion) 164 | |> Option.iter (fun entry -> 165 | Trace.traceErrorfn 166 | "You're trying to release version %s, but a later version %s already exists, released on %s" 167 | verStr 168 | entry.SemVer.AsString 169 | (if entry.Date.IsSome then 170 | entry.Date.Value.ToString("yyyy-MM-dd") 171 | else 172 | "(no date specified)") 173 | 174 | failwith "Can't release with a version number older than an existing release" 175 | ) 176 | 177 | let versionTuple version = 178 | (version.Major, version.Minor, version.Patch) 179 | 180 | let prereleaseEntries = 181 | changelog.Entries 182 | |> List.filter (fun entry -> 183 | entry.SemVer.PreRelease.IsSome 184 | && versionTuple entry.SemVer = versionTuple newVersion 185 | ) 186 | 187 | let prereleaseChanges = 188 | prereleaseEntries 189 | |> List.collect (fun entry -> 190 | entry.Changes 191 | |> List.filter ( 192 | not 193 | << isEmptyChange 194 | ) 195 | ) 196 | |> List.distinct 197 | 198 | let assemblyVersion, nugetVersion = Changelog.parseVersions newVersion.AsString 199 | 200 | let newEntry = 201 | Changelog.ChangelogEntry.New( 202 | assemblyVersion.Value, 203 | nugetVersion.Value, 204 | Some System.DateTime.Today, 205 | description, 206 | unreleasedChanges 207 | @ prereleaseChanges, 208 | false 209 | ) 210 | 211 | let newChangelog = 212 | Changelog.Changelog.New( 213 | changelog.Header, 214 | changelog.Description, 215 | None, 216 | newEntry 217 | :: changelog.Entries 218 | ) 219 | 220 | // Save changelog to temporary file before making any edits 221 | changelogBackupFilename <- System.IO.Path.GetTempFileName() 222 | 223 | changelogPath 224 | |> Shell.copyFile changelogBackupFilename 225 | 226 | Target.activateFinal "DeleteChangelogBackupFile" 227 | 228 | newChangelog 229 | |> Changelog.save changelogPath 230 | 231 | // Now update the link references at the end of the file 232 | let linkReferenceForLatestEntry = mkLinkReference newVersion changelog gitHubRepoUrl 233 | 234 | let linkReferenceForUnreleased = 235 | sprintf 236 | "[Unreleased]: %s/compare/%s...%s" 237 | gitHubRepoUrl 238 | (tagFromVersionNumber newVersion.AsString) 239 | "HEAD" 240 | 241 | let tailLines = 242 | File.read changelogPath 243 | |> List.ofSeq 244 | |> List.rev 245 | 246 | let isRef (line: string) = 247 | System.Text.RegularExpressions.Regex.IsMatch(line, @"^\[.+?\]:\s?[a-z]+://.*$") 248 | 249 | let linkReferenceTargets = 250 | tailLines 251 | |> List.skipWhile String.isNullOrWhiteSpace 252 | |> List.takeWhile isRef 253 | |> List.rev // Now most recent entry is at the head of the list 254 | 255 | let newLinkReferenceTargets = 256 | match linkReferenceTargets with 257 | | [] -> [ 258 | linkReferenceForUnreleased 259 | linkReferenceForLatestEntry 260 | ] 261 | | first :: rest when 262 | first 263 | |> String.startsWith "[Unreleased]:" 264 | -> 265 | linkReferenceForUnreleased 266 | :: linkReferenceForLatestEntry 267 | :: rest 268 | | first :: rest -> 269 | linkReferenceForUnreleased 270 | :: linkReferenceForLatestEntry 271 | :: first 272 | :: rest 273 | 274 | let blankLineCount = 275 | tailLines 276 | |> Seq.takeWhile String.isNullOrWhiteSpace 277 | |> Seq.length 278 | 279 | let linkRefCount = 280 | linkReferenceTargets 281 | |> List.length 282 | 283 | let skipCount = 284 | blankLineCount 285 | + linkRefCount 286 | 287 | let updatedLines = 288 | List.rev ( 289 | tailLines 290 | |> List.skip skipCount 291 | ) 292 | @ newLinkReferenceTargets 293 | 294 | File.write false changelogPath updatedLines 295 | 296 | // If build fails after this point but before we commit changes, undo our modifications 297 | Target.activateBuildFailure "RevertChangelog" 298 | 299 | newEntry 300 | -------------------------------------------------------------------------------- /build/FsDocs.fs: -------------------------------------------------------------------------------- 1 | namespace Fake.DotNet 2 | 3 | open Fake.Core 4 | open Fake.IO 5 | open Fake.IO.FileSystemOperators 6 | 7 | /// 8 | /// Contains tasks to interact with fsdocs tool to 9 | /// process F# script files, markdown and for generating API documentation. 10 | /// 11 | [] 12 | module Fsdocs = 13 | 14 | /// 15 | /// Fsdocs build command parameters and options 16 | /// 17 | type BuildCommandParams = { 18 | /// Input directory of content (default: docs) 19 | Input: string option 20 | 21 | /// Project files to build API docs for outputs, defaults to all packable projects 22 | Projects: seq option 23 | 24 | /// Output Directory (default output for build and tmp/watch for watch) 25 | Output: string option 26 | 27 | /// Disable generation of API docs 28 | NoApiDocs: bool option 29 | 30 | /// Evaluate F# fragments in scripts 31 | Eval: bool option 32 | 33 | /// Save images referenced in docs 34 | SaveImages: bool option 35 | 36 | /// Add line numbers 37 | LineNumbers: bool option 38 | 39 | /// Additional substitution parameters for templates 40 | Parameters: seq option 41 | 42 | /// Disable project cracking. 43 | IgnoreProjects: bool option 44 | 45 | /// In API doc generation qualify the output by the collection name, e.g. 'reference/FSharp.Core/...' instead of 'reference/...' . 46 | Qualify: bool option 47 | 48 | /// The tool will also generate documentation for non-public members 49 | NoPublic: bool option 50 | 51 | /// Do not copy default content styles, javascript or use default templates 52 | NoDefaultContent: bool option 53 | 54 | /// Clean the output directory 55 | Clean: bool option 56 | 57 | /// Display version information 58 | Version: bool option 59 | 60 | /// Provide properties to dotnet msbuild, e.g. --properties Configuration=Release Version=3.4 61 | Properties: string option 62 | 63 | /// Additional arguments passed down as otherflags to the F# compiler when the API is being generated. 64 | /// Note that these arguments are trimmed, this is to overcome a limitation in the command line argument 65 | /// processing. A typical use-case would be to pass an addition assembly reference. 66 | /// Example --fscoptions " -r:MyAssembly.dll" 67 | FscOptions: string option 68 | 69 | /// Fail if docs are missing or can't be generated 70 | Strict: bool option 71 | 72 | /// Source folder at time of component build (<FsDocsSourceFolder>) 73 | SourceFolder: string option 74 | 75 | /// Source repository for github links (<FsDocsSourceRepository>) 76 | SourceRepository: string option 77 | 78 | /// Assume comments in F# code are markdown (<UsesMarkdownComments>) 79 | MdComments: bool option 80 | } with 81 | 82 | /// Parameter default values. 83 | static member Default = { 84 | Input = None 85 | Projects = None 86 | Output = None 87 | NoApiDocs = None 88 | Eval = None 89 | SaveImages = None 90 | LineNumbers = None 91 | Parameters = None 92 | IgnoreProjects = None 93 | Qualify = None 94 | NoPublic = None 95 | NoDefaultContent = None 96 | Clean = None 97 | Version = None 98 | Properties = None 99 | FscOptions = None 100 | Strict = None 101 | SourceFolder = None 102 | SourceRepository = None 103 | MdComments = None 104 | } 105 | 106 | /// 107 | /// Fsdocs watch command parameters and options 108 | /// 109 | type WatchCommandParams = { 110 | /// Do not serve content when watching. 111 | NoServer: bool option 112 | 113 | /// Do not launch a browser window. 114 | NoLaunch: bool option 115 | 116 | /// URL extension to launch http://localhost:/%s. 117 | Open: string option 118 | 119 | /// Port to serve content for http://localhost serving. 120 | Port: int option 121 | 122 | /// Build Commands 123 | BuildCommandParams: BuildCommandParams option 124 | } with 125 | 126 | /// Parameter default values. 127 | static member Default = { 128 | NoServer = None 129 | NoLaunch = None 130 | Open = None 131 | Port = None 132 | BuildCommandParams = None 133 | } 134 | 135 | let internal buildBuildCommandParams (buildParams: BuildCommandParams) = 136 | let buildSubstitutionParameters (subParameters: seq) = 137 | let subParameters = 138 | subParameters 139 | |> Seq.map (fun (key, value) -> (sprintf "%s %s" key value)) 140 | |> String.concat " " 141 | 142 | sprintf "--parameters %s" subParameters 143 | 144 | System.Text.StringBuilder() 145 | |> StringBuilder.appendIfSome buildParams.Input (sprintf "--input %s") 146 | |> StringBuilder.appendIfSome 147 | buildParams.Projects 148 | (fun projects -> 149 | sprintf 150 | "--projects %s" 151 | (projects 152 | |> String.concat " ") 153 | ) 154 | |> StringBuilder.appendIfSome buildParams.Output (sprintf "--output %s") 155 | |> StringBuilder.appendIfSome buildParams.NoApiDocs (fun _ -> "--noapidocs") 156 | |> StringBuilder.appendIfSome buildParams.Eval (fun _ -> "--eval") 157 | |> StringBuilder.appendIfSome buildParams.SaveImages (fun _ -> "--saveimages") 158 | |> StringBuilder.appendIfSome buildParams.LineNumbers (fun _ -> "--linenumbers") 159 | |> StringBuilder.appendIfSome 160 | buildParams.Parameters 161 | (fun parameters -> buildSubstitutionParameters parameters) 162 | |> StringBuilder.appendIfSome buildParams.IgnoreProjects (fun _ -> "--ignoreprojects") 163 | |> StringBuilder.appendIfSome buildParams.Qualify (fun _ -> "--qualify") 164 | |> StringBuilder.appendIfSome buildParams.NoPublic (fun _ -> "--nonpublic") 165 | |> StringBuilder.appendIfSome buildParams.NoDefaultContent (fun _ -> "--nodefaultcontent") 166 | |> StringBuilder.appendIfSome buildParams.Clean (fun _ -> "--clean") 167 | |> StringBuilder.appendIfSome buildParams.Version (fun _ -> "--version") 168 | |> StringBuilder.appendIfSome buildParams.Properties (sprintf "--properties %s") 169 | |> StringBuilder.appendIfSome buildParams.FscOptions (sprintf "--fscoptions %s") 170 | |> StringBuilder.appendIfSome buildParams.Strict (fun _ -> "--strict") 171 | |> StringBuilder.appendIfSome buildParams.SourceFolder (sprintf "--sourcefolder %s") 172 | |> StringBuilder.appendIfSome buildParams.SourceRepository (sprintf "--sourcerepo %s") 173 | |> StringBuilder.appendIfSome buildParams.MdComments (fun _ -> "--mdcomments") 174 | |> StringBuilder.toText 175 | |> String.trim 176 | 177 | let internal buildWatchCommandParams (watchParams: WatchCommandParams) = 178 | System.Text.StringBuilder() 179 | |> StringBuilder.appendIfSome watchParams.NoServer (fun _ -> "--noserver") 180 | |> StringBuilder.appendIfSome watchParams.NoLaunch (fun _ -> "--nolaunch") 181 | |> StringBuilder.appendIfSome watchParams.Open (sprintf "--open %s") 182 | |> StringBuilder.appendIfSome watchParams.Port (sprintf "--port %i") 183 | |> StringBuilder.appendIfSome watchParams.BuildCommandParams buildBuildCommandParams 184 | |> StringBuilder.toText 185 | |> String.trim 186 | 187 | 188 | let cleanCache (workingDirectory) = 189 | Shell.cleanDirs [ 190 | workingDirectory 191 | ".fsdocs" 192 | ] 193 | 194 | /// 195 | /// Build documentation using fsdocs build command 196 | /// 197 | /// 198 | /// Function used to overwrite the dotnetOptions. 199 | /// Function used to overwrite the build command default parameters. 200 | /// 201 | /// 202 | /// 203 | /// Fsdocs.build (fun p -> { p with Clean = Some(true); Strict = Some(true) }) 204 | /// 205 | /// 206 | let build dotnetOptions setBuildParams = 207 | let buildParams = setBuildParams BuildCommandParams.Default 208 | let formattedParameters = buildBuildCommandParams buildParams 209 | 210 | // let dotnetOptions = (fun (buildOptions: DotNet.Options) -> buildOptions) 211 | let result = DotNet.exec dotnetOptions "fsdocs build" formattedParameters 212 | 213 | if 214 | 0 215 | <> result.ExitCode 216 | then 217 | failwithf "fsdocs build failed with exit code '%d'" result.ExitCode 218 | 219 | /// 220 | /// Watch documentation using fsdocs watch command 221 | /// 222 | /// 223 | /// Function used to overwrite the dotnetOptions. 224 | /// Function used to overwrite the watch command default parameters. 225 | /// 226 | /// 227 | /// 228 | /// Fsdocs.watch (fun p -> { p with Port = Some(3005) }) 229 | /// 230 | /// 231 | let watch dotnetOptions setWatchParams = 232 | let watchParams = setWatchParams WatchCommandParams.Default 233 | let formattedParameters = buildWatchCommandParams watchParams 234 | 235 | // let dotnetOptions = (fun (buildOptions: DotNet.Options) -> buildOptions) 236 | let result = DotNet.exec dotnetOptions "fsdocs watch" formattedParameters 237 | 238 | if 239 | 0 240 | <> result.ExitCode 241 | then 242 | failwithf "fsdocs watch failed with exit code '%d'" result.ExitCode 243 | -------------------------------------------------------------------------------- /build/build.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Fake.Core 3 | open Fake.DotNet 4 | open Fake.Tools 5 | open Fake.IO 6 | open Fake.IO.FileSystemOperators 7 | open Fake.IO.Globbing.Operators 8 | open Fake.Core.TargetOperators 9 | open Fake.Api 10 | open Fake.BuildServer 11 | open Argu 12 | 13 | let environVarAsBoolOrDefault varName defaultValue = 14 | let truthyConsts = [ 15 | "1" 16 | "Y" 17 | "YES" 18 | "T" 19 | "TRUE" 20 | ] 21 | 22 | try 23 | let envvar = (Environment.environVar varName).ToUpper() 24 | 25 | truthyConsts 26 | |> List.exists ((=) envvar) 27 | with _ -> 28 | defaultValue 29 | 30 | //----------------------------------------------------------------------------- 31 | // Metadata and Configuration 32 | //----------------------------------------------------------------------------- 33 | 34 | let rootDirectory = 35 | __SOURCE_DIRECTORY__ 36 | ".." 37 | 38 | let productName = "StringBuffer" 39 | 40 | let sln = 41 | rootDirectory 42 | "StringBuffer.sln" 43 | 44 | let srcCodeGlob = 45 | !!(rootDirectory 46 | "src/**/*.fs") 47 | ++ (rootDirectory 48 | "src/**/*.fsx") 49 | -- (rootDirectory 50 | "src/**/obj/**/*.fs") 51 | 52 | let testsCodeGlob = 53 | !!(rootDirectory 54 | "tests/**/*.fs") 55 | ++ (rootDirectory 56 | "tests/**/*.fsx") 57 | -- (rootDirectory 58 | "tests/**/obj/**/*.fs") 59 | 60 | let srcGlob = 61 | rootDirectory 62 | "src/**/*.??proj" 63 | 64 | let testsGlob = 65 | rootDirectory 66 | "tests/**/*.??proj" 67 | 68 | let srcAndTest = 69 | !!srcGlob 70 | ++ testsGlob 71 | 72 | let distDir = 73 | rootDirectory 74 | "dist" 75 | 76 | let distGlob = 77 | distDir 78 | "*.nupkg" 79 | 80 | let coverageThresholdPercent = 80 81 | 82 | let coverageReportDir = 83 | rootDirectory 84 | "docs" 85 | "coverage" 86 | 87 | 88 | let docsDir = 89 | rootDirectory 90 | "docs" 91 | 92 | let docsSrcDir = 93 | rootDirectory 94 | "docsSrc" 95 | 96 | let temp = 97 | rootDirectory 98 | "temp" 99 | 100 | let watchDocsDir = 101 | temp 102 | "watch-docs" 103 | 104 | let gitOwner = "simon-reynolds" 105 | let gitRepoName = "StringBuffer" 106 | 107 | let gitHubRepoUrl = sprintf "https://github.com/%s/%s/" gitOwner gitRepoName 108 | 109 | let documentationRootUrl = sprintf "https://%s.github.io/%s/" gitOwner gitRepoName 110 | 111 | let releaseBranch = "main" 112 | let readme = "README.md" 113 | let changelogFile = "CHANGELOG.md" 114 | 115 | let READMElink = Uri(Uri(gitHubRepoUrl), $"blob/{releaseBranch}/{readme}") 116 | let CHANGELOGlink = Uri(Uri(gitHubRepoUrl), $"blob/{releaseBranch}/{changelogFile}") 117 | 118 | let changelogPath = 119 | rootDirectory 120 | changelogFile 121 | 122 | let changelog = Fake.Core.Changelog.load changelogPath 123 | 124 | let mutable latestEntry = 125 | if Seq.isEmpty changelog.Entries then 126 | Changelog.ChangelogEntry.New("0.0.1", "0.0.1-alpha.1", Some DateTime.Today, None, [], false) 127 | else 128 | changelog.LatestEntry 129 | 130 | let mutable changelogBackupFilename = "" 131 | 132 | let publishUrl = "https://www.nuget.org" 133 | 134 | let enableCodeCoverage = environVarAsBoolOrDefault "ENABLE_COVERAGE" false 135 | 136 | let githubToken = Environment.environVarOrNone "GITHUB_TOKEN" 137 | 138 | let nugetToken = Environment.environVarOrNone "NUGET_TOKEN" 139 | 140 | //----------------------------------------------------------------------------- 141 | // Helpers 142 | //----------------------------------------------------------------------------- 143 | 144 | 145 | let isRelease (targets: Target list) = 146 | targets 147 | |> Seq.map (fun t -> t.Name) 148 | |> Seq.exists ((=) "PublishToNuGet") 149 | 150 | let invokeAsync f = async { f () } 151 | 152 | let configuration (targets: Target list) = 153 | let defaultVal = if isRelease targets then "Release" else "Debug" 154 | 155 | match Environment.environVarOrDefault "CONFIGURATION" defaultVal with 156 | | "Debug" -> DotNet.BuildConfiguration.Debug 157 | | "Release" -> DotNet.BuildConfiguration.Release 158 | | config -> DotNet.BuildConfiguration.Custom config 159 | 160 | let failOnBadExitAndPrint (p: ProcessResult) = 161 | if 162 | p.ExitCode 163 | <> 0 164 | then 165 | p.Errors 166 | |> Seq.iter Trace.traceError 167 | 168 | failwithf "failed with exitcode %d" p.ExitCode 169 | 170 | 171 | let isCI = lazy environVarAsBoolOrDefault "CI" false 172 | 173 | // CI Servers can have bizarre failures that have nothing to do with your code 174 | let rec retryIfInCI times fn = 175 | match isCI.Value with 176 | | true -> 177 | if times > 1 then 178 | try 179 | fn () 180 | with _ -> 181 | retryIfInCI (times - 1) fn 182 | else 183 | fn () 184 | | _ -> fn () 185 | 186 | let failOnWrongBranch () = 187 | if 188 | Git.Information.getBranchName "" 189 | <> releaseBranch 190 | then 191 | failwithf "Not on %s. If you want to release please switch to this branch." releaseBranch 192 | 193 | 194 | module dotnet = 195 | let watch cmdParam program args = 196 | DotNet.exec cmdParam (sprintf "watch %s" program) args 197 | 198 | let run cmdParam args = DotNet.exec cmdParam "run" args 199 | 200 | let tool optionConfig command args = 201 | DotNet.exec optionConfig (sprintf "%s" command) args 202 | |> failOnBadExitAndPrint 203 | 204 | let reportgenerator optionConfig args = 205 | tool optionConfig "reportgenerator" args 206 | 207 | let sourcelink optionConfig args = tool optionConfig "sourcelink" args 208 | 209 | let fcswatch optionConfig args = tool optionConfig "fcswatch" args 210 | 211 | let fsharpAnalyzer optionConfig args = 212 | tool optionConfig "fsharp-analyzers" args 213 | 214 | let fantomas args = DotNet.exec id "fantomas" args 215 | 216 | module FSharpAnalyzers = 217 | type Arguments = 218 | | Project of string 219 | | Analyzers_Path of string 220 | | Fail_On_Warnings of string list 221 | | Ignore_Files of string list 222 | | Verbose 223 | 224 | interface IArgParserTemplate with 225 | member s.Usage = "" 226 | 227 | 228 | module DocsTool = 229 | let quoted s = $"\"%s{s}\"" 230 | 231 | let fsDocsDotnetOptions (o: DotNet.Options) = { 232 | o with 233 | WorkingDirectory = rootDirectory 234 | } 235 | 236 | let fsDocsBuildParams configuration (p: Fsdocs.BuildCommandParams) = { 237 | p with 238 | Clean = Some true 239 | Input = Some(quoted docsSrcDir) 240 | Output = Some(quoted docsDir) 241 | Eval = Some true 242 | Projects = Some(Seq.map quoted (!!srcGlob)) 243 | Properties = Some($"Configuration=%s{configuration}") 244 | Parameters = 245 | Some [ 246 | // https://fsprojects.github.io/FSharp.Formatting/content.html#Templates-and-Substitutions 247 | "root", quoted documentationRootUrl 248 | "fsdocs-collection-name", quoted productName 249 | "fsdocs-repository-branch", quoted releaseBranch 250 | "fsdocs-package-version", quoted latestEntry.NuGetVersion 251 | "fsdocs-readme-link", quoted (READMElink.ToString()) 252 | "fsdocs-release-notes-link", quoted (CHANGELOGlink.ToString()) 253 | ] 254 | Strict = Some true 255 | } 256 | 257 | 258 | let cleanDocsCache () = Fsdocs.cleanCache rootDirectory 259 | 260 | let build (configuration) = 261 | Fsdocs.build fsDocsDotnetOptions (fsDocsBuildParams configuration) 262 | 263 | 264 | let watch (configuration) = 265 | let buildParams bp = 266 | let bp = 267 | Option.defaultValue Fsdocs.BuildCommandParams.Default bp 268 | |> fsDocsBuildParams configuration 269 | 270 | { 271 | bp with 272 | Output = Some watchDocsDir 273 | Strict = None 274 | } 275 | 276 | Fsdocs.watch 277 | fsDocsDotnetOptions 278 | (fun p -> { 279 | p with 280 | BuildCommandParams = Some(buildParams p.BuildCommandParams) 281 | }) 282 | 283 | let allReleaseChecks () = 284 | failOnWrongBranch () 285 | Changelog.failOnEmptyChangelog latestEntry 286 | 287 | 288 | let failOnLocalBuild () = 289 | if not isCI.Value then 290 | failwith "Not on CI. If you want to publish, please use CI." 291 | 292 | let failOnCIBuild () = 293 | if isCI.Value then 294 | failwith "On CI. If you want to run this target, please use a local build." 295 | 296 | let allPublishChecks () = 297 | failOnLocalBuild () 298 | Changelog.failOnEmptyChangelog latestEntry 299 | 300 | //----------------------------------------------------------------------------- 301 | // Target Implementations 302 | //----------------------------------------------------------------------------- 303 | 304 | /// So we don't require always being on the latest MSBuild.StructuredLogger 305 | let disableBinLog (p: MSBuild.CliArguments) = { p with DisableInternalBinLog = true } 306 | 307 | let clean _ = 308 | [ 309 | "bin" 310 | "temp" 311 | distDir 312 | coverageReportDir 313 | ] 314 | |> Shell.cleanDirs 315 | 316 | !!srcGlob 317 | ++ testsGlob 318 | |> Seq.collect (fun p -> 319 | [ 320 | "bin" 321 | "obj" 322 | ] 323 | |> Seq.map (fun sp -> 324 | IO.Path.GetDirectoryName p 325 | sp 326 | ) 327 | ) 328 | |> Shell.cleanDirs 329 | 330 | [ "paket-files/paket.restore.cached" ] 331 | |> Seq.iter Shell.rm 332 | 333 | let dotnetRestore _ = 334 | [ sln ] 335 | |> Seq.map (fun dir -> 336 | fun () -> 337 | let args = 338 | [] 339 | |> String.concat " " 340 | 341 | DotNet.restore 342 | (fun c -> { 343 | c with 344 | MSBuildParams = disableBinLog c.MSBuildParams 345 | Common = 346 | c.Common 347 | |> DotNet.Options.withCustomParams (Some(args)) 348 | }) 349 | dir 350 | ) 351 | |> Seq.iter (retryIfInCI 10) 352 | 353 | let updateChangelog ctx = 354 | latestEntry <- Changelog.updateChangelog changelogPath changelog gitHubRepoUrl ctx 355 | 356 | let revertChangelog _ = 357 | if String.isNotNullOrEmpty Changelog.changelogBackupFilename then 358 | Changelog.changelogBackupFilename 359 | |> Shell.copyFile changelogPath 360 | 361 | let deleteChangelogBackupFile _ = 362 | if String.isNotNullOrEmpty Changelog.changelogBackupFilename then 363 | Shell.rm Changelog.changelogBackupFilename 364 | 365 | 366 | let dotnetBuild ctx = 367 | let args = [ 368 | sprintf "/p:PackageVersion=%s" latestEntry.NuGetVersion 369 | "--no-restore" 370 | ] 371 | 372 | DotNet.build 373 | (fun c -> { 374 | c with 375 | MSBuildParams = disableBinLog c.MSBuildParams 376 | Configuration = configuration (ctx.Context.AllExecutingTargets) 377 | Common = 378 | c.Common 379 | |> DotNet.Options.withAdditionalArgs args 380 | 381 | }) 382 | sln 383 | 384 | let fsharpAnalyzers _ = 385 | let argParser = 386 | ArgumentParser.Create(programName = "fsharp-analyzers") 387 | 388 | !!srcGlob 389 | |> Seq.iter (fun proj -> 390 | let args = 391 | [ 392 | FSharpAnalyzers.Analyzers_Path( 393 | rootDirectory 394 | "packages/analyzers" 395 | ) 396 | FSharpAnalyzers.Arguments.Project proj 397 | FSharpAnalyzers.Arguments.Fail_On_Warnings [ "BDH0002" ] 398 | FSharpAnalyzers.Arguments.Ignore_Files [ "*AssemblyInfo.fs" ] 399 | FSharpAnalyzers.Verbose 400 | ] 401 | |> argParser.PrintCommandLineArgumentsFlat 402 | 403 | dotnet.fsharpAnalyzer id args 404 | ) 405 | 406 | let dotnetTest ctx = 407 | let excludeCoverage = 408 | !!testsGlob 409 | |> Seq.map IO.Path.GetFileNameWithoutExtension 410 | |> String.concat "|" 411 | 412 | let isGenerateCoverageReport = 413 | ctx.Context.TryFindTarget("GenerateCoverageReport").IsSome 414 | 415 | let args = [ 416 | "--no-build" 417 | if 418 | enableCodeCoverage 419 | || isGenerateCoverageReport 420 | then 421 | sprintf "/p:AltCover=true" 422 | 423 | if not isGenerateCoverageReport then 424 | sprintf "/p:AltCoverThreshold=%d" coverageThresholdPercent 425 | 426 | sprintf "/p:AltCoverAssemblyExcludeFilter=%s" excludeCoverage 427 | "/p:AltCoverLocalSource=true" 428 | ] 429 | 430 | DotNet.test 431 | (fun c -> 432 | 433 | { 434 | c with 435 | MSBuildParams = disableBinLog c.MSBuildParams 436 | Configuration = configuration (ctx.Context.AllExecutingTargets) 437 | Common = 438 | c.Common 439 | |> DotNet.Options.withAdditionalArgs args 440 | }) 441 | sln 442 | 443 | let generateCoverageReport _ = 444 | let coverageReports = 445 | !! "tests/**/coverage*.xml" 446 | |> String.concat ";" 447 | 448 | let sourceDirs = 449 | !!srcGlob 450 | |> Seq.map Path.getDirectory 451 | |> String.concat ";" 452 | 453 | let independentArgs = [ 454 | sprintf "-reports:\"%s\"" coverageReports 455 | sprintf "-targetdir:\"%s\"" coverageReportDir 456 | // Add source dir 457 | sprintf "-sourcedirs:\"%s\"" sourceDirs 458 | // Ignore Tests and if AltCover.Recorder.g sneaks in 459 | sprintf "-assemblyfilters:\"%s\"" "-*.Tests;-AltCover.Recorder.g" 460 | sprintf "-Reporttypes:%s" "Html" 461 | ] 462 | 463 | let args = 464 | independentArgs 465 | |> String.concat " " 466 | 467 | dotnet.reportgenerator id args 468 | 469 | let showCoverageReport _ = 470 | failOnCIBuild () 471 | 472 | coverageReportDir 473 | "index.html" 474 | |> Command.ShellCommand 475 | |> CreateProcess.fromCommand 476 | |> Proc.start 477 | |> ignore 478 | 479 | 480 | let watchTests _ = 481 | !!testsGlob 482 | |> Seq.map (fun proj -> 483 | fun () -> 484 | dotnet.watch 485 | (fun opt -> 486 | opt 487 | |> DotNet.Options.withWorkingDirectory (IO.Path.GetDirectoryName proj) 488 | ) 489 | "test" 490 | "" 491 | |> ignore 492 | ) 493 | |> Seq.iter ( 494 | invokeAsync 495 | >> Async.Catch 496 | >> Async.Ignore 497 | >> Async.Start 498 | ) 499 | 500 | printfn "Press Ctrl+C (or Ctrl+Break) to stop..." 501 | 502 | let cancelEvent = 503 | Console.CancelKeyPress 504 | |> Async.AwaitEvent 505 | |> Async.RunSynchronously 506 | 507 | cancelEvent.Cancel <- true 508 | 509 | let generateAssemblyInfo _ = 510 | 511 | let (|Fsproj|Csproj|Vbproj|) (projFileName: string) = 512 | match projFileName with 513 | | f when f.EndsWith("fsproj") -> Fsproj 514 | | f when f.EndsWith("csproj") -> Csproj 515 | | f when f.EndsWith("vbproj") -> Vbproj 516 | | _ -> 517 | failwith (sprintf "Project file %s not supported. Unknown project type." projFileName) 518 | 519 | let releaseChannel = 520 | match latestEntry.SemVer.PreRelease with 521 | | Some pr -> pr.Name 522 | | _ -> "release" 523 | 524 | let getAssemblyInfoAttributes projectName = [ 525 | AssemblyInfo.Title(projectName) 526 | AssemblyInfo.Product productName 527 | AssemblyInfo.Version latestEntry.AssemblyVersion 528 | AssemblyInfo.Metadata("ReleaseDate", latestEntry.Date.Value.ToString("o")) 529 | AssemblyInfo.FileVersion latestEntry.AssemblyVersion 530 | AssemblyInfo.InformationalVersion latestEntry.AssemblyVersion 531 | AssemblyInfo.Metadata("ReleaseChannel", releaseChannel) 532 | AssemblyInfo.Metadata("GitHash", Git.Information.getCurrentSHA1 (null)) 533 | ] 534 | 535 | let getProjectDetails (projectPath: string) = 536 | let projectName = IO.Path.GetFileNameWithoutExtension(projectPath) 537 | 538 | (projectPath, 539 | projectName, 540 | IO.Path.GetDirectoryName(projectPath), 541 | (getAssemblyInfoAttributes projectName)) 542 | 543 | !!srcGlob 544 | |> Seq.map getProjectDetails 545 | |> Seq.iter (fun (projFileName, _, folderName, attributes) -> 546 | match projFileName with 547 | | Fsproj -> 548 | AssemblyInfoFile.createFSharp 549 | (folderName 550 | "AssemblyInfo.fs") 551 | attributes 552 | | Csproj -> 553 | AssemblyInfoFile.createCSharp 554 | ((folderName 555 | "Properties") 556 | "AssemblyInfo.cs") 557 | attributes 558 | | Vbproj -> 559 | AssemblyInfoFile.createVisualBasic 560 | ((folderName 561 | "My Project") 562 | "AssemblyInfo.vb") 563 | attributes 564 | ) 565 | 566 | let dotnetPack ctx = 567 | // Get release notes with properly-linked version number 568 | let releaseNotes = Changelog.mkReleaseNotes changelog latestEntry gitHubRepoUrl 569 | 570 | let args = [ 571 | $"/p:PackageVersion={latestEntry.NuGetVersion}" 572 | $"/p:PackageReleaseNotes=\"{releaseNotes}\"" 573 | ] 574 | 575 | DotNet.pack 576 | (fun c -> { 577 | c with 578 | MSBuildParams = disableBinLog c.MSBuildParams 579 | Configuration = configuration (ctx.Context.AllExecutingTargets) 580 | OutputPath = Some distDir 581 | Common = 582 | c.Common 583 | |> DotNet.Options.withAdditionalArgs args 584 | }) 585 | sln 586 | 587 | let sourceLinkTest _ = 588 | !!distGlob 589 | |> Seq.iter (fun nupkg -> dotnet.sourcelink id (sprintf "test %s" nupkg)) 590 | 591 | let publishToNuget _ = 592 | allPublishChecks () 593 | 594 | Paket.push (fun c -> { 595 | c with 596 | ToolType = ToolType.CreateLocalTool() 597 | PublishUrl = publishUrl 598 | WorkingDir = "dist" 599 | ApiKey = 600 | match nugetToken with 601 | | Some s -> s 602 | | _ -> c.ApiKey // assume paket-config was set properly 603 | }) 604 | 605 | let gitRelease _ = 606 | allReleaseChecks () 607 | 608 | let releaseNotesGitCommitFormat = latestEntry.ToString() 609 | 610 | Git.Staging.stageFile "" "CHANGELOG.md" 611 | |> ignore 612 | 613 | !!(rootDirectory 614 | "src/**/AssemblyInfo.fs") 615 | ++ (rootDirectory 616 | "tests/**/AssemblyInfo.fs") 617 | |> Seq.iter ( 618 | Git.Staging.stageFile "" 619 | >> ignore 620 | ) 621 | 622 | let msg = 623 | sprintf "Bump version to %s\n\n%s" latestEntry.NuGetVersion releaseNotesGitCommitFormat 624 | 625 | Git.Commit.exec "" msg 626 | 627 | Target.deactivateBuildFailure "RevertChangelog" 628 | 629 | Git.Branches.push "" 630 | 631 | let tag = Changelog.tagFromVersionNumber latestEntry.NuGetVersion 632 | 633 | Git.Branches.tag "" tag 634 | Git.Branches.pushTag "" "origin" tag 635 | 636 | let githubRelease _ = 637 | allPublishChecks () 638 | 639 | let token = 640 | match githubToken with 641 | | Some s -> s 642 | | _ -> 643 | failwith 644 | "please set the github_token environment variable to a github personal access token with repo access." 645 | 646 | let files = !!distGlob 647 | // Get release notes with properly-linked version number 648 | 649 | let releaseNotes = Changelog.mkReleaseNotes changelog latestEntry gitHubRepoUrl 650 | 651 | GitHub.createClientWithToken token 652 | |> GitHub.draftNewRelease 653 | gitOwner 654 | gitRepoName 655 | (Changelog.tagFromVersionNumber latestEntry.NuGetVersion) 656 | (latestEntry.SemVer.PreRelease 657 | <> None) 658 | (releaseNotes 659 | |> Seq.singleton) 660 | |> GitHub.uploadFiles files 661 | |> GitHub.publishDraft 662 | |> Async.RunSynchronously 663 | 664 | let formatCode _ = 665 | let result = dotnet.fantomas $"{rootDirectory}" 666 | 667 | if not result.OK then 668 | printfn "Errors while formatting all files: %A" result.Messages 669 | 670 | let checkFormatCode ctx = 671 | let result = dotnet.fantomas $"{rootDirectory} --check" 672 | 673 | if result.ExitCode = 0 then 674 | Trace.log "No files need formatting" 675 | elif result.ExitCode = 99 then 676 | failwith "Some files need formatting, check output for more info" 677 | else 678 | Trace.logf "Errors while formatting: %A" result.Errors 679 | 680 | 681 | let cleanDocsCache _ = DocsTool.cleanDocsCache () 682 | 683 | let buildDocs ctx = 684 | let configuration = configuration (ctx.Context.AllExecutingTargets) 685 | DocsTool.build (string configuration) 686 | 687 | let watchDocs ctx = 688 | let configuration = configuration (ctx.Context.AllExecutingTargets) 689 | DocsTool.watch (string configuration) 690 | 691 | 692 | let initTargets () = 693 | BuildServer.install [ GitHubActions.Installer ] 694 | 695 | /// Defines a dependency - y is dependent on x. Finishes the chain. 696 | let (==>!) x y = 697 | x ==> y 698 | |> ignore 699 | 700 | /// Defines a soft dependency. x must run before y, if it is present, but y does not require x to be run. Finishes the chain. 701 | let (?=>!) x y = 702 | x ?=> y 703 | |> ignore 704 | //----------------------------------------------------------------------------- 705 | // Hide Secrets in Logger 706 | //----------------------------------------------------------------------------- 707 | Option.iter (TraceSecrets.register "") githubToken 708 | Option.iter (TraceSecrets.register "") nugetToken 709 | //----------------------------------------------------------------------------- 710 | // Target Declaration 711 | //----------------------------------------------------------------------------- 712 | 713 | Target.create "Clean" clean 714 | Target.create "DotnetRestore" dotnetRestore 715 | Target.create "UpdateChangelog" updateChangelog 716 | Target.createBuildFailure "RevertChangelog" revertChangelog // Do NOT put this in the dependency chain 717 | Target.createFinal "DeleteChangelogBackupFile" deleteChangelogBackupFile // Do NOT put this in the dependency chain 718 | Target.create "DotnetBuild" dotnetBuild 719 | Target.create "FSharpAnalyzers" fsharpAnalyzers 720 | Target.create "DotnetTest" dotnetTest 721 | Target.create "GenerateCoverageReport" generateCoverageReport 722 | Target.create "ShowCoverageReport" showCoverageReport 723 | Target.create "WatchTests" watchTests 724 | Target.create "GenerateAssemblyInfo" generateAssemblyInfo 725 | Target.create "DotnetPack" dotnetPack 726 | Target.create "SourceLinkTest" sourceLinkTest 727 | Target.create "PublishToNuGet" publishToNuget 728 | Target.create "GitRelease" gitRelease 729 | Target.create "GitHubRelease" githubRelease 730 | Target.create "FormatCode" formatCode 731 | Target.create "CheckFormatCode" checkFormatCode 732 | Target.create "Release" ignore // For local 733 | Target.create "Publish" ignore //For CI 734 | Target.create "CleanDocsCache" cleanDocsCache 735 | Target.create "BuildDocs" buildDocs 736 | Target.create "WatchDocs" watchDocs 737 | 738 | //----------------------------------------------------------------------------- 739 | // Target Dependencies 740 | //----------------------------------------------------------------------------- 741 | 742 | 743 | // Only call Clean if DotnetPack was in the call chain 744 | // Ensure Clean is called before DotnetRestore 745 | "Clean" 746 | ?=>! "DotnetRestore" 747 | 748 | "Clean" 749 | ==>! "DotnetPack" 750 | 751 | // Only call GenerateAssemblyInfo if GitRelease was in the call chain 752 | // Ensure GenerateAssemblyInfo is called after DotnetRestore and before DotnetBuild 753 | "DotnetRestore" 754 | ?=>! "GenerateAssemblyInfo" 755 | 756 | "GenerateAssemblyInfo" 757 | ?=>! "DotnetBuild" 758 | 759 | // Ensure UpdateChangelog is called after DotnetRestore 760 | "DotnetRestore" 761 | ?=>! "UpdateChangelog" 762 | 763 | "UpdateChangelog" 764 | ?=>! "GenerateAssemblyInfo" 765 | 766 | "CleanDocsCache" 767 | ==>! "BuildDocs" 768 | 769 | "DotnetBuild" 770 | ?=>! "BuildDocs" 771 | 772 | "DotnetBuild" 773 | ==>! "BuildDocs" 774 | 775 | 776 | "DotnetBuild" 777 | ==>! "WatchDocs" 778 | 779 | "DotnetTest" 780 | ==> "GenerateCoverageReport" 781 | ==>! "ShowCoverageReport" 782 | 783 | "UpdateChangelog" 784 | ==> "GenerateAssemblyInfo" 785 | ==> "GitRelease" 786 | ==>! "Release" 787 | 788 | 789 | "DotnetRestore" 790 | =?> ("CheckFormatCode", isCI.Value) 791 | ==> "DotnetBuild" 792 | ==> "DotnetTest" 793 | ==> "DotnetPack" 794 | ==> "PublishToNuGet" 795 | ==> "GitHubRelease" 796 | ==>! "Publish" 797 | 798 | "DotnetRestore" 799 | ==>! "WatchTests" 800 | 801 | //----------------------------------------------------------------------------- 802 | // Target Start 803 | //----------------------------------------------------------------------------- 804 | [] 805 | let main argv = 806 | argv 807 | |> Array.toList 808 | |> Context.FakeExecutionContext.Create false "build.fsx" 809 | |> Context.RuntimeContext.Fake 810 | |> Context.setExecutionContext 811 | 812 | initTargets () 813 | Target.runOrDefaultWithArguments "DotnetPack" 814 | 815 | 0 // return an integer exit code 816 | -------------------------------------------------------------------------------- /build/build.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | 3390;$(WarnOn) 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /build/paket.references: -------------------------------------------------------------------------------- 1 | group Build 2 | FSharp.Core 3 | Fake.IO.FileSystem 4 | Fake.Core.Target 5 | Fake.Core.ReleaseNotes 6 | FAKE.Core.Environment 7 | Fake.DotNet.Cli 8 | FAKE.Core.Process 9 | Fake.DotNet.AssemblyInfoFile 10 | Fake.Tools.Git 11 | Fake.DotNet.Paket 12 | Fake.Api.GitHub 13 | Fake.BuildServer.GitHubActions 14 | Argu 15 | Octokit 16 | -------------------------------------------------------------------------------- /docsSrc/_menu-item_template.html: -------------------------------------------------------------------------------- 1 |
  • {{fsdocs-menu-item-content}}
  • -------------------------------------------------------------------------------- /docsSrc/_menu_template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docsSrc/_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{fsdocs-page-title}} 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 25 | 26 | {{fsdocs-watch-script}} 27 | 28 | 29 | 30 | 125 | 126 | 136 |
    137 | {{fsdocs-content}} 138 | {{fsdocs-tooltips}} 139 |
    140 | 141 | 142 | 144 | 147 | 148 | 149 | 151 | 152 | 155 | 156 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-custom.css: -------------------------------------------------------------------------------- 1 | .fsharp-icon-logo { 2 | width: 25px; 3 | margin-top: -2px; 4 | -webkit-filter: grayscale(100%) brightness(1) invert(1); /* Safari 6.0 - 9.0 */ 5 | filter: grayscale(100%) brightness(1) invert(1); 6 | } 7 | 8 | 9 | body .navbar .dropdown-menu .active .bi { 10 | display: block !important; 11 | } 12 | 13 | nav.navbar .dropdown-item img.fsharp-icon-logo { 14 | margin-right: 0px; 15 | } 16 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-dark.css: -------------------------------------------------------------------------------- 1 | @import url('https://raw.githubusercontent.com/tonsky/FiraCode/fixed/distr/fira_code.css'); 2 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 3 | /*-------------------------------------------------------------------------- 4 | Formatting for page & standard document content 5 | /*--------------------------------------------------------------------------*/ 6 | 7 | :root { 8 | --fsdocs-text-color:#d1d1d1; 9 | --fsdocs-pre-border-color: #000000; 10 | --fsdocs-pre-border-color-top: #070707; 11 | --fsdocs-pre-background-color: #1E1E1E; 12 | --fsdocs-pre-color: #e2e2e2; 13 | --fsdocs-table-pre-background-color: #1d1d1d; 14 | --fsdocs-table-pre-color: #c9c9c9; 15 | 16 | --fsdocs-code-strings-color: #ea9a75; 17 | --fsdocs-code-printf-color: #E0C57F; 18 | --fsdocs-code-escaped-color: #EA8675; 19 | --fsdocs-code-identifiers-color: var(--fsdocs-text-color); 20 | --fsdocs-code-module-color: #43AEC6; 21 | --fsdocs-code-reference-color: #6a8dd8; 22 | --fsdocs-code-value-color: #43AEC6; 23 | --fsdocs-code-interface-color: #43AEC6; 24 | --fsdocs-code-typearg-color: #43AEC6; 25 | --fsdocs-code-disposable-color: #2f798a; 26 | --fsdocs-code-property-color: #43AEC6; 27 | --fsdocs-code-punctuation-color: #43AEC6; 28 | --fsdocs-code-punctuation2-color: #e1e1e1; 29 | --fsdocs-code-function-color: #e1e1e1; 30 | --fsdocs-code-function2-color: #43AEC6; 31 | --fsdocs-code-activepattern-color: #4ec9b0; 32 | --fsdocs-code-unioncase-color: #4ec9b0; 33 | --fsdocs-code-enumeration-color: #4ec9b0; 34 | --fsdocs-code-keywords-color: #2248c4; 35 | --fsdocs-code-comment-color: #329215; 36 | --fsdocs-code-operators-color: #af75c1; 37 | --fsdocs-code-numbers-color: #96C71D; 38 | --fsdocs-code-linenumbers-color: #80b0b0; 39 | --fsdocs-code-mutable-color: #997f0c; 40 | --fsdocs-code-inactive-color: #808080; 41 | --fsdocs-code-preprocessor-color: #af75c1; 42 | --fsdocs-code-fsioutput-color: #808080; 43 | --fsdocs-code-tooltip-color: #d1d1d1; 44 | } 45 | 46 | 47 | .fsdocs-source-link img { 48 | -webkit-filter: grayscale(100%) brightness(1) invert(1); /* Safari 6.0 - 9.0 */ 49 | filter: grayscale(100%) brightness(1) invert(1); 50 | } -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-light.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 2 | /*-------------------------------------------------------------------------- 3 | Formatting for page & standard document content 4 | /*--------------------------------------------------------------------------*/ 5 | 6 | :root { 7 | --fsdocs-text-color:#262626; 8 | --fsdocs-pre-border-color: #d8d8d8; 9 | --fsdocs-pre-border-color-top: #e3e3e3; 10 | --fsdocs-pre-background-color: #f3f4f7; 11 | --fsdocs-pre-color: #8e0e2b; 12 | --fsdocs-table-pre-background-color: #fff7ed; 13 | --fsdocs-table-pre-color: #837b79; 14 | 15 | --fsdocs-code-strings-color: #dd1144; 16 | --fsdocs-code-printf-color: #E0C57F; 17 | --fsdocs-code-escaped-color: #EA8675; 18 | --fsdocs-code-identifiers-color: var(--fsdocs-text-color); 19 | --fsdocs-code-module-color: #009999; 20 | --fsdocs-code-reference-color: #4974D1; 21 | --fsdocs-code-value-color: #43AEC6; 22 | --fsdocs-code-interface-color: #43AEC6; 23 | --fsdocs-code-typearg-color: #43AEC6; 24 | --fsdocs-code-disposable-color: #43AEC6; 25 | --fsdocs-code-property-color: #43AEC6; 26 | --fsdocs-code-punctuation-color: #43AEC6; 27 | --fsdocs-code-punctuation2-color: #var(--fsdocs-text-color); 28 | --fsdocs-code-function-color: #e1e1e1; 29 | --fsdocs-code-function2-color: #990000; 30 | --fsdocs-code-activepattern-color: #4ec9b0; 31 | --fsdocs-code-unioncase-color: #4ec9b0; 32 | --fsdocs-code-enumeration-color: #4ec9b0; 33 | --fsdocs-code-keywords-color: #b68015; 34 | --fsdocs-code-comment-color: #808080; 35 | --fsdocs-code-operators-color: #af75c1; 36 | --fsdocs-code-numbers-color: #009999; 37 | --fsdocs-code-linenumbers-color: #80b0b0; 38 | --fsdocs-code-mutable-color: #d1d1d1; 39 | --fsdocs-code-inactive-color: #808080; 40 | --fsdocs-code-preprocessor-color: #af75c1; 41 | --fsdocs-code-fsioutput-color: #808080; 42 | --fsdocs-code-tooltip-color: #d1d1d1; 43 | } 44 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 2 | /*-------------------------------------------------------------------------- 3 | Formatting for page & standard document content 4 | /*--------------------------------------------------------------------------*/ 5 | 6 | body { 7 | font-family: 'Hind Vadodara', sans-serif; 8 | /* padding-top: 0px; 9 | padding-bottom: 40px; 10 | */ 11 | } 12 | 13 | blockquote { 14 | margin: 0 1em 0 0.25em; 15 | margin-top: 0px; 16 | margin-right: 1em; 17 | margin-bottom: 0px; 18 | margin-left: 0.25em; 19 | padding: 0 .75em 0 1em; 20 | border-left: 1px solid #777; 21 | border-right: 0px solid #777; 22 | } 23 | 24 | /* Format the heading - nicer spacing etc. */ 25 | .masthead { 26 | overflow: hidden; 27 | } 28 | 29 | .masthead .muted a { 30 | text-decoration: none; 31 | color: #999999; 32 | } 33 | 34 | .masthead ul, .masthead li { 35 | margin-bottom: 0px; 36 | } 37 | 38 | .masthead .nav li { 39 | margin-top: 15px; 40 | font-size: 110%; 41 | } 42 | 43 | .masthead h3 { 44 | margin-top: 15px; 45 | margin-bottom: 5px; 46 | font-size: 170%; 47 | } 48 | 49 | /*-------------------------------------------------------------------------- 50 | Formatting fsdocs-content 51 | /*--------------------------------------------------------------------------*/ 52 | 53 | /* Change font sizes for headings etc. */ 54 | #fsdocs-content h1 { 55 | margin: 30px 0px 15px 0px; 56 | /* font-weight: 400; */ 57 | font-size: 2rem; 58 | letter-spacing: 1.78px; 59 | line-height: 2.5rem; 60 | font-weight: 400; 61 | } 62 | 63 | #fsdocs-content h2 { 64 | font-size: 1.6rem; 65 | margin: 20px 0px 10px 0px; 66 | font-weight: 400; 67 | } 68 | 69 | #fsdocs-content h3 { 70 | font-size: 1.2rem; 71 | margin: 15px 0px 10px 0px; 72 | font-weight: 400; 73 | } 74 | 75 | #fsdocs-content hr { 76 | margin: 0px 0px 20px 0px; 77 | } 78 | 79 | #fsdocs-content li { 80 | font-size: 1.0rem; 81 | line-height: 1.375rem; 82 | letter-spacing: 0.01px; 83 | font-weight: 500; 84 | margin: 0px 0px 15px 0px; 85 | } 86 | 87 | #fsdocs-content p { 88 | font-size: 1.0rem; 89 | line-height: 1.375rem; 90 | letter-spacing: 0.01px; 91 | font-weight: 500; 92 | color: var(--fsdocs-text-color);; 93 | } 94 | 95 | #fsdocs-content a:not(.btn) { 96 | color: #4974D1; 97 | } 98 | /* remove the default bootstrap bold on dt elements */ 99 | #fsdocs-content dt { 100 | font-weight: normal; 101 | } 102 | 103 | 104 | 105 | /*-------------------------------------------------------------------------- 106 | Formatting tables in fsdocs-content, using learn.microsoft.com tables 107 | /*--------------------------------------------------------------------------*/ 108 | 109 | #fsdocs-content .table { 110 | table-layout: auto; 111 | width: 100%; 112 | font-size: 0.875rem; 113 | } 114 | 115 | #fsdocs-content .table caption { 116 | font-size: 0.8rem; 117 | font-weight: 600; 118 | letter-spacing: 2px; 119 | text-transform: uppercase; 120 | padding: 1.125rem; 121 | border-width: 0 0 1px; 122 | border-style: solid; 123 | border-color: #e3e3e3; 124 | text-align: right; 125 | } 126 | 127 | #fsdocs-content .table td, 128 | #fsdocs-content .table th { 129 | display: table-cell; 130 | word-wrap: break-word; 131 | padding: 0.75rem 1rem 0.75rem 0rem; 132 | line-height: 1.5; 133 | vertical-align: top; 134 | border-top: 1px solid #e3e3e3; 135 | border-right: 0; 136 | border-left: 0; 137 | border-bottom: 0; 138 | border-style: solid; 139 | } 140 | 141 | /* suppress the top line on inner lists such as tables of exceptions */ 142 | #fsdocs-content .table .fsdocs-exception-list td, 143 | #fsdocs-content .table .fsdocs-exception-list th { 144 | border-top: 0 145 | } 146 | 147 | #fsdocs-content .table td p:first-child, 148 | #fsdocs-content .table th p:first-child { 149 | margin-top: 0; 150 | } 151 | 152 | #fsdocs-content .table td.nowrap, 153 | #fsdocs-content .table th.nowrap { 154 | white-space: nowrap; 155 | } 156 | 157 | #fsdocs-content .table td.is-narrow, 158 | #fsdocs-content .table th.is-narrow { 159 | width: 15%; 160 | } 161 | 162 | #fsdocs-content .table th:not([scope='row']) { 163 | border-top: 0; 164 | border-bottom: 1px; 165 | } 166 | 167 | #fsdocs-content .table > caption + thead > tr:first-child > td, 168 | #fsdocs-content .table > colgroup + thead > tr:first-child > td, 169 | #fsdocs-content .table > thead:first-child > tr:first-child > td { 170 | border-top: 0; 171 | } 172 | 173 | #fsdocs-content .table table-striped > tbody > tr:nth-of-type(odd) { 174 | background-color: var(--box-shadow-light); 175 | } 176 | 177 | #fsdocs-content .table.min { 178 | width: unset; 179 | } 180 | 181 | #fsdocs-content .table.is-left-aligned td:first-child, 182 | #fsdocs-content .table.is-left-aligned th:first-child { 183 | padding-left: 0; 184 | } 185 | 186 | #fsdocs-content .table.is-left-aligned td:first-child a, 187 | #fsdocs-content .table.is-left-aligned th:first-child a { 188 | outline-offset: -0.125rem; 189 | } 190 | 191 | @media screen and (max-width: 767px), screen and (min-resolution: 120dpi) and (max-width: 767.9px) { 192 | #fsdocs-content .table.is-stacked-mobile td:nth-child(1) { 193 | display: block; 194 | width: 100%; 195 | padding: 1rem 0; 196 | } 197 | 198 | #fsdocs-content .table.is-stacked-mobile td:not(:nth-child(1)) { 199 | display: block; 200 | border-width: 0; 201 | padding: 0 0 1rem; 202 | } 203 | } 204 | 205 | #fsdocs-content .table.has-inner-borders th, 206 | #fsdocs-content .table.has-inner-borders td { 207 | border-right: 1px solid #e3e3e3; 208 | } 209 | 210 | #fsdocs-content .table.has-inner-borders th:last-child, 211 | #fsdocs-content .table.has-inner-borders td:last-child { 212 | border-right: none; 213 | } 214 | 215 | .fsdocs-entity-list .fsdocs-entity-name { 216 | width: 25%; 217 | font-weight: bold; 218 | } 219 | 220 | .fsdocs-member-list .fsdocs-member-usage { 221 | width: 35%; 222 | } 223 | 224 | /*-------------------------------------------------------------------------- 225 | Formatting xmldoc sections in fsdocs-content 226 | /*--------------------------------------------------------------------------*/ 227 | 228 | .fsdocs-xmldoc, .fsdocs-entity-xmldoc, .fsdocs-member-xmldoc { 229 | font-size: 1.0rem; 230 | line-height: 1.375rem; 231 | letter-spacing: 0.01px; 232 | font-weight: 500; 233 | color: var(--fsdocs-text-color);; 234 | } 235 | 236 | .fsdocs-xmldoc h1 { 237 | font-size: 1.2rem; 238 | margin: 10px 0px 0px 0px; 239 | } 240 | 241 | .fsdocs-xmldoc h2 { 242 | font-size: 1.2rem; 243 | margin: 10px 0px 0px 0px; 244 | } 245 | 246 | .fsdocs-xmldoc h3 { 247 | font-size: 1.1rem; 248 | margin: 10px 0px 0px 0px; 249 | } 250 | 251 | /* #fsdocs-nav .searchbox { 252 | margin-top: 30px; 253 | margin-bottom: 30px; 254 | } */ 255 | 256 | #fsdocs-nav img.logo{ 257 | width:90%; 258 | /* height:140px; */ 259 | /* margin:10px 0px 0px 20px; */ 260 | margin-top:40px; 261 | border-style:none; 262 | } 263 | 264 | #fsdocs-nav input{ 265 | /* margin-left: 20px; */ 266 | margin-right: 20px; 267 | margin-top: 20px; 268 | margin-bottom: 20px; 269 | width: 93%; 270 | -webkit-border-radius: 0; 271 | border-radius: 0; 272 | } 273 | 274 | #fsdocs-nav { 275 | /* margin-left: -5px; */ 276 | /* width: 90%; */ 277 | font-size:0.95rem; 278 | } 279 | 280 | #fsdocs-nav li.nav-header{ 281 | /* margin-left: -5px; */ 282 | /* width: 90%; */ 283 | padding-left: 0; 284 | color: var(--fsdocs-text-color);; 285 | text-transform: none; 286 | font-size:16px; 287 | margin-top: 9px; 288 | font-weight: bold; 289 | } 290 | 291 | #fsdocs-nav a{ 292 | padding-left: 0; 293 | color: #6c6c6d; 294 | /* margin-left: 5px; */ 295 | /* width: 90%; */ 296 | } 297 | 298 | /*-------------------------------------------------------------------------- 299 | Formatting pre and code sections in fsdocs-content (code highlighting is 300 | further below) 301 | /*--------------------------------------------------------------------------*/ 302 | 303 | #fsdocs-content code { 304 | /* font-size: 0.83rem; */ 305 | font: 0.85rem 'Roboto Mono', monospace; 306 | background-color: #f7f7f900; 307 | border: 0px; 308 | padding: 0px; 309 | /* word-wrap: break-word; */ 310 | /* white-space: pre; */ 311 | } 312 | 313 | /* omitted */ 314 | #fsdocs-content span.omitted { 315 | background: #3c4e52; 316 | border-radius: 5px; 317 | color: #808080; 318 | padding: 0px 0px 1px 0px; 319 | } 320 | 321 | #fsdocs-content pre .fssnip code { 322 | font: 0.86rem 'Roboto Mono', monospace; 323 | } 324 | 325 | #fsdocs-content table.pre, 326 | #fsdocs-content pre.fssnip, 327 | #fsdocs-content pre { 328 | line-height: 13pt; 329 | border: 0px solid var(--fsdocs-pre-border-color); 330 | border-top: 0px solid var(--fsdocs-pre-border-color-top); 331 | border-collapse: separate; 332 | white-space: pre; 333 | font: 0.86rem 'Roboto Mono', monospace; 334 | width: 100%; 335 | margin: 10px 0px 20px 0px; 336 | background-color: var(--fsdocs-pre-background-color); 337 | padding: 10px; 338 | border-radius: 5px; 339 | color: var(--fsdocs-pre-color); 340 | max-width: none; 341 | box-sizing: border-box; 342 | } 343 | 344 | #fsdocs-content pre.fssnip code { 345 | font: 0.86rem 'Roboto Mono', monospace; 346 | font-weight: 600; 347 | } 348 | 349 | #fsdocs-content table.pre { 350 | background-color: var(--fsdocs-table-pre-background-color);; 351 | } 352 | 353 | #fsdocs-content table.pre pre { 354 | padding: 0px; 355 | margin: 0px; 356 | border-radius: 0px; 357 | width: 100%; 358 | background-color: var(--fsdocs-table-pre-background-color); 359 | color: var(--fsdocs-table-pre-color); 360 | } 361 | 362 | #fsdocs-content table.pre td { 363 | padding: 0px; 364 | white-space: normal; 365 | margin: 0px; 366 | width: 100%; 367 | } 368 | 369 | #fsdocs-content table.pre td.lines { 370 | width: 30px; 371 | } 372 | 373 | 374 | #fsdocs-content pre { 375 | word-wrap: inherit; 376 | } 377 | 378 | .fsdocs-example-header { 379 | font-size: 1.0rem; 380 | line-height: 1.375rem; 381 | letter-spacing: 0.01px; 382 | font-weight: 700; 383 | color: var(--fsdocs-text-color);; 384 | } 385 | 386 | /*-------------------------------------------------------------------------- 387 | Formatting github source links 388 | /*--------------------------------------------------------------------------*/ 389 | 390 | .fsdocs-source-link { 391 | float: right; 392 | text-decoration: none; 393 | } 394 | 395 | .fsdocs-source-link img { 396 | border-style: none; 397 | margin-left: 10px; 398 | width: auto; 399 | height: 1.4em; 400 | } 401 | 402 | .fsdocs-source-link .hover { 403 | display: none; 404 | } 405 | 406 | .fsdocs-source-link:hover .hover { 407 | display: block; 408 | } 409 | 410 | .fsdocs-source-link .normal { 411 | display: block; 412 | } 413 | 414 | .fsdocs-source-link:hover .normal { 415 | display: none; 416 | } 417 | 418 | /*-------------------------------------------------------------------------- 419 | Formatting logo 420 | /*--------------------------------------------------------------------------*/ 421 | 422 | #fsdocs-logo { 423 | width:40px; 424 | height:40px; 425 | margin:10px 0px 0px 0px; 426 | border-style:none; 427 | } 428 | 429 | /*-------------------------------------------------------------------------- 430 | 431 | /*--------------------------------------------------------------------------*/ 432 | 433 | #fsdocs-content table.pre pre { 434 | padding: 0px; 435 | margin: 0px; 436 | border: none; 437 | } 438 | 439 | /*-------------------------------------------------------------------------- 440 | Remove formatting from links 441 | /*--------------------------------------------------------------------------*/ 442 | 443 | #fsdocs-content h1 a, 444 | #fsdocs-content h1 a:hover, 445 | #fsdocs-content h1 a:focus, 446 | #fsdocs-content h2 a, 447 | #fsdocs-content h2 a:hover, 448 | #fsdocs-content h2 a:focus, 449 | #fsdocs-content h3 a, 450 | #fsdocs-content h3 a:hover, 451 | #fsdocs-content h3 a:focus, 452 | #fsdocs-content h4 a, 453 | #fsdocs-content h4 a:hover, #fsdocs-content 454 | #fsdocs-content h4 a:focus, 455 | #fsdocs-content h5 a, 456 | #fsdocs-content h5 a:hover, 457 | #fsdocs-content h5 a:focus, 458 | #fsdocs-content h6 a, 459 | #fsdocs-content h6 a:hover, 460 | #fsdocs-content h6 a:focus { 461 | color: var(--fsdocs-text-color);; 462 | text-decoration: none; 463 | text-decoration-style: none; 464 | /* outline: none */ 465 | } 466 | 467 | /*-------------------------------------------------------------------------- 468 | Formatting for F# code snippets 469 | /*--------------------------------------------------------------------------*/ 470 | 471 | .fsdocs-param-name, 472 | .fsdocs-return-name, 473 | .fsdocs-param { 474 | font-weight: 900; 475 | font-size: 0.85rem; 476 | font-family: 'Roboto Mono', monospace; 477 | } 478 | /* strings --- and stlyes for other string related formats */ 479 | #fsdocs-content span.s { 480 | color: var(--fsdocs-code-strings-color); 481 | } 482 | /* printf formatters */ 483 | #fsdocs-content span.pf { 484 | color: var(--fsdocs-code-printf-color); 485 | } 486 | /* escaped chars */ 487 | #fsdocs-content span.e { 488 | color: var(--fsdocs-code-escaped-color); 489 | } 490 | 491 | /* identifiers --- and styles for more specific identifier types */ 492 | #fsdocs-content span.id { 493 | color: var(--fsdocs-identifiers-color);; 494 | } 495 | /* module */ 496 | #fsdocs-content span.m { 497 | color:var(--fsdocs-code-module-color); 498 | } 499 | /* reference type */ 500 | #fsdocs-content span.rt { 501 | color: var(--fsdocs-code-reference-color); 502 | } 503 | /* value type */ 504 | #fsdocs-content span.vt { 505 | color: var(--fsdocs-code-value-color); 506 | } 507 | /* interface */ 508 | #fsdocs-content span.if { 509 | color: var(--fsdocs-code-interface-color); 510 | } 511 | /* type argument */ 512 | #fsdocs-content span.ta { 513 | color: var(--fsdocs-code-typearg-color); 514 | } 515 | /* disposable */ 516 | #fsdocs-content span.d { 517 | color: var(--fsdocs-code-disposable-color); 518 | } 519 | /* property */ 520 | #fsdocs-content span.prop { 521 | color: var(--fsdocs-code-property-color); 522 | } 523 | /* punctuation */ 524 | #fsdocs-content span.p { 525 | color: var(--fsdocs-code-punctuation-color); 526 | } 527 | #fsdocs-content span.pn { 528 | color: var(--fsdocs-code-punctuation2-color); 529 | } 530 | /* function */ 531 | #fsdocs-content span.f { 532 | color: var(--fsdocs-code-function-color); 533 | } 534 | #fsdocs-content span.fn { 535 | color: var(--fsdocs-code-function2-color); 536 | } 537 | /* active pattern */ 538 | #fsdocs-content span.pat { 539 | color: var(--fsdocs-code-activepattern-color); 540 | } 541 | /* union case */ 542 | #fsdocs-content span.u { 543 | color: var(--fsdocs-code-unioncase-color); 544 | } 545 | /* enumeration */ 546 | #fsdocs-content span.e { 547 | color: var(--fsdocs-code-enumeration-color); 548 | } 549 | /* keywords */ 550 | #fsdocs-content span.k { 551 | color: var(--fsdocs-code-keywords-color); 552 | /* font-weight: bold; */ 553 | } 554 | /* comment */ 555 | #fsdocs-content span.c { 556 | color: var(--fsdocs-code-comment-color); 557 | font-weight: 400; 558 | font-style: italic; 559 | } 560 | /* operators */ 561 | #fsdocs-content span.o { 562 | color: var(--fsdocs-code-operators-color); 563 | } 564 | /* numbers */ 565 | #fsdocs-content span.n { 566 | color: var(--fsdocs-code-numbers-color); 567 | } 568 | /* line number */ 569 | #fsdocs-content span.l { 570 | color: var(--fsdocs-code-linenumbers-color); 571 | } 572 | /* mutable var or ref cell */ 573 | #fsdocs-content span.v { 574 | color: var(--fsdocs-code-mutable-color); 575 | font-weight: bold; 576 | } 577 | /* inactive code */ 578 | #fsdocs-content span.inactive { 579 | color: var(--fsdocs-code-inactive-color); 580 | } 581 | /* preprocessor */ 582 | #fsdocs-content span.prep { 583 | color: var(--fsdocs-code-preprocessor-color); 584 | } 585 | /* fsi output */ 586 | #fsdocs-content span.fsi { 587 | color: var(--fsdocs-code-fsioutput-color); 588 | } 589 | 590 | /* tool tip */ 591 | div.fsdocs-tip { 592 | background: #475b5f; 593 | border-radius: 4px; 594 | font: 0.85rem 'Roboto Mono', monospace; 595 | padding: 6px 8px 6px 8px; 596 | display: none; 597 | color: var(--fsdocs-code-tooltip-color); 598 | pointer-events: none; 599 | } 600 | 601 | div.fsdocs-tip code { 602 | color: var(--fsdocs-code-tooltip-color); 603 | font: 0.85rem 'Roboto Mono', monospace; 604 | } 605 | -------------------------------------------------------------------------------- /docsSrc/content/navbar-fixed-left.css: -------------------------------------------------------------------------------- 1 | /* CSS for Bootstrap 5 Fixed Left Sidebar Navigation */ 2 | 3 | 4 | 5 | @media (min-width: 992px){ 6 | 7 | body { 8 | padding-left: 300px; 9 | padding-right: 60px; 10 | } 11 | 12 | #fsdocs-logo { 13 | width:140px; 14 | height:140px; 15 | margin:10px 0px 0px 0px; 16 | border-style:none; 17 | } 18 | 19 | 20 | nav.navbar { 21 | position: fixed; 22 | left: 0; 23 | width: 300px; 24 | bottom: 0; 25 | top: 0; 26 | overflow-y: auto; 27 | overflow-x: hidden; 28 | display: block; 29 | border-right: 1px solid #cecece; 30 | } 31 | 32 | nav.navbar>.container { 33 | flex-direction: column; 34 | padding: 0; 35 | } 36 | 37 | nav.navbar .navbar-nav { 38 | flex-direction: column; 39 | } 40 | nav.navbar .navbar-collapse { 41 | width: 100%; 42 | } 43 | 44 | nav.navbar .navbar-nav { 45 | width: 100%; 46 | } 47 | 48 | nav.navbar .navbar-nav .dropdown-menu { 49 | position: static; 50 | display: block; 51 | } 52 | 53 | nav.navbar .dropdown { 54 | margin-bottom: 5px; 55 | font-size: 14px; 56 | } 57 | 58 | nav.navbar .dropdown-item { 59 | white-space: normal; 60 | font-size: 14px; 61 | vertical-align: middle; 62 | } 63 | 64 | nav.navbar .dropdown-item img { 65 | margin-right: 5px; 66 | } 67 | 68 | nav.navbar .dropdown-toggle { 69 | cursor: default; 70 | } 71 | 72 | nav.navbar .dropdown-menu { 73 | border-radius: 0; 74 | border-left: 0; 75 | border-right: 0; 76 | } 77 | 78 | nav.navbar .dropdown-toggle:not(#bd-theme)::after { 79 | display: none; 80 | } 81 | 82 | .dropdown-menu[data-bs-popper] { 83 | top: auto; 84 | left: auto; 85 | margin-top: auto; 86 | } 87 | 88 | .nav-link:focus, .nav-link:hover { 89 | color: auto; 90 | } 91 | } -------------------------------------------------------------------------------- /docsSrc/content/theme-toggle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. 5 | */ 6 | 7 | (() => { 8 | 'use strict' 9 | 10 | const storedTheme = localStorage.getItem('theme') 11 | 12 | const getPreferredTheme = () => { 13 | if (storedTheme) { 14 | return storedTheme 15 | } 16 | 17 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 18 | } 19 | 20 | const setTheme = function (theme) { 21 | const fsdocsTheme = document.getElementById("fsdocs-theme") 22 | const re = /fsdocs-.*.css/ 23 | if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark') 25 | fsdocsTheme.setAttribute("href", fsdocsTheme.getAttribute("href").replace(re,"fsdocs-dark.css")) 26 | 27 | } else { 28 | document.documentElement.setAttribute('data-bs-theme', theme) 29 | 30 | fsdocsTheme.setAttribute("href", fsdocsTheme.getAttribute("href").replace(re,`fsdocs-${theme}.css`)) 31 | } 32 | } 33 | 34 | setTheme(getPreferredTheme()) 35 | 36 | const showActiveTheme = theme => { 37 | const activeThemeIcon = document.getElementById('theme-icon-active') 38 | const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) 39 | const svgOfActiveBtn = btnToActive.querySelector('i').getAttribute('class') 40 | 41 | document.querySelectorAll('[data-bs-theme-value]').forEach(element => { 42 | element.classList.remove('active') 43 | }) 44 | 45 | btnToActive.classList.add('active') 46 | activeThemeIcon.setAttribute('class', svgOfActiveBtn) 47 | } 48 | 49 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 50 | if (storedTheme !== 'light' || storedTheme !== 'dark') { 51 | setTheme(getPreferredTheme()) 52 | } 53 | }) 54 | 55 | window.addEventListener('DOMContentLoaded', () => { 56 | showActiveTheme(getPreferredTheme()) 57 | 58 | document.querySelectorAll('[data-bs-theme-value]') 59 | .forEach(toggle => { 60 | toggle.addEventListener('click', () => { 61 | const theme = toggle.getAttribute('data-bs-theme-value') 62 | localStorage.setItem('theme', theme) 63 | setTheme(theme) 64 | showActiveTheme(theme) 65 | }) 66 | }) 67 | }) 68 | })() -------------------------------------------------------------------------------- /docsSrc/index.md: -------------------------------------------------------------------------------- 1 | # StringBuffer 2 | 3 | --- 4 | 5 | ## What is StringBuffer? 6 | 7 | StringBuffer is an F# computation expression for being able to easily write formatted text. 8 | 9 | It is built with the goal of being able to easily write code within an F# program. 10 | 11 | ## Why use StringBuffer? 12 | 13 | As part of working on other projects I found myself needing to generate F# code within a program. 14 | 15 | This was particularly problematic considering F#, like many languages, is whitespace sensitive, using indentation to declare scopes. 16 | 17 | I found myself needing a way to create code with correct indentation while still being able to break apart large chunks of generated code into smaller sections that could be built separately and then put together to create the final file. 18 | 19 | --- 20 | 21 | # Installing StringBuffer 22 | 23 | Simply add it from nuget, paket, however you source your packages. The module automatically opens and will be available throughout your project. 24 | 25 | Find the latest version at 26 | 27 | The module provides two computation expressions: 28 | 29 | * `stringBuffer` 30 | * This is the one you want to start off with 31 | * `indent` 32 | * Have a part of a `stringBuffer` you want to indent? Use this 33 | 34 | # Samples 35 | 36 | ## Basic build 37 | 38 | ```fsharp 39 | let result = stringBuffer { 40 | "My first line" 41 | "My second line" 42 | sprintf "This line has a number in it: %i" 3 43 | } 44 | 45 | ``` 46 | 47 | Output 48 | 49 | ``` 50 | My first line 51 | My second line 52 | This line has a number in it: 3 53 | ``` 54 | 55 | ## Using variables and loops 56 | ```fsharp 57 | let firstLine = "Line 1" 58 | let moreLines = [2 .. 5] 59 | 60 | let result = stringBuffer { 61 | firstLine 62 | moreLines |> Seq.map(fun i -> sprintf "Line %d" i) 63 | } 64 | ``` 65 | Output 66 | ``` 67 | Line 1 68 | Line 2 69 | Line 3 70 | Line 4 71 | Line 5 72 | ``` 73 | 74 | ## Use multiple lines, even other instances of stringBuffer 75 | ```fsharp 76 | let firstTwoLines = seq { 77 | "Line 1" 78 | "Line 2" 79 | } 80 | 81 | let thirdLine = "Line 3" 82 | 83 | let result = stringBuffer { 84 | firstTwoLines 85 | thirdLine 86 | "Line 4" 87 | 88 | stringBuffer { 89 | "Line 5" 90 | } 91 | 92 | } 93 | ``` 94 | 95 | Output 96 | ``` 97 | Line 1 98 | Line 2 99 | Line 3 100 | Line 4 101 | Line 5 102 | ``` 103 | 104 | ## Indenting 105 | ```fsharp 106 | let result = stringBuffer { 107 | "let square x =" 108 | indent { 109 | "x * x" 110 | } 111 | } 112 | ``` 113 | 114 | Output 115 | ``` 116 | let square x = 117 | x * x 118 | ``` 119 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.100", 4 | "rollForward": "feature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | source https://api.nuget.org/v3/index.json 3 | storage: none 4 | nuget FSharp.Core >= 6.0 lowest_matching: true 5 | nuget Microsoft.SourceLink.GitHub copy_local: true 6 | nuget Expecto 7 | nuget YoloDev.Expecto.TestSdk 8 | nuget Microsoft.NET.Test.Sdk 9 | nuget altcover 10 | 11 | // [ FAKE GROUP ] 12 | group Build 13 | storage: none 14 | source https://www.nuget.org/api/v2 15 | source https://api.nuget.org/v3/index.json 16 | nuget Fake.IO.FileSystem 17 | nuget Fake.Core.Target 18 | nuget Fake.Core.ReleaseNotes 19 | nuget FAKE.Core.Environment 20 | nuget Fake.DotNet.Cli 21 | nuget FAKE.Core.Process 22 | nuget Fake.DotNet.AssemblyInfoFile 23 | nuget Fake.Tools.Git 24 | nuget Fake.DotNet.Paket 25 | nuget Fake.Api.GitHub 26 | nuget Fake.BuildServer.GitHubActions 27 | nuget Argu 28 | nuget Octokit 29 | nuget Microsoft.Build 17.3.2 30 | nuget Microsoft.Build.Framework 17.3.2 31 | nuget Microsoft.Build.Tasks.Core 17.3.2 32 | nuget Microsoft.Build.Utilities.Core 17.3.2 33 | 34 | group Analyzers 35 | source https://www.nuget.org/api/v2 36 | source https://api.nuget.org/v3/index.json 37 | nuget BinaryDefense.FSharp.Analyzers.Hashing 0.2.2 38 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | true 6 | false 7 | 8 | 9 | true 10 | true 11 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/StringBuffer/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "StringBuffer" 17 | let [] AssemblyProduct = "StringBuffer" 18 | let [] AssemblyVersion = "1.0.2" 19 | let [] AssemblyMetadata_ReleaseDate = "2024-11-15T00:00:00.0000000+00:00" 20 | let [] AssemblyFileVersion = "1.0.2" 21 | let [] AssemblyInformationalVersion = "1.0.2" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "4a937e6a9c4ae145986a1c30c6f7b3979c849b6e" 24 | -------------------------------------------------------------------------------- /src/StringBuffer/StringBuffer.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module rec StringBuffer 3 | 4 | open System 5 | open System.Text 6 | open System.IO 7 | 8 | type private Indenter(s: IndentedStringBuilder) = 9 | 10 | do 11 | s.IncrementIndent() 12 | |> ignore 13 | 14 | interface IDisposable with 15 | 16 | member this.Dispose() = 17 | s.DecrementIndent() 18 | |> ignore 19 | 20 | 21 | type IndentedStringBuilder() = 22 | 23 | let indentSize = 4 24 | 25 | let mutable _indent = 0 26 | let mutable _indentPending = true 27 | 28 | let _stringBuilder = StringBuilder() 29 | 30 | let doIndent () = 31 | if 32 | _indentPending 33 | && _indent > 0 34 | then 35 | _stringBuilder.Append( 36 | ' ', 37 | _indent 38 | * indentSize 39 | ) 40 | |> ignore 41 | 42 | _indentPending <- false 43 | 44 | member this.IndentCount = _indent 45 | member this.Length = _stringBuilder.Length 46 | 47 | member this.Clear() = 48 | _stringBuilder.Clear() 49 | |> ignore 50 | 51 | _indent <- 0 52 | this 53 | 54 | member this.IncrementIndent() = 55 | _indent <- 56 | _indent 57 | + 1 58 | 59 | this 60 | 61 | member this.DecrementIndent() = 62 | _indent <- 63 | Math.Max( 64 | _indent 65 | - 1, 66 | 0 67 | ) 68 | 69 | this 70 | 71 | member this.Indent() = new Indenter(this) :> IDisposable 72 | 73 | member this.Append(value: string) = 74 | doIndent () 75 | 76 | _stringBuilder.Append(value) 77 | |> ignore 78 | 79 | this 80 | 81 | member this.Append(value: FormattableString) = 82 | doIndent () 83 | 84 | _stringBuilder.Append(value) 85 | |> ignore 86 | 87 | this 88 | 89 | member this.Append(value: char) = 90 | doIndent () 91 | 92 | _stringBuilder.Append(value) 93 | |> ignore 94 | 95 | this 96 | 97 | member this.Append(values: string seq) = 98 | doIndent () 99 | 100 | values 101 | |> Seq.iter ( 102 | _stringBuilder.Append 103 | >> ignore 104 | ) 105 | 106 | this 107 | 108 | member this.Append(values: char seq) = 109 | doIndent () 110 | 111 | values 112 | |> Seq.iter ( 113 | _stringBuilder.Append 114 | >> ignore 115 | ) 116 | 117 | this 118 | 119 | member this.AppendLine(value: string) = 120 | if 121 | value.Length 122 | <> 0 123 | then 124 | doIndent () 125 | 126 | _stringBuilder.AppendLine(value) 127 | |> ignore 128 | 129 | _indentPending <- true 130 | 131 | this 132 | 133 | member this.AppendLine(value: FormattableString) = 134 | doIndent () 135 | 136 | _stringBuilder.Append(value).AppendLine() 137 | |> ignore 138 | 139 | _indentPending <- true 140 | 141 | this 142 | 143 | member this.AppendLine() = 144 | doIndent () 145 | 146 | this.AppendLine(String.Empty) 147 | |> ignore 148 | 149 | this 150 | 151 | member this.AppendLines(value: string, ?skipFinalNewLine: bool) = 152 | 153 | let skipFinalNewLine = defaultArg skipFinalNewLine false 154 | 155 | use reader = new StringReader(value) 156 | 157 | Seq.initInfinite (fun _ -> reader.ReadLine()) 158 | |> Seq.takeWhile ( 159 | isNull 160 | >> not 161 | ) 162 | |> Seq.iteri (fun idx line -> 163 | if idx <> 0 then 164 | this.AppendLine() 165 | |> ignore 166 | 167 | if line.Length > 0 then 168 | this.Append line 169 | |> ignore 170 | ) 171 | 172 | if not skipFinalNewLine then 173 | this.AppendLine() 174 | |> ignore 175 | 176 | this 177 | 178 | member this.AppendLines(values: string seq) = 179 | doIndent () 180 | 181 | values 182 | |> Seq.iter ( 183 | _stringBuilder.AppendLine 184 | >> ignore 185 | ) 186 | 187 | this 188 | 189 | member this.AppendLines(values: char seq) = 190 | doIndent () 191 | 192 | values 193 | |> Seq.iter ( 194 | string 195 | >> _stringBuilder.AppendLine 196 | >> ignore 197 | ) 198 | 199 | this 200 | 201 | member this.AppendJoin(values: string seq, ?separator: string) = 202 | 203 | let separator = defaultArg separator ", " 204 | 205 | doIndent () 206 | 207 | _stringBuilder.AppendJoin(separator, values) 208 | |> ignore 209 | 210 | this 211 | 212 | member this.AppendJoin(separator: string, [] values: string[]) = 213 | 214 | doIndent () 215 | 216 | _stringBuilder.AppendJoin(separator, values) 217 | |> ignore 218 | 219 | this 220 | 221 | override this.ToString() = _stringBuilder.ToString() 222 | 223 | type StringBuffer = IndentedStringBuilder -> unit 224 | 225 | type private IndentationLevel = 226 | | NoIndent 227 | | Indent 228 | 229 | let private writeStringBuffer (f: StringBuffer) (indent: IndentationLevel) = 230 | let b = IndentedStringBuilder() 231 | 232 | match indent with 233 | | NoIndent -> () 234 | | Indent -> 235 | b.IncrementIndent() 236 | |> ignore 237 | 238 | do f b 239 | 240 | match indent with 241 | | NoIndent -> () 242 | | Indent -> 243 | b.DecrementIndent() 244 | |> ignore 245 | 246 | b.ToString() 247 | 248 | let writeNamespaces (namespaces: string seq) = 249 | let strings = 250 | namespaces 251 | |> Seq.map (fun n -> 252 | "open " 253 | + n 254 | ) 255 | 256 | String.Join(Environment.NewLine, strings) 257 | 258 | type StringBufferBuilder() = 259 | 260 | member _.Yield(txt: string) = 261 | fun (b: IndentedStringBuilder) -> 262 | Printf.kprintf 263 | (b.AppendLines 264 | >> ignore) 265 | $"%s{txt}" 266 | 267 | member _.Yield(c: char) = 268 | fun (b: IndentedStringBuilder) -> 269 | Printf.kprintf 270 | (b.AppendLine 271 | >> ignore) 272 | $"%c{c}" 273 | 274 | member _.YieldFrom(f: StringBuffer) = f 275 | 276 | member _.Combine(f, g) = 277 | fun (b: IndentedStringBuilder) -> 278 | f b 279 | g b 280 | 281 | member _.Delay f = 282 | fun (b: IndentedStringBuilder) -> (f ()) b 283 | 284 | member _.Zero() = fun (_: IndentedStringBuilder) -> () 285 | 286 | member _.For(xs: 'a seq, f: 'a -> StringBuffer) = 287 | fun (b: IndentedStringBuilder) -> 288 | use e = xs.GetEnumerator() 289 | 290 | while e.MoveNext() do 291 | (f e.Current) b 292 | 293 | member _.While(p: unit -> bool, f: StringBuffer) = 294 | fun (b: IndentedStringBuilder) -> 295 | while p () do 296 | f b 297 | 298 | abstract member Run: StringBuffer -> string 299 | default this.Run(f: StringBuffer) = writeStringBuffer f NoIndent 300 | 301 | member _.Yield(lines: string seq) = 302 | fun (b: IndentedStringBuilder) -> 303 | lines 304 | |> Seq.iter (fun txt -> 305 | Printf.kprintf 306 | (b.AppendLines 307 | >> ignore) 308 | $"%s{txt}" 309 | ) 310 | 311 | member _.Yield(lines: char seq) = 312 | fun (b: IndentedStringBuilder) -> 313 | lines 314 | |> Seq.iter (fun txt -> 315 | Printf.kprintf 316 | (b.AppendLines 317 | >> ignore) 318 | $"%c{txt}" 319 | ) 320 | 321 | member _.Yield(txt: string option) = 322 | fun (b: IndentedStringBuilder) -> 323 | match txt with 324 | | Some t -> 325 | Printf.kprintf 326 | (b.AppendLines 327 | >> ignore) 328 | $"%s{t}" 329 | | None -> () 330 | 331 | member _.Yield(txt: string option seq) = 332 | fun (b: IndentedStringBuilder) -> 333 | for tt in txt do 334 | match tt with 335 | | Some t -> 336 | Printf.kprintf 337 | (b.AppendLines 338 | >> ignore) 339 | $"%s{t}" 340 | | None -> () 341 | 342 | 343 | type IndentStringBufferBuilder() = 344 | inherit StringBufferBuilder() 345 | 346 | member _.Yield(txt: string) = base.Yield(txt) 347 | 348 | member _.Yield(c: char) = base.Yield(c) 349 | 350 | member _.YieldFrom(f: StringBuffer) = base.YieldFrom(f) 351 | 352 | member _.Combine(f, g) = base.Combine(f, g) 353 | 354 | member _.Delay f = base.Delay f 355 | 356 | member _.Zero() = base.Zero() 357 | 358 | member _.For(xs: 'a seq, f: 'a -> StringBuffer) = base.For(xs, f) 359 | 360 | member _.While(p: unit -> bool, f: StringBuffer) = base.While(p, f) 361 | 362 | member _.Yield(lines: string seq) = base.Yield(lines) 363 | 364 | member _.Yield(lines: char seq) = base.Yield(lines) 365 | 366 | member _.Yield(txt: string option) = base.Yield(txt) 367 | 368 | member _.Yield(txt: string option seq) = base.Yield(txt) 369 | 370 | override _.Run(f: StringBuffer) = writeStringBuffer f Indent 371 | 372 | let stringBuffer = StringBufferBuilder() 373 | let indent = IndentStringBufferBuilder() 374 | -------------------------------------------------------------------------------- /src/StringBuffer/StringBuffer.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | StringBuffer 8 | An F# library that provides an easy way to write code that writes code 9 | 10 | 11 | 12 | true 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/StringBuffer/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Microsoft.SourceLink.GitHub 3 | 4 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/StringBuffer.Tests/Main.fs: -------------------------------------------------------------------------------- 1 | namespace StringBuffer.Tests 2 | 3 | module ExpectoTemplate = 4 | 5 | open Expecto 6 | 7 | [] 8 | let main argv = 9 | Tests.runTestsInAssembly defaultConfig argv 10 | -------------------------------------------------------------------------------- /tests/StringBuffer.Tests/StringBuffer.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/StringBuffer.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | namespace StringBuffer.Tests 2 | 3 | open System 4 | open Expecto 5 | 6 | module Tests = 7 | 8 | [] 9 | let indentedStringBuilderTests = 10 | testList "IndentedStringBuilder Tests" [ 11 | testCase "Test IncrementIndent and DecrementIndent" 12 | <| fun _ -> 13 | let subject = 14 | IndentedStringBuilder() 15 | .AppendLine("Line 1") 16 | .IncrementIndent() 17 | .AppendLine("Line 2") 18 | .DecrementIndent() 19 | .Append("Line 3") 20 | .ToString() 21 | 22 | let expected = 23 | "Line 1" 24 | + Environment.NewLine 25 | + " Line 2" 26 | + Environment.NewLine 27 | + "Line 3" 28 | 29 | Expect.equal subject expected "Works as expected" 30 | 31 | testCase "Test Properties" 32 | <| fun _ -> 33 | let subject = IndentedStringBuilder() 34 | 35 | Expect.equal subject.Length 0 "Initial Length is zero" 36 | Expect.equal subject.IndentCount 0 "Initial IndentCount is zero" 37 | 38 | subject.Append 'c' 39 | |> ignore 40 | 41 | Expect.equal subject.Length 1 "Initial Length is one" 42 | 43 | subject.Clear() 44 | |> ignore 45 | 46 | Expect.equal subject.Length 0 "Initial Length is zero" 47 | 48 | testCase "Indenter Disposable works as expected" 49 | <| fun _ -> 50 | let subject = IndentedStringBuilder().AppendLine("Line 1") 51 | 52 | use indent = subject.Indent() 53 | 54 | subject.AppendLine("Line 2") 55 | |> ignore 56 | 57 | indent.Dispose() 58 | 59 | subject.Append("Line 3") 60 | |> ignore 61 | 62 | let expected = 63 | "Line 1" 64 | + Environment.NewLine 65 | + " Line 2" 66 | + Environment.NewLine 67 | + "Line 3" 68 | 69 | Expect.equal (subject.ToString()) expected "Works as expected" 70 | 71 | testCase "Append - FormattableString works as expected" 72 | <| fun _ -> 73 | 74 | let s: FormattableString = $"Test" 75 | 76 | let subject = IndentedStringBuilder().Append(s).ToString() 77 | 78 | let expected = "Test" 79 | 80 | Expect.equal subject expected "Works as expected" 81 | 82 | testCase "AppendLine - FormattableString works as expected" 83 | <| fun _ -> 84 | 85 | let s: FormattableString = $"Test" 86 | 87 | let subject = IndentedStringBuilder().AppendLine(s).ToString() 88 | 89 | let expected = 90 | "Test" 91 | + Environment.NewLine 92 | 93 | Expect.equal subject expected "Works as expected" 94 | 95 | testCase "Append - String seq works as expected" 96 | <| fun _ -> 97 | 98 | let s = 99 | seq { 100 | "Line 1" 101 | "Line 2" 102 | } 103 | 104 | let subject = IndentedStringBuilder().Append(s).ToString() 105 | 106 | let expected = "Line 1Line 2" 107 | 108 | Expect.equal subject expected "Works as expected" 109 | 110 | testCase "Append - Char seq works as expected" 111 | <| fun _ -> 112 | 113 | let s = 114 | seq { 115 | 'a' 116 | 'b' 117 | } 118 | 119 | let subject = IndentedStringBuilder().Append(s).ToString() 120 | 121 | let expected = "ab" 122 | 123 | Expect.equal subject expected "Works as expected" 124 | 125 | testCase "AppendLines - String seq works as expected" 126 | <| fun _ -> 127 | 128 | let s = 129 | seq { 130 | "Line 1" 131 | "Line 2" 132 | } 133 | 134 | let subject = IndentedStringBuilder().AppendLines(s).ToString() 135 | 136 | let expected = 137 | "Line 1" 138 | + Environment.NewLine 139 | + "Line 2" 140 | + Environment.NewLine 141 | 142 | Expect.equal subject expected "Works as expected" 143 | 144 | testCase "AppendLines - Char seq works as expected" 145 | <| fun _ -> 146 | 147 | let s = 148 | seq { 149 | 'a' 150 | 'b' 151 | } 152 | 153 | let subject = IndentedStringBuilder().AppendLines(s).ToString() 154 | 155 | let expected = 156 | "a" 157 | + Environment.NewLine 158 | + "b" 159 | + Environment.NewLine 160 | 161 | Expect.equal subject expected "Works as expected" 162 | 163 | testCase "AppendLines - skipFinalNewLine works as expected" 164 | <| fun _ -> 165 | 166 | let initial = 167 | "a" 168 | + Environment.NewLine 169 | + "b" 170 | + Environment.NewLine 171 | + "" 172 | + Environment.NewLine 173 | + "c" 174 | 175 | 176 | let subject = IndentedStringBuilder().AppendLines(initial, false).ToString() 177 | 178 | let expected = 179 | "a" 180 | + Environment.NewLine 181 | + "b" 182 | + Environment.NewLine 183 | + "" 184 | + Environment.NewLine 185 | + "c" 186 | + Environment.NewLine 187 | 188 | Expect.equal subject expected "Works as expected" 189 | 190 | let subject = IndentedStringBuilder().AppendLines(initial, true).ToString() 191 | 192 | let expected = 193 | "a" 194 | + Environment.NewLine 195 | + "b" 196 | + Environment.NewLine 197 | + "" 198 | + Environment.NewLine 199 | + "c" 200 | 201 | Expect.equal subject expected "Works as expected" 202 | 203 | testCase "AppendJoin - String seq works as expected" 204 | <| fun _ -> 205 | let values = 206 | seq { 207 | "Line 1" 208 | "Line 2" 209 | } 210 | 211 | let defaultSeparator = IndentedStringBuilder().AppendJoin(values).ToString() 212 | 213 | Expect.equal defaultSeparator "Line 1, Line 2" "Works as expected" 214 | 215 | let customSeparator = IndentedStringBuilder().AppendJoin(values, "|").ToString() 216 | 217 | Expect.equal customSeparator "Line 1|Line 2" "Works as expected" 218 | 219 | testCase "AppendJoin - ParamArray works as expected" 220 | <| fun _ -> 221 | let values = [| 222 | "Line 1" 223 | "Line 2" 224 | |] 225 | 226 | let asArray = IndentedStringBuilder().AppendJoin("|", values).ToString() 227 | 228 | Expect.equal asArray "Line 1|Line 2" "Works as expected" 229 | 230 | let asParamArray = 231 | IndentedStringBuilder().AppendJoin("|", "Line 1", "Line 2").ToString() 232 | 233 | Expect.equal asParamArray "Line 1|Line 2" "Works as expected" 234 | 235 | ] 236 | 237 | [] 238 | let stringBufferTests = 239 | testList "StringBuffer Tests" [ 240 | testCase "Two lines" 241 | <| fun _ -> 242 | let subject = 243 | stringBuffer { 244 | "Line 1" 245 | "Line 2" 246 | } 247 | 248 | let expected = 249 | "Line 1" 250 | + Environment.NewLine 251 | + "Line 2" 252 | + Environment.NewLine 253 | 254 | Expect.equal subject expected "Works as expected" 255 | testCase "Three lines, last one indented" 256 | <| fun _ -> 257 | let subject = 258 | stringBuffer { 259 | "Line 1" 260 | "Line 2" 261 | indent { "Line 3" } 262 | } 263 | 264 | let expected = 265 | "Line 1" 266 | + Environment.NewLine 267 | + "Line 2" 268 | + Environment.NewLine 269 | + " Line 3" 270 | + Environment.NewLine 271 | 272 | Expect.equal subject expected "Works as expected" 273 | testCase "Three lines, middle one indented" 274 | <| fun _ -> 275 | let subject = 276 | stringBuffer { 277 | "Line 1" 278 | indent { "Line 2" } 279 | "Line 3" 280 | } 281 | 282 | let expected = 283 | "Line 1" 284 | + Environment.NewLine 285 | + " Line 2" 286 | + Environment.NewLine 287 | + "Line 3" 288 | + Environment.NewLine 289 | 290 | Expect.equal subject expected "Works as expected" 291 | 292 | testCase "Different types work as expected" 293 | <| fun _ -> 294 | 295 | let string = "String" 296 | 297 | let stringSeq = 298 | seq { 299 | "Seq1" 300 | "Seq2" 301 | } 302 | 303 | let char = 'c' 304 | 305 | let charSeq = 306 | seq { 307 | 'd' 308 | 'e' 309 | } 310 | 311 | let subject = 312 | stringBuffer { 313 | string 314 | stringSeq 315 | char 316 | charSeq 317 | } 318 | 319 | let expected = 320 | "String" 321 | + Environment.NewLine 322 | + "Seq1" 323 | + Environment.NewLine 324 | + "Seq2" 325 | + Environment.NewLine 326 | + "c" 327 | + Environment.NewLine 328 | + "d" 329 | + Environment.NewLine 330 | + "e" 331 | + Environment.NewLine 332 | 333 | Expect.equal subject expected "Works as expected" 334 | 335 | testCase "Writes namespaces as expected" 336 | <| fun _ -> 337 | let namespaces = [ 338 | "Some.Namespace" 339 | "Another.Namespace" 340 | ] 341 | 342 | let subject = stringBuffer { writeNamespaces namespaces } 343 | 344 | let expected = 345 | "open Some.Namespace" 346 | + Environment.NewLine 347 | + "open Another.Namespace" 348 | + Environment.NewLine 349 | 350 | Expect.equal subject expected "Works as expected" 351 | 352 | testCase "String seq works as expected" 353 | <| fun _ -> 354 | let strings = 355 | seq { 356 | "Line 1" 357 | "Line 2" 358 | } 359 | 360 | let subject = stringBuffer { strings } 361 | 362 | let expected = 363 | "Line 1" 364 | + Environment.NewLine 365 | + "Line 2" 366 | + Environment.NewLine 367 | 368 | Expect.equal subject expected "Works as expected" 369 | 370 | testCase "char seq works as expected" 371 | <| fun _ -> 372 | let chars = 373 | seq { 374 | 'a' 375 | 'b' 376 | } 377 | 378 | let subject = stringBuffer { chars } 379 | 380 | let expected = 381 | "a" 382 | + Environment.NewLine 383 | + "b" 384 | + Environment.NewLine 385 | 386 | Expect.equal subject expected "Works as expected" 387 | 388 | testCase "String option works as expected" 389 | <| fun _ -> 390 | 391 | let subject = 392 | stringBuffer { 393 | Some "Line 1" 394 | None 395 | Some "Line 2" 396 | } 397 | 398 | let expected = 399 | "Line 1" 400 | + Environment.NewLine 401 | + "Line 2" 402 | + Environment.NewLine 403 | 404 | Expect.equal subject expected "Works as expected" 405 | 406 | testCase "String option seq works as expected" 407 | <| fun _ -> 408 | 409 | let s = 410 | seq { 411 | Some "Line 1" 412 | None 413 | Some "Line 2" 414 | } 415 | 416 | let subject = stringBuffer { s } 417 | 418 | let expected = 419 | "Line 1" 420 | + Environment.NewLine 421 | + "Line 2" 422 | + Environment.NewLine 423 | 424 | Expect.equal subject expected "Works as expected" 425 | 426 | testCase "For works as expected" 427 | <| fun _ -> 428 | let s = seq { 1..3 } 429 | 430 | let subject = 431 | stringBuffer { 432 | for i in s do 433 | $"Line %d{i}" 434 | } 435 | 436 | let expected = 437 | "Line 1" 438 | + Environment.NewLine 439 | + "Line 2" 440 | + Environment.NewLine 441 | + "Line 3" 442 | + Environment.NewLine 443 | 444 | Expect.equal subject expected "Works as expected" 445 | 446 | testCase "While works as expected" 447 | <| fun _ -> 448 | 449 | let subject = 450 | stringBuffer { 451 | let mutable i = 0 452 | 453 | while i < 3 do 454 | $"Line %d{i}" 455 | i <- i + 1 456 | } 457 | 458 | let expected = 459 | "Line 0" 460 | + Environment.NewLine 461 | + "Line 1" 462 | + Environment.NewLine 463 | + "Line 2" 464 | + Environment.NewLine 465 | 466 | Expect.equal subject expected "Works as expected" 467 | 468 | testCase "YieldFrom works as expected" 469 | <| fun _ -> 470 | let s = 471 | fun (sb: IndentedStringBuilder) -> 472 | sb.AppendLine "1" 473 | |> ignore 474 | 475 | sb.AppendLine "2" 476 | |> ignore 477 | 478 | sb.AppendLine "3" 479 | |> ignore 480 | 481 | let subject = stringBuffer { yield! s } 482 | 483 | let expected = 484 | "1" 485 | + Environment.NewLine 486 | + "2" 487 | + Environment.NewLine 488 | + "3" 489 | + Environment.NewLine 490 | 491 | Expect.equal subject expected "Works as expected" 492 | 493 | testCase "Using Seq.map works as expected" 494 | <| fun _ -> 495 | 496 | let s = 497 | seq { 498 | "Line 1" 499 | "Line 2" 500 | } 501 | 502 | let subject = 503 | stringBuffer { 504 | s 505 | |> Seq.map (fun s -> 506 | s 507 | + " mapped" 508 | ) 509 | } 510 | 511 | let expected = 512 | "Line 1 mapped" 513 | + Environment.NewLine 514 | + "Line 2 mapped" 515 | + Environment.NewLine 516 | 517 | Expect.equal subject expected "Works as expected" 518 | 519 | testCase "Using string seq in indent works as expected" 520 | <| fun _ -> 521 | let lines = 522 | seq { 523 | 1 524 | 2 525 | } 526 | 527 | let subject = 528 | stringBuffer { 529 | "root" 530 | 531 | indent { 532 | lines 533 | |> Seq.map (fun l -> $"Line {l}") 534 | } 535 | } 536 | 537 | let expected = 538 | "root" 539 | + Environment.NewLine 540 | + " Line 1" 541 | + Environment.NewLine 542 | + " Line 2" 543 | + Environment.NewLine 544 | 545 | Expect.equal subject expected "Works as expected" 546 | 547 | ] 548 | -------------------------------------------------------------------------------- /tests/StringBuffer.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | Microsoft.NET.Test.Sdk 4 | YoloDev.Expecto.TestSdk 5 | altcover 6 | --------------------------------------------------------------------------------