diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9e89cf8..388d50e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "fantomas" ] + }, + "fsdocs-tool": { + "version": "19.0.0", + "commands": [ + "fsdocs" + ] } } } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a3dd1e..0cb5752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,11 @@ on: branches: - master +permissions: + contents: read + pages: write + id-token: write + jobs: ci: @@ -21,3 +26,18 @@ jobs: - name: Build run: dotnet fsi build.fsx + + - name: Upload documentation + if: github.ref == 'refs/heads/master' + uses: actions/upload-pages-artifact@v1 + with: + path: ./output + + deploy: + runs-on: ubuntu-latest + needs: ci + if: github.ref == 'refs/heads/master' + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore index 1164358..d16ded4 100644 --- a/.gitignore +++ b/.gitignore @@ -549,4 +549,9 @@ FodyWeavers.xsd ### VisualStudio Patch ### # Additional files built by Visual Studio -# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,macos,windows,rider \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,macos,windows,rider + +# fsdocs +tmp +.fsdocs +output \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 036e5be..bbefdef 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,12 @@ $(OtherFlags) --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen + + https://github.com/ionide/FSharp.Analyzers.SDK/blob/master/LICENSE.md + https://github.com/ionide/FSharp.Analyzers.SDK/blob/master/CHANGELOG.md + https://github.com/ionide/FSharp.Analyzers.SDK + + diff --git a/build.fsx b/build.fsx index 5c00849..9428cb2 100644 --- a/build.fsx +++ b/build.fsx @@ -20,6 +20,9 @@ pipeline "Build" { run "dotnet run --project src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose" } + stage "docs" { + run "dotnet fsdocs build --properties Configuration=Release --eval --nodefaultcontent --clean --strict" + } runIfOnlySpecified false } @@ -29,4 +32,11 @@ pipeline "ReleaseBuild" { runIfOnlySpecified true } +pipeline "Docs" { + restoreStage + buildStage + stage "fsdocs" { run "dotnet fsdocs watch --properties Configuration=Release --eval" } + runIfOnlySpecified true +} + tryPrintPipelineCommandHelp () diff --git a/docs/_template.html b/docs/_template.html new file mode 100644 index 0000000..8eb6949 --- /dev/null +++ b/docs/_template.html @@ -0,0 +1,94 @@ + + + + + + + + {{fsdocs-page-title}} | {{fsdocs-collection-name}} + + + + + + + {{fsdocs-watch-script}} + + + + + + + + + + FSharp.Analyzers.SDK + + + Search the docs... + + + + + + + + + + + + + + + + + {{fsdocs-content}} + {{fsdocs-tooltips}} + + + + + + + \ No newline at end of file diff --git a/docs/content/Getting Started.fsx b/docs/content/Getting Started.fsx new file mode 100644 index 0000000..9ed15cc --- /dev/null +++ b/docs/content/Getting Started.fsx @@ -0,0 +1,74 @@ +(** +--- +category: end-users +categoryindex: 1 +index: 1 +--- + +# Getting started + +## Create project + +Create a new class library targeting `net6.0` + +```shell +dotnet new classlib -lang F# -f net6.0 -n OptionValueAnalyzer +``` + +Note that the assembly name needs to contain `Analyzer` in the name in order for it to be picked up. + +Add a reference to the analyzers SDK: + +```shell +dotnet add package FSharp.Analyzers.SDK +``` + +```shell +paket add FSharp.Analyzers.SDK +``` + +## First analyzer + +An [Analyzer](../reference/fsharp-analyzers-sdk-analyzer.html) is a function that takes a `Context` and returns a list of `Message`. +*) + +(*** hide ***) +#r "../../src/FSharp.Analyzers.Cli/bin/Release/net6.0/FSharp.Analyzers.SDK.dll" +#r "../../src/FSharp.Analyzers.Cli/bin/Release/net6.0/FSharp.Compiler.Service.dll" +(** *) + +module OptionAnalyzer = + + open FSharp.Analyzers.SDK + + // This attribute is required! + [] + let optionValueAnalyzer: Analyzer = + fun (context: Context) -> + // inspect context to determine the error/warning messages + // A potential implementation might traverse the untyped syntax tree + // to find any references of `Option.Value` + [ + { + Type = "Option.Value analyzer" + Message = "Option.Value shouldn't be used" + Code = "OV001" + Severity = Warning + Range = FSharp.Compiler.Text.Range.Zero + Fixes = [] + } + ] + +(** +## Running your first analyzer + +After building your project you can run your analyzer on a project of your choosing using the [fsharp-analyzers](https://www.nuget.org/packages/fsharp-analyzers) tool. + +```shell +dotnet tool install --global fsharp-analyzers +``` + +```shell +fsharp-analyzers --project YourProject.fsproj --analyzers-path ./OptionAnalyzer/bin/Release --verbose +``` +*) diff --git a/docs/copyCommand.js b/docs/copyCommand.js new file mode 100644 index 0000000..3474b46 --- /dev/null +++ b/docs/copyCommand.js @@ -0,0 +1,51 @@ +import {LitElement, html, css} from 'https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js'; +import copy from 'https://esm.sh/copy-to-clipboard@3.3.3'; + +export class CopyCommandButton extends LitElement { + static properties = { + content: {type: String, attribute: true}, + clicked: {type: Boolean, state: true} + } + + constructor() { + super(); + this.clicked = false; + } + + static styles = css` + iconify-icon { + cursor: pointer; + } + ` + + onClick() { + copy(this.content); + this.clicked = true; + setTimeout( () => { + this.clicked = false; + }, 500); + } + + render() { + return this.clicked ? html` + ` : html` + `; + } +} + +customElements.define('copy-icon', CopyCommandButton); + +const codeToCopy = [...document.querySelectorAll("code[lang=shell],code[lang=bash]")]; +codeToCopy.forEach(code => { + const copyIcon = document.createElement("copy-icon"); + copyIcon.setAttribute("content", code.textContent); + const wrapInTd = element => { const td = document.createElement("td"); td.append(element); return td } + const row = code.parentElement.parentElement.parentElement; + row.append(wrapInTd(copyIcon)); + + const terminalIcon = document.createElement("iconify-icon"); + terminalIcon.setAttribute("icon", "ph:terminal-bold"); + terminalIcon.setAttribute("width", "16"); + terminalIcon.setAttribute("height", "16"); + row.prepend(wrapInTd(terminalIcon)); +}); \ No newline at end of file diff --git a/docs/images/favicon.png b/docs/images/favicon.png new file mode 100644 index 0000000..babbee5 Binary files /dev/null and b/docs/images/favicon.png differ diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..babbee5 Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4906686 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,139 @@ +# Ionide FSharp.Analyzers.SDK + +Library used for building custom analyzers for FSAC / F# editors. + +F# analyzers are live, real-time, project based plugins that enables to diagnose source code and surface custom errors, warnings and code fixes into editor. Read more about analyzers here - https://medium.com/lambda-factory/introducing-f-analyzers-772487889429 + +## How to build + +1. Install the .NET SDK version specified in `global.json` +2. `dotnet tool restore` +2. Open and build in your favorite IDE, or use `dotnet build` + +## How to run sample +1. `dotnet build -c Release` +2. Run + +```shell +dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose +``` + + +You can also set up a run configuration of FSharp.Analyzers.Cli in your favorite IDE using similar arguments. This also allows you to debug FSharp.Analyzers.Cli. + +## Writing Analyzers + +Analyzers that are consumed by this SDK and from Ionide are simply .NET core class libraries. These class libraries expose a *value* of type `Analyzer` which is effectively a function that has input of type `Context` and returns a list of `Message` records: +```fsharp +module BadCodeAnalyzer + +open FSharp.Analyzers.SDK + +[] +let badCodeAnalyzer : Analyzer = + fun (context: Context) -> + // inspect context to determine the error/warning messages + [ ] +``` +Notice how we expose the function `BadCodeAnalyzer.badCodeAnalyzer` with an attribute `[]` that allows the SDK to detect the function. The input `Context` is a record that contains information about a single F# file such as the typed AST, the AST, the file content, the file name and more. The SDK runs this function against all files of a project during editing. The output messages that come out of the function are eventually used by Ionide to highlight the inspected code as a warning or error depending on the `Severity` level of each message. + +Analyzers can also be named which allows for better logging if something went wrong while using the SDK from Ionide: +```fs +[] +let badCodeAnalyzer : Analyzer = + fun (context: Context) -> + // inspect context to determine the error/warning messages + [ ] +``` +### Analyzer Requirements + +Analyzers are .NET core class libraries and they are distributed as such. However, since the SDK relies on dynamically loading the analyzers during runtime, there are some requirements to get them to work properly: +- The analyzer class library has to target the `net6.0` framework +- The analyzer has to reference the latest `FSharp.Analyzers.SDK` (at least the version used by FsAutoComplete which is subsequently used by Ionide) + +### Packaging and Distribution + +Since analyzers are just .NET core libraries, you can distribute them to the nuget registry just like you would with a normal .NET package. Simply run `dotnet pack --configuration Release` against the analyzer project to get a nuget package and publish it with + +```shell +dotnet nuget push {NugetPackageFullPath} -s nuget.org -k {NugetApiKey} +``` + +However, the story is different and slightly more complicated when your analyzer package has third-party dependencies also coming from nuget. Since the SDK dynamically loads the package assemblies (`.dll` files), the assemblies of the dependencies has be there *next* to the main assembly of the analyzer. Using `dotnet pack` will **not** include these dependencies into the output Nuget package. More specifically, the `./lib/net6.0` directory of the nuget package must have all the required assemblies, also those from third-party packages. In order to package the analyzer properly with all the assemblies, you need to take the output you get from running: +```shell +dotnet publish --configuration Release --framework net6.0 +``` +against the analyzer project and put every file from that output into the `./lib/net6.0` directory of the nuget package. This requires some manual work by unzipping the nuget package first (because it is just an archive), modifying the directories then zipping the package again. It can be done using a FAKE build target to automate the work: +```fs +// make ZipFile available +#r "System.IO.Compression.FileSystem.dll" + +let releaseNotes = ReleaseNotes.load "RELEASE_NOTES.md" + +Target.create "PackAnalyzer" (fun _ -> + let analyzerProject = "src" > "BadCodeAnalyzer" + let args = + [ + "pack" + "--configuration Release" + sprintf "/p:PackageVersion=%s" releaseNotes.NugetVersion + sprintf "/p:PackageReleaseNotes=\"%s\"" (String.concat "\n" releaseNotes.Notes) + sprintf "--output %s" (__SOURCE_DIRECTORY__ > "dist") + ] + + // create initial nuget package + let exitCode = Shell.Exec("dotnet", String.concat " " args, analyzerProject) + if exitCode <> 0 then + failwith "dotnet pack failed" + else + match Shell.Exec("dotnet", "publish --configuration Release --framework net6.0", analyzerProject) with + | 0 -> + let nupkg = + System.IO.Directory.GetFiles(__SOURCE_DIRECTORY__ > "dist") + |> Seq.head + |> IO.Path.GetFullPath + + let nugetParent = DirectoryInfo(nupkg).Parent.FullName + let nugetFileName = IO.Path.GetFileNameWithoutExtension(nupkg) + + let publishPath = analyzerProject > "bin" > "Release" > "net6.0" > "publish" + // Unzip the nuget + ZipFile.ExtractToDirectory(nupkg, nugetParent > nugetFileName) + // delete the initial nuget package + File.Delete nupkg + // remove stuff from ./lib/net6.0 + Shell.deleteDir (nugetParent > nugetFileName > "lib" > "net6.0") + // move the output of publish folder into the ./lib/net6.0 directory + Shell.copyDir (nugetParent > nugetFileName > "lib" > "net6.0") publishPath (fun _ -> true) + // re-create the nuget package + ZipFile.CreateFromDirectory(nugetParent > nugetFileName, nupkg) + // delete intermediate directory + Shell.deleteDir(nugetParent > nugetFileName) + | _ -> + failwith "dotnet publish failed" +) +``` + +## How to contribute + +*Imposter syndrome disclaimer*: I want your help. No really, I do. + +There might be a little voice inside that tells you you're not ready; that you need to do one more tutorial, or learn another framework, or write a few more blog posts before you can help me with this project. + +I assure you, that's not the case. + +This project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/CONTRIBUTING.md). + +The contribution guidelines outline the process that you'll need to follow to get a patch merged. By making expectations and process explicit, I hope it will make it easier for you to contribute. + +And you don't just have to write code. You can help out by writing documentation, tests, or even by giving feedback about this work. (And yes, that includes giving feedback about the contribution guidelines.) + +Thank you for contributing! + + +## Contributing and copyright + +The project is hosted on [GitHub](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK) where you can [report issues](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/issues), fork +the project and submit pull requests. + +The library is available under [MIT license](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..e2171de --- /dev/null +++ b/docs/style.css @@ -0,0 +1,723 @@ +:root { + --monospace-font: "Fira Code", monospace; + --system-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + + --unit: 0.25rem; + --unit-2: calc(2 * var(--unit)); + --unit-3: calc(3 * var(--unit)); + --unit-4: calc(4 * var(--unit)); + --unit-5: calc(5 * var(--unit)); + --unit-6: calc(6 * var(--unit)); + --unit-7: calc(7 * var(--unit)); + --unit-8: calc(8 * var(--unit)); + --unit-9: calc(9 * var(--unit)); + --unit-10: calc(10 * var(--unit)); + --unit-11: calc(11 * var(--unit)); + --unit-12: calc(12 * var(--unit)); + --radius: 6px; + --icon-size: 24px; + + --container-sm: 768px; + --container-md: 960px; + --container-lg: 1024px; + --container-xl: 1200px; + --container-xxl: 1400px; + --aside-width: 200px; + + /* light theme */ + --primary: #1e8bc3; + --background: #f5f5f6; + --text-color: #020202; + --text-hover: #282828; + --code-background: #ffffff; + --code-color: #1B6504; + --header-border: #e8ecf1; + --menu-icon-hover: #F7F7F7; + --menu-icon-hover-background: #bdc3c7; + --doc-tip-background: #F7F7F7; + --link-color: #4871f7; + --search-background: rgb(229, 231, 235); + --nav-category: rgb(156, 163, 175); + + --code-strings-color: #0093A1; + --code-printf-color: #6B2FBA; + --code-escaped-color: #EA8675; + --code-identifiers-color: #6B2FBA; + --code-module-color: #009999; + --code-reference-color: #4974D1; + --code-value-color: #1B6600; + --code-interface-color: #43AEC6; + --code-typearg-color: #43AEC6; + --code-disposable-color: #43AEC6; + --code-property-color: #43AEC6; + --code-punctuation-color: #43AEC6; + --code-punctuation2-color: var(--text-color); + --code-function-color: #6B2FBA; + --code-function2-color: #990000; + --code-activepattern-color: #4ec9b0; + --code-unioncase-color: #4ec9b0; + --code-enumeration-color: #8C6C41; + --code-keywords-color: #0F54D6; + --code-comment-color: #707070; + --code-operators-color: #0F54D6; + --code-numbers-color: #009999; + --code-linenumbers-color: #80b0b0; + --code-mutable-color: #1b6600; + --code-inactive-color: #808080; + --code-preprocessor-color: #af75c1; + --code-fsioutput-color: #808080; + --code-tooltip-color: #d1d1d1; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; + font-family: var(--system-font); + background-color: var(--background); + color: var(--text-color); +} + +header { + display: flex; + align-content: center; + justify-content: space-between; + padding: var(--unit-3); + border-bottom: 1px solid var(--header-border); + + & .start { + display: flex; + align-items: center; + + #search-input { + display: none; + } + } + + & .end { + display: flex; + align-items: center; + + & a { + display: none; + } + } + + & iconify-icon { + padding: 0 var(--unit); + box-sizing: border-box; + cursor: pointer; + } + + & img { + width: 32px; + height: 32px; + margin: 0 var(--unit-2); + } + + & strong { + font-size: var(--unit-4); + line-height: var(--unit-8); + } +} + +.menu-icon { + border: 1px solid var(--header-border); + + &:hover { + cursor: pointer; + background-color: var(--menu-icon-hover-background); + color: var(--menu-icon-hover); + } + + &:active { + color: var(--text-color); + } +} + +aside { + position: fixed; + z-index: 10; + top: -300px; + visibility: hidden; + opacity: 0; + left: 0; + background-color: var(--background); + transition: top 200ms, visibility 200ms; + width: 100%; + + &.open { + top: 0; + visibility: visible; + opacity: 1; + } + + .close { + padding: var(--unit-3); + border-bottom: 1px solid var(--header-border); + + > iconify-icon { + padding: var(--unit); + box-sizing: border-box; + border: 1px solid var(--header-border); + + &:hover { + cursor: pointer; + } + } + } + + .content { + padding: var(--unit-3); + + .nav-header { + text-transform: uppercase; + list-style: none; + font-size: var(--unit-3); + color: var(--nav-category); + } + + .nav-header + .nav-item { + margin-bottom: var(--unit-6); + } + + .nav-item { + list-style: none; + padding-left: var(--unit-4); + margin: var(--unit-2) 0; + border-left: 1px solid var(--header-border); + transition: border-color 200ms; + + &:hover { + border-color: var(--text-color); + } + + & a { + text-decoration: none; + color: var(--text-color); + font-size: var(--unit-4); + } + } + } +} + +main { + padding: var(--unit-3) var(--unit-6); + + & nav { + display: none; + } +} + +@media screen and (min-width: 768px) { + body { + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr; + grid-gap: 0; + } + + header { + grid-column-start: 1; + grid-column-end: 3; + grid-row: 1; + + .start { + > strong { + margin: 0 var(--unit-2); + } + + #search-input { + display: flex; + align-items: center; + margin: 0 var(--unit-2); + background-color: var(--search-background); + border-radius: var(--radius); + padding: var(--unit); + cursor: pointer; + + &:hover { + opacity: 0.8; + } + + & iconify-icon { + opacity: 0.5; + display: block; + } + + & iconify-icon:last-child { + border: 1px dashed var(--text-hover); + border-radius: var(--radius); + padding: var(--unit) calc(var(--unit) / 2); + } + + & em { + display: block; + font-size: var(--unit-4); + line-height: 2; + opacity: 0.7; + margin-right: var(--unit-4); + } + } + } + + .end { + & a { + display: block; + color: var(--text-color); + height: var(--icon-size); + } + + .search { + display: none; + } + } + } + + aside { + position: initial; + visibility: visible; + opacity: 1; + height: 100%; + grid-column-start: 1; + grid-column-end: 1; + grid-row: 2; + width: var(--aside-width); + border-right: 1px solid var(--header-border); + + .close { + display: none; + } + } + + main { + grid-row: 2; + grid-column-start: 2; + grid-column-end: 3; + overflow-x: auto; + margin: 0; + } + + .menu-icon { + display: none; + } +} + +@media screen and (min-width: 768px) { + #fsdocs-content { + max-width: calc(var(--container-sm) - var(--aside-width) - var(--unit-8)); + min-width: calc(var(--container-sm) - var(--aside-width) - var(--unit-8)); + margin: 0 auto; + } +} + +@media screen and (min-width: 960px) { + #fsdocs-content { + min-width: calc(var(--container-md) - var(--aside-width) - var(--unit-8)); + max-width: calc(var(--container-md) - var(--aside-width) - var(--unit-8)); + } +} + +@media screen and (min-width: 1024px) { + #fsdocs-content { + min-width: calc(var(--container-lg) - var(--aside-width) - var(--unit-8)); + max-width: calc(var(--container-lg) - var(--aside-width) - var(--unit-8)); + } +} + +@media screen and (min-width: 1200px) { + #fsdocs-content { + min-width: calc(var(--container-xl) - var(--aside-width) - var(--unit-8)); + max-width: calc(var(--container-xl) - var(--aside-width) - var(--unit-8)); + } +} + +@media screen and (min-width: 1400px) { + #fsdocs-content { + max-width: calc(var(--container-xxl) - var(--aside-width) - var(--unit-8)); + } +} + +/* Headings */ + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + color: var(--primary); + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; +} + +h1 { + font-size: var(--unit-8); +} + +h2 { + font-size: var(--unit-6); +} + +h3 { + font-size: var(--unit-5); +} + +h4 { + font-size: var(--unit-4); +} + +h5 { + font-size: var(--unit-3); +} + +h6 { + font-size: var(--unit-2); +} + +/* Common items */ +a { + color: var(--link-color); +} + +/* Code snippets */ + +/* reset browser style */ +pre { + margin: 0; + padding: 0; +} + +code, table.pre, pre { + background-color: var(--code-background); + color: var(--code-color); + font-family: var(--monospace-font); +} + +table.pre, pre.fssnip.highlighted { + margin-bottom: var(--unit-3); +} + +table.pre, pre.fssnip { + padding: var(--unit) var(--unit-2); +} + +p > code, li > code { + margin: 0 var(--unit); + padding: var(--unit); +} + +table.pre, code { + overflow-x: auto; + max-width: calc(100vw - var(--unit-12)); + box-sizing: border-box; + border-radius: var(--radius); +} + +table.pre, pre > code { + display: block; +} + +pre.fssnip > code { + max-width: initial; + margin-bottom: initial; +} + +/* copy icons in the shell commands */ +table.pre td iconify-icon { + margin-top: 4px; +} + +@media screen and (min-width: 768px) { + table.pre, code { + max-width: var(--container-sm); + } +} + +@media screen and (min-width: 960px) { + table.pre, code { + max-width: var(--container-md); + } +} + +@media screen and (min-width: 1024px) { + table.pre, code { + max-width: var(--container-lg); + } +} + +@media screen and (min-width: 1200px) { + table.pre, code { + max-width: var(--container-xl); + } +} + +@media screen and (min-width: 1400px) { + table.pre, code { + max-width: var(--container-xxl); + } +} + +/* Code coloring */ +.param-name, +.return-name, +.param { + font-weight: 900; + font-size: 0.85rem; + font-family: 'Roboto Mono', monospace; +} + +.fssnip { + /* strings --- and stlyes for other string related formats */ + + & span.s { + color: var(--code-strings-color); + } + + /* printf formatters */ + + & span.pf { + color: var(--code-printf-color); + } + + /* escaped chars */ + + & span.e { + color: var(--code-escaped-color); + } + + /* identifiers --- and styles for more specific identifier types */ + + & span.id { + color: var(--code-identifiers-color);; + } + + /* module */ + + & span.m { + color: var(--code-module-color); + } + + /* reference type */ + + & span.rt { + color: var(--code-reference-color); + } + + /* value type */ + + & span.vt { + color: var(--code-value-color); + } + + /* interface */ + + & span.if { + color: var(--code-interface-color); + } + + /* type argument */ + + & span.ta { + color: var(--code-typearg-color); + } + + /* disposable */ + + & span.d { + color: var(--code-disposable-color); + } + + /* property */ + + & span.prop { + color: var(--code-property-color); + } + + /* punctuation */ + + & span.p { + color: var(--code-punctuation-color); + } + + & span.pn { + color: var(--code-punctuation2-color); + } + + /* function */ + + & span.f { + color: var(--code-function-color); + } + + & span.fn { + color: var(--code-function2-color); + } + + /* active pattern */ + + & span.pat { + color: var(--code-activepattern-color); + } + + /* union case */ + + & span.u { + color: var(--code-unioncase-color); + } + + /* enumeration */ + + & span.e { + color: var(--code-enumeration-color); + } + + /* keywords */ + + & span.k { + color: var(--code-keywords-color); + } + + /* comment */ + + & span.c { + color: var(--code-comment-color); + font-weight: 400; + font-style: italic; + } + + /* operators */ + + & span.o { + color: var(--code-operators-color); + } + + /* numbers */ + + & span.n { + color: var(--code-numbers-color); + } + + /* line number */ + + & span.l { + color: var(--code-linenumbers-color); + } + + /* mutable var or ref cell */ + + & span.v { + color: var(--code-mutable-color); + font-weight: bold; + } + + /* inactive code */ + + & span.inactive { + color: var(--code-inactive-color); + } + + /* preprocessor */ + + & span.prep { + color: var(--code-preprocessor-color); + } + + /* fsi output */ + + & span.fsi { + color: var(--code-fsioutput-color); + } +} + + +/* tooltips */ + +div.fsdocs-tip { + display: none; + background-color: var(--doc-tip-background); + border-right: var(--radius); + padding: var(--unit-3); + font-family: var(--monospace-font); + color: var(--code-color); + + & code { + color: var(--code-color); + } +} + +span[onmouseout] { + cursor: pointer; +} + +/* dark theme */ + +[data-theme=dark] { + --primary: #81cfe0; + --background: #32283c; + --text-color: #F7F7F7; + --text-hover: #FFF; + --code-background: #3e3e42; + --code-color: #f5f5f6; + --header-border: #9b9b9b; + --doc-tip-background: #2e293a; + --link-color: #c5eff7; + --search-background: #020202; + --nav-category: rgb(207, 211, 215); + + --code-strings-color: #5adfec; + --code-printf-color: #6B2FBA; + --code-escaped-color: #EA8675; + --code-identifiers-color: #d1b3f5; + --code-module-color: #15e1e1; + --code-reference-color: #7a9cee; + --code-value-color: #66d73d; + --code-interface-color: #43AEC6; + --code-typearg-color: #43AEC6; + --code-disposable-color: #43AEC6; + --code-property-color: #43AEC6; + --code-punctuation-color: #43AEC6; + --code-punctuation2-color: var(--text-color); + --code-function-color: #6B2FBA; + --code-function2-color: #da9d9d; + --code-activepattern-color: #4ec9b0; + --code-unioncase-color: #4ec9b0; + --code-enumeration-color: #8C6C41; + --code-keywords-color: #a7c2f8; + --code-comment-color: #dcd8d8; + --code-operators-color: #b4c6ee; + --code-numbers-color: #009999; + --code-linenumbers-color: #80b0b0; + --code-mutable-color: #1b6600; + --code-inactive-color: #808080; + --code-preprocessor-color: #af75c1; + --code-fsioutput-color: #808080; + --code-tooltip-color: #d1d1d1; +} + +/* theme toggling */ + +.themeToggle.dark { + display: block; +} + +.themeToggle.light { + display: none; +} + +[data-theme=dark] { + .themeToggle.light { + display: block; + } + + .themeToggle.dark { + display: none; + } +} + +.themeToggle { + cursor: pointer; + color: var(--text-color); + + &:hover { + color: var(--text-hover); + } +} \ No newline at end of file diff --git a/docs/tooltips.js b/docs/tooltips.js new file mode 100644 index 0000000..6a21c40 --- /dev/null +++ b/docs/tooltips.js @@ -0,0 +1,58 @@ +let currentTip = null; +let currentTipElement = null; + +function hideTip(evt, name, unique) { + const el = document.getElementById(name); + el.style.display = "none"; + currentTip = null; +} + +function findPos(obj) { + // no idea why, but it behaves differently in webbrowser component + if (window.location.search === "?inapp") + return [obj.offsetLeft + 10, obj.offsetTop + 30]; + + let curleft = 0; + let curtop = obj.offsetHeight; + while (obj) { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + obj = obj.offsetParent; + } + return [curleft, curtop]; +} + +function hideUsingEsc(e) { + hideTip(e, currentTipElement, currentTip); +} + +function showTip(evt, name, unique, owner) { + document.onkeydown = hideUsingEsc; + if (currentTip === unique) return; + currentTip = unique; + currentTipElement = name; + + let pos = findPos(owner ? owner : (evt.srcElement ? evt.srcElement : evt.target)); + const posx = pos[0]; + const posy = pos[1]; + + const el = document.getElementById(name); + el.style.position = "absolute"; + el.style.left = posx + "px"; + el.style.top = posy + "px"; + el.style.display = "block"; +} + +function Clipboard_CopyTo(value) { + const tempInput = document.createElement("input"); + tempInput.value = value; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand("copy"); + document.body.removeChild(tempInput); +} + +window.showTip = showTip; +window.hideTip = hideTip; +// Used by API documentation +window.Clipboard_CopyTo = Clipboard_CopyTo; \ No newline at end of file