From 4f9afb0a8579fed7f7c059ceb89336e930454aba Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Sun, 23 Jun 2024 13:48:17 -0500 Subject: [PATCH 01/21] Update fable metadata and standalone packages --- package-lock.json | 24 ++++++++++++++++-------- package.json | 6 +++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index caba1e2..ce7bae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,8 +5,8 @@ "packages": { "": { "dependencies": { - "@fable-org/fable-metadata": "^1.0.0", - "@fable-org/fable-standalone": "^1.1.0", + "@fable-org/fable-metadata": "^1.1.0", + "@fable-org/fable-standalone": "^1.8.1", "@monaco-editor/react": "^4.6.0", "monaco-editor": "^0.46.0", "react": "^18.2.0", @@ -741,14 +741,17 @@ } }, "node_modules/@fable-org/fable-metadata": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@fable-org/fable-metadata/-/fable-metadata-1.0.0.tgz", - "integrity": "sha512-O6xT43XenR1SjtaHtEFDL3DLPn8/JjVWyOYG1BFNBM30sF/mknIuIer1oy+iwNG2MJQiu8BtLrB0OMkGnt/2JQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fable-org/fable-metadata/-/fable-metadata-1.1.0.tgz", + "integrity": "sha512-ccDGXMAxnd+zmtjl7r/oxTr9g/xaO1o1PsCCWZYMyFa9lXgHMd8aXDGm4F5lIdWvF550rEVA0BC6rES50OWE4A==", + "dependencies": { + "dirname-filename-esm": "^1.1.1" + } }, "node_modules/@fable-org/fable-standalone": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fable-org/fable-standalone/-/fable-standalone-1.1.0.tgz", - "integrity": "sha512-/vasnCXt5DOKFhEa1DIhzwdLCpwXsz/VzTH8EudbBK9QbV5+bXVBc2LzWy9GV7k2eHbyHitB/xzx0OVdoKYYPg==" + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@fable-org/fable-standalone/-/fable-standalone-1.8.1.tgz", + "integrity": "sha512-+HTkVzl/+1NmypPSgYouufQJa9BpptCDGLYgJQgruENYpprfGlawa62gC97HohxsWeEZyRn9wok6L4V72zrBjw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", @@ -1315,6 +1318,11 @@ "node": ">=0.3.1" } }, + "node_modules/dirname-filename-esm": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/dirname-filename-esm/-/dirname-filename-esm-1.1.1.tgz", + "integrity": "sha512-BWBkv157Cf/z7Hjod2v2JS7vyC36Dk1QQolAtuLjpl8RBlv7Z92X7+Ufc2cjfR/B3iJUiK0QmGZkgtsmmLz0Tw==" + }, "node_modules/electron-to-chromium": { "version": "1.4.678", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.678.tgz", diff --git a/package.json b/package.json index ef7c383..4597aec 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "bundle": "dotnet fable src/App --outDir dist/ --run vite build" }, "dependencies": { - "@fable-org/fable-metadata": "^1.0.0", - "@fable-org/fable-standalone": "^1.1.0", + "@fable-org/fable-metadata": "^1.1.0", + "@fable-org/fable-standalone": "^1.8.1", "@monaco-editor/react": "^4.6.0", "monaco-editor": "^0.46.0", "react": "^18.2.0", @@ -20,4 +20,4 @@ "@vitejs/plugin-react": "^4.2.1", "vite": "^5.0.12" } -} \ No newline at end of file +} From 34c9b5503de5905691408458084200507d299b55 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:10:16 -0500 Subject: [PATCH 02/21] Update README.md --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54eb47e..5b4ecd7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # F# Tour + A guided interactive tour of the F# language. ## Development + +### Prerequisites +* .NET 8.0 +* NodeJS (npm) + +### Running the Application ``` dotnet fsi build.fsx -- -p Setup npm start ``` ## Inspirations -- [Fable REPL](https://github.com/fable-compiler/repl) -- [Gleam Language Tour](https://tour.gleam.run) \ No newline at end of file +- Originally based on the [Gleam Language Tour](https://tour.gleam.run) and the [Kotlin Tour](https://kotlinlang.org/docs/kotlin-tour-welcome.html) +- The core logic for compiling F# in the browser is based on the work done in the [Fable REPL](https://github.com/fable-compiler/repl). \ No newline at end of file From e3a741f559300e3c6bee3428c66beed571013e8a Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:11:04 -0500 Subject: [PATCH 03/21] Don't load F# files in the documentation loader --- src/App/Documentation.fs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/App/Documentation.fs b/src/App/Documentation.fs index 9c0ace6..e9db6cf 100644 --- a/src/App/Documentation.fs +++ b/src/App/Documentation.fs @@ -7,7 +7,6 @@ open Thoth.Json type Page = { Title: string Route: string list - FSharpCode: string MarkdownDocumentation: string } @@ -92,13 +91,11 @@ let private fetchAsString path = let private loadPageFromJson (categoryJson: Json.CategoryJson) (pageJson: Json.PageJson) = promise { - let! fsharpCode = fetchAsString (documentationPath pageJson.FSharpFile) let! markdownDoc = fetchAsString (documentationPath pageJson.MarkdownFile) return { Title = pageJson.Title Route = [ categoryJson.RouteSegment; pageJson.RouteSegment ] - FSharpCode = fsharpCode MarkdownDocumentation = markdownDoc } } From 59e6c34c336e772e258dcdc0c66f14b4f6435d25 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:11:54 -0500 Subject: [PATCH 04/21] Update iframe command to only parse messages from a specific iframe. --- src/App/Iframe.fs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/App/Iframe.fs b/src/App/Iframe.fs index 0903634..0ec509c 100644 --- a/src/App/Iframe.fs +++ b/src/App/Iframe.fs @@ -13,24 +13,28 @@ type MessageArgs<'msg> = { ConsoleError: string -> 'msg } -let command (args: MessageArgs<'Msg>) = +let command (iframeId: string) (args: MessageArgs<'Msg>) = let handler dispatch = window.addEventListener ( "message", fun ev -> - let iframeMessageDecoder = - Decode.field "type" Decode.string - |> Decode.option - |> Decode.andThen (function - | Some "console_log" -> Decode.field "content" Decode.string |> Decode.map args.ConsoleLog - | Some "console_warn" -> Decode.field "content" Decode.string |> Decode.map args.ConsoleWarn - | Some "console_error" -> Decode.field "content" Decode.string |> Decode.map args.ConsoleError - | _ -> Decode.fail "Invalid message") - - Decode.fromValue "$" iframeMessageDecoder ev?data - |> function - | Error _ -> () - | Ok msg -> dispatch msg + let iframeElement = document.getElementById iframeId + + if ev?source = iframeElement?contentWindow then + let iframeMessageDecoder = + Decode.field "type" Decode.string + |> Decode.option + |> Decode.andThen (function + | Some "console_log" -> Decode.field "content" Decode.string |> Decode.map args.ConsoleLog + | Some "console_warn" -> Decode.field "content" Decode.string |> Decode.map args.ConsoleWarn + | Some "console_error" -> + Decode.field "content" Decode.string |> Decode.map args.ConsoleError + | _ -> Decode.fail "Invalid message") + + Decode.fromValue "$" iframeMessageDecoder ev?data + |> function + | Error _ -> () + | Ok msg -> dispatch msg ) [ handler ] From 7f0c9b64f055138a4016aeb8dacd28de4c2a23f3 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:12:50 -0500 Subject: [PATCH 05/21] Remove react-syntax-highlighter dependency --- package-lock.json | 258 ---------------------------------------------- package.json | 3 +- 2 files changed, 1 insertion(+), 260 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce7bae4..5c08486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.4", - "react-syntax-highlighter": "^15.5.0", "react-toastify": "^9.1.3", "use-sync-external-store": "^1.2.0" }, @@ -312,17 +311,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", @@ -1212,24 +1200,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -1390,26 +1360,6 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "node_modules/fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1451,15 +1401,6 @@ "node": ">=4" } }, - "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/hast-util-whitespace": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", @@ -1469,79 +1410,11 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -1564,24 +1437,6 @@ "node": ">=4" } }, - "node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -1641,27 +1496,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lowlight/node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2210,32 +2044,6 @@ "node": ">=0.10.0" } }, - "node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2270,14 +2078,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2369,29 +2169,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, - "node_modules/react-syntax-highlighter/node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" - } - }, "node_modules/react-toastify": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", @@ -2404,33 +2181,6 @@ "react-dom": ">=16" } }, - "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/remark-parse": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", @@ -2818,14 +2568,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 4597aec..4979093 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.4", - "react-syntax-highlighter": "^15.5.0", "react-toastify": "^9.1.3", "use-sync-external-store": "^1.2.0" }, @@ -20,4 +19,4 @@ "@vitejs/plugin-react": "^4.2.1", "vite": "^5.0.12" } -} +} \ No newline at end of file From ac1d297244795a515d3ce9beb09a43c039a41d10 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:13:50 -0500 Subject: [PATCH 06/21] Rework the docs to use a single editor instance per markdown code block --- src/App/App.fs | 659 +++++++++++++++++++++++-------------------------- 1 file changed, 309 insertions(+), 350 deletions(-) diff --git a/src/App/App.fs b/src/App/App.fs index 11f9333..feeb67c 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -18,64 +18,6 @@ open Feliz.UseMediaQuery importSideEffects "react-toastify/dist/ReactToastify.css" importSideEffects "./monaco-vite.js" -[] -type LogLevel = - | Log - | Warn - | Error - -[] -module LogLevel = - let toCssColor logLevel = - match logLevel with - | LogLevel.Log -> "inherit" - | LogLevel.Warn -> "darkorange" - | LogLevel.Error -> "red" - -type Model = { - Logs: (string * LogLevel) list - FSharpCode: string - CompiledJavaScript: string - Markdown: string - IFrameUrl: string - Worker: ObservableWorker - TableOfContents: Documentation.TableOfContents - CurrentPage: Navigation.Page - DocEntryNavigation: DocEntryNavigation - Editor: Monaco.Editor.IStandaloneCodeEditor - Markers: Monaco.Editor.IMarkerData array - Debouncer: Debouncer.State -} - -type Msg = - | Compile - | SetIFrameUrl of string - | SetFSharpCode of string - | SetMarkdown of string - | AddConsoleLog of LogLevel * string - | Compiled of code: string * language: string * errors: Error array * stats: CompileStats - | FetchedTableOfContents of Documentation.TableOfContents - | FetchTableOfContentsExn of exn - | SetUrl of string list - | CalculateMarkdownAndCodeValues - | CalculateDocEntryNavigation - | SetMarkers of Monaco.Editor.IMarkerData array - | SetEditor of Monaco.Editor.IStandaloneCodeEditor - | DebouncerSelfMsg of Debouncer.SelfMessage - | ParseCode - -[] -type SyntaxHighlighter = - static member inline language(value: string) = Interop.mkAttr "language" value - static member inline style(value: string) = Interop.mkAttr "style" value - static member inline children(value: ReactElement seq) = Interop.mkAttr "children" value - - static member inline highlighter(properties: IReactProperty list) = - Interop.reactApi.createElement ( - import "Prism as ReactSyntaxHighlighter" "react-syntax-highlighter", - createObj !!properties - ) - [] type MonacoEditor = static member inline onChange(f: string -> unit) = Interop.mkAttr "onChange" f @@ -91,17 +33,24 @@ type MonacoEditor = static member inline editor(properties: IReactProperty list) = Interop.reactApi.createElement (import "Editor" "@monaco-editor/react", createObj !!properties) +[] module WebWorker = - let create () = Worker.Create(Constants.worker) + open MonacoEditor.Monaco.Editor - let command (worker: ObservableWorker<_>) = + let create () = Worker.Create Constants.worker + + let command + (setMarkersMsg: IMarkerData array -> 'msg) + (compiledMsg: string * string * Error array * CompileStats -> 'msg) + (worker: ObservableWorker<_>) + = let handler dispatch = worker |> Observable.add (function | Loaded version -> () | LoadFailed -> () - | ParsedCode errors -> errors |> Editor.mapErrorToMarker |> SetMarkers |> dispatch - | CompilationFinished(code, lang, errors, stats) -> dispatch (Compiled(code, lang, errors, stats)) + | ParsedCode errors -> errors |> Editor.mapErrorToMarker |> setMarkersMsg |> dispatch + | CompilationFinished(code, lang, errors, stats) -> dispatch (compiledMsg (code, lang, errors, stats)) | CompilationsFinished(code, lang, errors, stats) -> () | CompilerCrashed msg -> () | FoundTooltip _ -> () @@ -110,294 +59,345 @@ module WebWorker = [ handler ] -let getCurrentPage tableOfContents url = - let pages = Documentation.TableOfContents.allPages tableOfContents - Page.fromUrl pages url - -let init () = - let fsharpOptions = [| "--define:FABLE_COMPILER"; "--langversion:preview" |] - let worker = ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, "MAIN APP") - - CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) - |> worker.Post +[] +module EditorInstance = + [] + type LogLevel = + | Log + | Warn + | Error + + [] + module LogLevel = + let toCssColor logLevel = + match logLevel with + | LogLevel.Log -> "inherit" + | LogLevel.Warn -> "darkorange" + | LogLevel.Error -> "red" + + let fromCompilerError (error: Error) = + if error.IsWarning then LogLevel.Warn else LogLevel.Error + + let toastNotificationFromErrors (errors: Error array) = + match errors with + | [||] -> Toastify.success "Compiled Successfully." + | _ -> Toastify.error "Failed to Compile." + + let setModelMarkers (editor: Monaco.Editor.IStandaloneCodeEditor) (markers: Monaco.Editor.IMarkerData array) = + match editor.getModel () with + | None -> () + | Some textModel -> Monaco.editor.setModelMarkers (textModel, "FSharpErrors", ResizeArray markers) + + type Model = { + Logs: (string * LogLevel) list + FSharpCode: string + CompiledJavaScript: string + IFrameIdentifier: string + IFrameUrl: string + Worker: ObservableWorker + Editor: Monaco.Editor.IStandaloneCodeEditor + Markers: Monaco.Editor.IMarkerData array + Debouncer: Debouncer.State + } + + [] + type Msg = + | Compile + | ParseCode + | Compiled of code: string * language: string * errors: Error array * stats: CompileStats + | AddConsoleLog of string * LogLevel + | SetIFrameUrl of string + | SetFSharpCode of string + | DebouncerSelfMsg of Debouncer.SelfMessage + | SetMarkers of Monaco.Editor.IMarkerData array + | SetEditor of Monaco.Editor.IStandaloneCodeEditor + + let compile model = + let language = "javascript" + let fsharpOptions = [||] + CompileCode(model.FSharpCode, language, fsharpOptions) |> model.Worker.Post + + let init initialFsharpCode = + fun () -> + let randomIdentifier = Guid.NewGuid().ToString() + let fsharpOptions = [| "--define:FABLE_COMPILER"; "--langversion:preview" |] + + let worker = + ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, randomIdentifier) + + CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) + |> worker.Post + + { + Logs = [] + FSharpCode = initialFsharpCode + CompiledJavaScript = "" + IFrameIdentifier = randomIdentifier + IFrameUrl = "" + Worker = worker + Editor = Unchecked.defaultof<_> + Markers = [||] + Debouncer = Debouncer.create () + }, + Cmd.batch [ + WebWorker.command Msg.SetMarkers Msg.Compiled worker + Iframe.command randomIdentifier { + ConsoleLog = fun text -> Msg.AddConsoleLog(text, LogLevel.Log) + ConsoleWarn = fun text -> Msg.AddConsoleLog(text, LogLevel.Warn) + ConsoleError = fun text -> Msg.AddConsoleLog(text, LogLevel.Error) + } + ] - let cmd = - Cmd.batch [ - WebWorker.command worker - Iframe.command { - ConsoleLog = fun out -> AddConsoleLog(LogLevel.Log, out) - ConsoleWarn = fun out -> AddConsoleLog(LogLevel.Warn, out) - ConsoleError = fun out -> AddConsoleLog(LogLevel.Error, out) + let update msg model = + match msg with + | Msg.Compile -> { model with Logs = [] }, Cmd.ofEffect (fun _ -> compile model) + | Msg.ParseCode -> + model, Cmd.ofEffect (fun _ -> WorkerRequest.ParseCode(model.FSharpCode, [||]) |> model.Worker.Post) + | Msg.SetIFrameUrl url -> { model with IFrameUrl = url }, Cmd.none + | Msg.SetEditor editor -> { model with Editor = editor }, Cmd.none + | Msg.AddConsoleLog(logText, logLevel) -> + let logs = model.Logs @ [ (logText, logLevel) ] + { model with Logs = logs }, Cmd.none + | Msg.SetMarkers markers -> + let model = { model with Markers = markers } + let cmd = Cmd.ofEffect (fun _ -> setModelMarkers model.Editor model.Markers) + model, cmd + | Msg.SetFSharpCode code -> + let (debouncerModel, debouncerCmd) = + model.Debouncer + |> Debouncer.bounce (TimeSpan.FromSeconds 1) "user_input" Msg.ParseCode + + { + model with + FSharpCode = code + Debouncer = debouncerModel + }, + Cmd.map Msg.DebouncerSelfMsg debouncerCmd + | Msg.Compiled(code, language, errors, stats) -> + let logs = + if errors.Length = 0 then + model.Logs + else + let errorLogs = + errors + |> Array.map (fun error -> error.Message, LogLevel.fromCompilerError error) + |> Array.toList + + model.Logs @ errorLogs + + let model = { + model with + CompiledJavaScript = code + Logs = logs } - Cmd.OfPromise.either Documentation.loadTableOfContents () FetchedTableOfContents FetchTableOfContentsExn - ] - let currentUrl = Router.currentUrl () - let tableOfContents = Documentation.emptyTableOfContents - let currentPage = getCurrentPage tableOfContents currentUrl - - { - Logs = [] - FSharpCode = "" - CompiledJavaScript = "" - Markdown = "" - IFrameUrl = "" - Worker = worker - TableOfContents = tableOfContents - CurrentPage = currentPage - DocEntryNavigation = { - PreviousEntry = None - NextEntry = None - } - Editor = Unchecked.defaultof - Markers = [||] - Debouncer = Debouncer.create () - }, - cmd - -let compile model = - let language = "javascript" - let fsharpOptions = [||] - CompileCode(model.FSharpCode, language, fsharpOptions) |> model.Worker.Post - -let helloWorldCode = "printfn \"Hello, World!\"" - -let calculateMarkdownValue (tableOfContents: Documentation.TableOfContents) currentPage = - match currentPage with - | Page.DocPage docPage -> docPage.MarkdownDocumentation - | Page.NotFound - | Page.Homepage -> tableOfContents.RootMarkdown - | Page.TableOfContents -> Documentation.TableOfContents.toMarkdownString tableOfContents - -let calculateFSharpCodeValue currentPage = - match currentPage with - | Page.DocPage docPage -> docPage.FSharpCode - | Page.NotFound - | Page.Homepage - | Page.TableOfContents -> helloWorldCode - -let setModelMarkers (editor: Monaco.Editor.IStandaloneCodeEditor) (markers: Monaco.Editor.IMarkerData array) = - match editor.getModel () with - | None -> () - | Some textModel -> Monaco.editor.setModelMarkers (textModel, "FSharpErrors", ResizeArray markers) - -let errorToLogLevel (error: Error) = - if error.IsWarning then LogLevel.Warn else LogLevel.Error - -let toastNotificationFromErrors (errors: Error array) = - match errors with - | [||] -> Toastify.success "Compiled Successfully." - | _ -> Toastify.error "Failed to Compile." - -let scrollToTopOfMarkdown () = - let markdownElement = document.getElementById "markdown-content" - markdownElement.scrollTo (0, 0) - -let update msg model = - match msg with - | Compile -> { model with Logs = [] }, Cmd.ofEffect (fun _ -> compile model) - | SetIFrameUrl url -> { model with IFrameUrl = url }, Cmd.none - | SetMarkdown doc -> { model with Markdown = doc }, Cmd.none - | SetEditor editor -> { model with Editor = editor }, Cmd.none - | ParseCode -> model, Cmd.ofEffect (fun _ -> WorkerRequest.ParseCode(model.FSharpCode, [||]) |> model.Worker.Post) - | SetFSharpCode code -> - let (debouncerModel, debouncerCmd) = - model.Debouncer - |> Debouncer.bounce (TimeSpan.FromSeconds 1) "user_input" ParseCode + model, + Cmd.batch [ + Cmd.ofEffect (fun _ -> toastNotificationFromErrors errors |> ignore) + errors |> Editor.mapErrorToMarker |> Msg.SetMarkers |> Cmd.ofMsg + Cmd.OfFunc.perform Iframe.generateHtmlBlobUrl model.CompiledJavaScript Msg.SetIFrameUrl + ] + | Msg.DebouncerSelfMsg debouncerMsg -> + let (debouncerModel, debouncerCmd) = Debouncer.update debouncerMsg model.Debouncer - { - model with - FSharpCode = code - Debouncer = debouncerModel - }, - Cmd.map DebouncerSelfMsg debouncerCmd - | SetMarkers markers -> - let model = { model with Markers = markers } - let cmd = Cmd.ofEffect (fun _ -> setModelMarkers model.Editor model.Markers) - model, cmd - | AddConsoleLog(level, output) -> - let logs = model.Logs @ [ (output, level) ] - { model with Logs = logs }, Cmd.none - | Compiled(code, _, errors, _) -> - let logs = - if errors.Length = 0 then - model.Logs - else - let errorLogs = - errors - |> Array.map (fun error -> error.Message, errorToLogLevel error) - |> Array.toList - - model.Logs @ errorLogs - - let model = { - model with - CompiledJavaScript = code - Logs = logs - } - - model, - Cmd.batch [ - Cmd.ofEffect (fun _ -> toastNotificationFromErrors errors |> ignore) - errors |> Editor.mapErrorToMarker |> SetMarkers |> Cmd.ofMsg - Cmd.OfFunc.perform Iframe.generateHtmlBlobUrl model.CompiledJavaScript SetIFrameUrl - ] - | FetchedTableOfContents tableOfContents -> - let url = Router.currentUrl () - let currentPage = getCurrentPage tableOfContents url + { + model with + Debouncer = debouncerModel + }, + debouncerCmd - { - model with - CurrentPage = currentPage - TableOfContents = tableOfContents - }, - Cmd.batch [ - Cmd.ofMsg CalculateMarkdownAndCodeValues - Cmd.ofMsg CalculateDocEntryNavigation - ] - | FetchTableOfContentsExn exn -> model, Cmd.none // TODO: this. - | SetUrl url -> - let currentPage = getCurrentPage model.TableOfContents url - - { model with CurrentPage = currentPage }, - Cmd.batch [ - Cmd.ofMsg CalculateMarkdownAndCodeValues - Cmd.ofMsg CalculateDocEntryNavigation - ] - | CalculateMarkdownAndCodeValues -> - let fsharpCode = calculateFSharpCodeValue model.CurrentPage - let markdown = calculateMarkdownValue model.TableOfContents model.CurrentPage - // clear the logs when we calculate new values. - { model with Logs = [] }, - // this has useful side-effects like triggering an initial parse through the `SetFSharpCode` msg. - Cmd.batch [ - Cmd.ofMsg (SetFSharpCode fsharpCode) - Cmd.ofMsg (SetMarkdown markdown) - Cmd.ofEffect (fun _ -> scrollToTopOfMarkdown ()) + [] + let Component initialFsharpCode = + let model, dispatch = React.useElmish (init initialFsharpCode, update) + + Html.div [ + MonacoEditor.editor [ + MonacoEditor.height "500px" + prop.className "monaco-editor" + MonacoEditor.defaultLanguage "fsharp" + MonacoEditor.value model.FSharpCode + MonacoEditor.theme "vs" + MonacoEditor.onChange (Msg.SetFSharpCode >> dispatch) + MonacoEditor.onMount (Editor.onFSharpEditorDidMount model.Worker (Msg.SetEditor >> dispatch)) + ] + Html.button [ prop.text "Compile"; prop.onClick (fun _ -> dispatch Msg.Compile) ] + Html.button [ prop.text "Open in Playground" ] + Html.article [ + prop.style [ style.height (length.percent 30); style.overflow.scroll ] + prop.children [ + Html.h4 "Output" + for (log, level) in model.Logs do + Html.p [ prop.style [ style.color (LogLevel.toCssColor level) ]; prop.text log ] + + Html.hr [] + ] + ] + Html.iframe [ + prop.id model.IFrameIdentifier + prop.src model.IFrameUrl + prop.style [ + style.position.absolute + style.width 0 + style.height 0 + style.border (0, borderStyle.hidden, "") + ] + ] ] - | CalculateDocEntryNavigation -> - let allEntries = Documentation.TableOfContents.allPages model.TableOfContents - let currentEntry = - match model.CurrentPage with - | Page.DocPage docPage -> Entry docPage - | _ -> NotViewingEntry +[] +module App = + type Model = { + Markdown: string + TableOfContents: Documentation.TableOfContents + CurrentPage: Navigation.Page + DocEntryNavigation: DocEntryNavigation + } + + [] + type Msg = + | SetMarkdown of string + | FetchedTableOfContents of Documentation.TableOfContents + | SetUrl of string list + | GetMarkdownValue + | GetDocEntryNavigation + + let private getCurrentPage tableOfContents url = + let docPages = Documentation.TableOfContents.allPages tableOfContents + Page.fromUrl docPages url + + let (|DesktopSize|MobileSize|) (screenSize: ScreenSize) = + match screenSize with + | ScreenSize.Desktop + | ScreenSize.WideScreen -> DesktopSize + | ScreenSize.Tablet + | ScreenSize.Mobile + | ScreenSize.MobileLandscape -> MobileSize + + let calculateMarkdownValue (tableOfContents: Documentation.TableOfContents) currentPage = + match currentPage with + | Page.DocPage docPage -> docPage.MarkdownDocumentation + | Page.NotFound + | Page.Homepage -> tableOfContents.RootMarkdown + | Page.TableOfContents -> Documentation.TableOfContents.toMarkdownString tableOfContents + + let scrollToTopOfMarkdown () = + let markdownElement = document.getElementById "markdown-content" + markdownElement.scrollTo (0, 0) + + let mobileNavbar = + Html.ul [ Html.li [ Html.a [ prop.href (Router.format []); prop.text "F# For You" ] ] ] + + let imageLink href src text = + Html.a [ + prop.href href + prop.target "_blank" + prop.children [ + Html.img [ + prop.src src + prop.width 40 + prop.height 40 + prop.style [ style.marginRight (length.px 5) ] + ] + Html.small (text: string) + ] + ] - { - model with - DocEntryNavigation = getDocEntryNavigation currentEntry allEntries - }, - Cmd.none - | DebouncerSelfMsg debouncerMsg -> - let (debouncerModel, debouncerCmd) = Debouncer.update debouncerMsg model.Debouncer + let desktopNavbar = [ + Html.ul [ Html.li [ imageLink (Router.format []) "img/fsharp.png" "F# For You!" ] ] - { - model with - Debouncer = debouncerModel - }, - debouncerCmd - -let (|DesktopSize|MobileSize|) (screenSize: ScreenSize) = - match screenSize with - | ScreenSize.Desktop - | ScreenSize.WideScreen -> DesktopSize - | ScreenSize.Tablet - | ScreenSize.Mobile - | ScreenSize.MobileLandscape -> MobileSize - -let mobileNavbar = - Html.ul [ Html.li [ Html.a [ prop.href (Router.format []); prop.text "F# For You" ] ] ] - -let imageLink href src text = - Html.a [ - prop.href href - prop.target "_blank" - prop.children [ - Html.img [ - prop.src src - prop.width 40 - prop.height 40 - prop.style [ style.marginRight (length.px 5) ] + Html.ul [ + Html.li [ imageLink "https://fable.io" "img/fable.png" "Powered by Fable" ] + Html.li [ + imageLink "https://github.com/fsharpforyou/tour" "img/github.png" "View Source Code" ] - Html.small (text: string) ] ] -let desktopNavbar = [ - Html.ul [ Html.li [ imageLink (Router.format []) "img/fsharp.png" "F# For You!" ] ] + let init () = + let currentUrl = Router.currentUrl () + let tableOfContents = Documentation.emptyTableOfContents + let currentPage = getCurrentPage tableOfContents currentUrl - Html.ul [ - Html.li [ imageLink "https://fable.io" "img/fable.png" "Powered by Fable" ] - Html.li [ - imageLink "https://github.com/fsharpforyou/tour" "img/github.png" "View Source Code" - ] - ] -] + { + Markdown = "" + TableOfContents = tableOfContents + CurrentPage = currentPage + DocEntryNavigation = { + PreviousEntry = None + NextEntry = None + } + }, + Cmd.OfPromise.perform Documentation.loadTableOfContents () Msg.FetchedTableOfContents + + let update msg model = + match msg with + | Msg.SetMarkdown doc -> { model with Markdown = doc }, Cmd.none + | Msg.FetchedTableOfContents tableOfContents -> + let url = Router.currentUrl () + let currentPage = getCurrentPage tableOfContents url + + { + model with + CurrentPage = currentPage + TableOfContents = tableOfContents + }, + Cmd.batch [ Cmd.ofMsg Msg.GetMarkdownValue; Cmd.ofMsg Msg.GetDocEntryNavigation ] + | Msg.SetUrl url -> + let currentPage = getCurrentPage model.TableOfContents url + + { model with CurrentPage = currentPage }, + Cmd.batch [ Cmd.ofMsg Msg.GetMarkdownValue; Cmd.ofMsg Msg.GetDocEntryNavigation ] + | Msg.GetMarkdownValue -> + let markdown = calculateMarkdownValue model.TableOfContents model.CurrentPage + { model with Markdown = markdown }, Cmd.batch [ Cmd.ofEffect (fun _ -> scrollToTopOfMarkdown ()) ] + | Msg.GetDocEntryNavigation -> + let allEntries = Documentation.TableOfContents.allPages model.TableOfContents + + let currentEntry = + match model.CurrentPage with + | Page.DocPage docPage -> Entry docPage + | _ -> NotViewingEntry + + { + model with + DocEntryNavigation = getDocEntryNavigation currentEntry allEntries + }, + Cmd.none -module View = [] - let AppView () = + let Component () = let model, dispatch = React.useElmish (init, update) let screenSize = React.useResponsive Breakpoints.defaults React.router [ - router.onUrlChanged (SetUrl >> dispatch) + router.onUrlChanged (Msg.SetUrl >> dispatch) router.children [ Html.main [ - prop.style [ - style.display.grid - - match screenSize with - | MobileSize -> - style.gridTemplateAreas [| [| "header" |]; [| "markdown" |]; [| "editor" |] |] - style.gridTemplateRows [| length.percent 10; length.auto; length.px 750 |] - style.gridTemplateColumns [| length.percent 100 |] - | DesktopSize -> - style.height (length.percent 100) - style.gridTemplateAreas [| [| "header"; "header" |]; [| "markdown"; "editor" |] |] - style.gridTemplateRows [| length.percent 10; length.percent 90 |] - style.gridTemplateColumns [| length.percent 50; length.percent 50 |] - ] prop.children [ Html.header [ - prop.style [ style.gridArea "header" ] prop.children [ Html.nav [ match screenSize with | MobileSize -> mobileNavbar | DesktopSize -> yield! desktopNavbar - - Html.ul [ - Html.button [ prop.text "Run"; prop.onClick (fun _ -> dispatch Compile) ] - ] ] ] ] Html.section [ prop.id "markdown-content" - prop.style [ - style.gridArea "markdown" - style.custom ("text-wrap", "balance") - style.overflowX.hidden - ] + // prop.style [ style.custom ("text-wrap", "balance"); style.overflowX.hidden ] prop.children [ Markdown.markdown [ + // markdown.disallowedTypes [ unbox "pre" ] markdown.children model.Markdown markdown.components [ + markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` markdown.components.code (fun props -> if props.isInline then Html.code props.children else - let style = - import "vs" "react-syntax-highlighter/dist/esm/styles/prism" - - let language = props.className.Replace("language-", "") - - SyntaxHighlighter.highlighter [ - SyntaxHighlighter.language language - SyntaxHighlighter.style style - SyntaxHighlighter.children props.children - ]) + // this is an interesting way to get the value of a code block + EditorInstance.Component(string props.children[0])) ] ] @@ -431,47 +431,6 @@ module View = ] ] ] - Html.section [ - prop.style [ style.gridArea "editor" ] - prop.children [ - Html.section [ - prop.style [ style.height (length.percent 70) ] - prop.children [ - MonacoEditor.editor [ - MonacoEditor.defaultLanguage "fsharp" - MonacoEditor.value model.FSharpCode - MonacoEditor.theme "vs" - MonacoEditor.onChange (SetFSharpCode >> dispatch) - MonacoEditor.onMount ( - Editor.onFSharpEditorDidMount model.Worker (SetEditor >> dispatch) - ) - ] - ] - ] - Html.article [ - prop.style [ style.height (length.percent 30); style.overflow.scroll ] - prop.children [ - Html.h4 "Output" - for (log, level) in model.Logs do - Html.p [ - prop.style [ style.color (LogLevel.toCssColor level) ] - prop.text log - ] - - Html.hr [] - ] - ] - ] - ] - ] - ] - Html.iframe [ - prop.src model.IFrameUrl - prop.style [ - style.position.absolute - style.width 0 - style.height 0 - style.border (0, borderStyle.hidden, "") ] ] Toastify.container [ @@ -482,4 +441,4 @@ module View = ] ] -ReactDOM.createRoot(document.getElementById "app").render (View.AppView()) +ReactDOM.createRoot(document.getElementById "app").render (App.Component()) From 37ae80ef86ca9bdd43eda3bc5aa778b4297a096f Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 14:56:22 -0500 Subject: [PATCH 07/21] Actually remove the .fsx file from documentation loading logic --- src/App/Documentation.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/App/Documentation.fs b/src/App/Documentation.fs index e9db6cf..c9da8a6 100644 --- a/src/App/Documentation.fs +++ b/src/App/Documentation.fs @@ -47,7 +47,6 @@ module private Json = type PageJson = { Title: string RouteSegment: string - FSharpFile: string MarkdownFile: string } @@ -66,7 +65,6 @@ module private Json = Decode.object (fun get -> { Title = get.Required.Field "title" Decode.string RouteSegment = get.Required.Field "route_segment" Decode.string - FSharpFile = get.Required.Field "fsharp_file" Decode.string MarkdownFile = get.Required.Field "markdown_file" Decode.string }) From f497b28808a1e3f351706649709840fdebaaf942 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 15:43:55 -0500 Subject: [PATCH 08/21] Small misc improvements --- src/App/App.fs | 116 +++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/src/App/App.fs b/src/App/App.fs index feeb67c..8b19805 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -212,7 +212,7 @@ module EditorInstance = Html.div [ MonacoEditor.editor [ - MonacoEditor.height "500px" + MonacoEditor.height "350px" prop.className "monaco-editor" MonacoEditor.defaultLanguage "fsharp" MonacoEditor.value model.FSharpCode @@ -220,18 +220,23 @@ module EditorInstance = MonacoEditor.onChange (Msg.SetFSharpCode >> dispatch) MonacoEditor.onMount (Editor.onFSharpEditorDidMount model.Worker (Msg.SetEditor >> dispatch)) ] + Html.button [ prop.text "Compile"; prop.onClick (fun _ -> dispatch Msg.Compile) ] Html.button [ prop.text "Open in Playground" ] - Html.article [ - prop.style [ style.height (length.percent 30); style.overflow.scroll ] - prop.children [ - Html.h4 "Output" - for (log, level) in model.Logs do - Html.p [ prop.style [ style.color (LogLevel.toCssColor level) ]; prop.text log ] - - Html.hr [] + + match model.Logs with + | [] -> Html.none + | logs -> + Html.article [ + prop.style [ style.height (length.percent 30); style.overflow.scroll ] + prop.children [ + // Html.h4 "Output" + for (log, level) in logs do + Html.p [ prop.style [ style.color (LogLevel.toCssColor level) ]; prop.text log ] + + Html.hr [] + ] ] - ] Html.iframe [ prop.id model.IFrameIdentifier prop.src model.IFrameUrl @@ -373,61 +378,58 @@ module App = router.onUrlChanged (Msg.SetUrl >> dispatch) router.children [ Html.main [ - prop.children [ - Html.header [ - prop.children [ - Html.nav [ - match screenSize with - | MobileSize -> mobileNavbar - | DesktopSize -> yield! desktopNavbar - ] - ] + Html.header [ + Html.nav [ + match screenSize with + | MobileSize -> mobileNavbar + | DesktopSize -> yield! desktopNavbar ] - Html.section [ - prop.id "markdown-content" - // prop.style [ style.custom ("text-wrap", "balance"); style.overflowX.hidden ] - prop.children [ - Markdown.markdown [ - // markdown.disallowedTypes [ unbox "pre" ] - markdown.children model.Markdown - markdown.components [ - markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` - markdown.components.code (fun props -> - if props.isInline then - Html.code props.children - else - // this is an interesting way to get the value of a code block - EditorInstance.Component(string props.children[0])) - ] + ] + Html.section [ + prop.id "markdown-content" + prop.children [ + Markdown.markdown [ + markdown.children model.Markdown + markdown.components [ + markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` + markdown.components.code (fun props -> + if props.isInline then + Html.code props.children + else + // this is an interesting way to get the value of a code block + props.children + |> Seq.tryHead + |> Option.map (string >> EditorInstance.Component) + |> Option.defaultValue Html.none) ] + ] - Html.nav [ - Html.ul [ - match model.DocEntryNavigation.PreviousEntry with - | None -> Html.none - | Some entry -> - Html.li [ - Html.a [ - prop.href (Router.format entry.Route) - prop.text $"< {entry.Title}" - ] - ] + Html.nav [ + Html.ul [ + match model.DocEntryNavigation.PreviousEntry with + | None -> Html.none + | Some entry -> Html.li [ Html.a [ - prop.href (Router.format [ "table-of-contents" ]) - prop.text "Table of Contents" + prop.href (Router.format entry.Route) + prop.text $"< {entry.Title}" ] ] - match model.DocEntryNavigation.NextEntry with - | None -> Html.none - | Some entry -> - Html.li [ - Html.a [ - prop.href (Router.format entry.Route) - prop.text $"{entry.Title} >" - ] - ] + Html.li [ + Html.a [ + prop.href (Router.format [ "table-of-contents" ]) + prop.text "Table of Contents" + ] ] + match model.DocEntryNavigation.NextEntry with + | None -> Html.none + | Some entry -> + Html.li [ + Html.a [ + prop.href (Router.format entry.Route) + prop.text $"{entry.Title} >" + ] + ] ] ] ] From c80735847d49c89b6c7e86522ee2a1810fa5bbf5 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 24 Jun 2024 15:44:18 -0500 Subject: [PATCH 09/21] Remove F# files from table-of-contents.json --- public/documentation/table-of-contents.json | 28 --------------------- 1 file changed, 28 deletions(-) diff --git a/public/documentation/table-of-contents.json b/public/documentation/table-of-contents.json index e57ed21..ba8a6ed 100644 --- a/public/documentation/table-of-contents.json +++ b/public/documentation/table-of-contents.json @@ -8,43 +8,36 @@ { "title": "Hello, World!", "route_segment": "hello-world", - "fsharp_file": "basics/hello-world.fsx", "markdown_file": "basics/hello-world.md" }, { "title": "Expressions", "route_segment": "expressions", - "fsharp_file": "basics/expressions.fsx", "markdown_file": "basics/expressions.md" }, { "title": "Primitive Types", "route_segment": "primitive-types", - "fsharp_file": "basics/primitive-types.fsx", "markdown_file": "basics/primitive-types.md" }, { "title": "String Formatting", "route_segment": "string-formatting", - "fsharp_file": "basics/string-formatting.fsx", "markdown_file": "basics/string-formatting.md" }, { "title": "Operators", "route_segment": "operators", - "fsharp_file": "basics/operators.fsx", "markdown_file": "basics/operators.md" }, { "title": "Conditional Expressions", "route_segment": "conditional-expressions", - "fsharp_file": "basics/conditional-expressions.fsx", "markdown_file": "basics/conditional-expressions.md" }, { "title": "Patterns and Match Expressions", "route_segment": "pattern-matching", - "fsharp_file": "basics/patterns-and-match-expressions.fsx", "markdown_file": "basics/patterns-and-match-expressions.md" } ] @@ -56,31 +49,26 @@ { "title": "Defining Functions", "route_segment": "definition", - "fsharp_file": "functions/defining-functions.fsx", "markdown_file": "functions/defining-functions.md" }, { "title": "Functions as Values", "route_segment": "values", - "fsharp_file": "functions/functions-as-values.fsx", "markdown_file": "functions/functions-as-values.md" }, { "title": "Recursive Functions", "route_segment": "recursive-functions", - "fsharp_file": "functions/recursive-functions.fsx", "markdown_file": "functions/recursive-functions.md" }, { "title": "Currying and Partial Application", "route_segment": "currying-and-partial-application", - "fsharp_file": "functions/currying-and-partial-application.fsx", "markdown_file": "functions/currying-and-partial-application.md" }, { "title": "Pipelines and Composition", "route_segment": "pipelines-and-composition", - "fsharp_file": "functions/pipelines-and-composition.fsx", "markdown_file": "functions/pipelines-and-composition.md" } ] @@ -92,67 +80,56 @@ { "title": "Tuples", "route_segment": "tuples", - "fsharp_file": "data-and-types/tuples.fsx", "markdown_file": "data-and-types/tuples.md" }, { "title": "Lists", "route_segment": "lists", - "fsharp_file": "data-and-types/lists.fsx", "markdown_file": "data-and-types/lists.md" }, { "title": "Sequences", "route_segment": "sequences", - "fsharp_file": "data-and-types/sequences.fsx", "markdown_file": "data-and-types/sequences.md" }, { "title": "Sequence Expressions", "route_segment": "sequence-expressions", - "fsharp_file": "data-and-types/sequence-expressions.fsx", "markdown_file": "data-and-types/sequence-expressions.md" }, { "title": "Type Abbreviations", "route_segment": "abbreviations", - "fsharp_file": "data-and-types/abbreviations.fsx", "markdown_file": "data-and-types/abbreviations.md" }, { "title": "Records", "route_segment": "records", - "fsharp_file": "data-and-types/records.fsx", "markdown_file": "data-and-types/records.md" }, { "title": "Discriminated Unions", "route_segment": "discriminated-unions", - "fsharp_file": "data-and-types/discriminated-unions.fsx", "markdown_file": "data-and-types/discriminated-unions.md" }, { "title": "Generic Types", "route_segment": "generic-types", - "fsharp_file": "data-and-types/generic-types.fsx", "markdown_file": "data-and-types/generic-types.md" }, { "title": "The Option Type", "route_segment": "options", - "fsharp_file": "data-and-types/the-option-type.fsx", "markdown_file": "data-and-types/the-option-type.md" }, { "title": "The Result Type", "route_segment": "results", - "fsharp_file": "data-and-types/the-result-type.fsx", "markdown_file": "data-and-types/the-result-type.md" }, { "title": "Units of Measure", "route_segment": "units-of-measure", - "fsharp_file": "data-and-types/units-of-measure.fsx", "markdown_file": "data-and-types/units-of-measure.md" } ] @@ -164,31 +141,26 @@ { "title": "Objects and Members", "route_segment": "objects-and-members", - "fsharp_file": "object-programming/objects-and-members.fsx", "markdown_file": "object-programming/objects-and-members.md" }, { "title": "Interfaces", "route_segment": "interfaces", - "fsharp_file": "object-programming/interfaces.fsx", "markdown_file": "object-programming/interfaces.md" }, { "title": "Abstract Classes and Inheritance", "route_segment": "abstract-classes-and-inheritance", - "fsharp_file": "object-programming/abstract-classes-and-inheritance.fsx", "markdown_file": "object-programming/abstract-classes-and-inheritance.md" }, { "title": "Object Expressions", "route_segment": "object-expressions", - "fsharp_file": "object-programming/object-expressions.fsx", "markdown_file": "object-programming/object-expressions.md" }, { "title": "Type Extensions", "route_segment": "type-extensions", - "fsharp_file": "object-programming/type-extensions.fsx", "markdown_file": "object-programming/type-extensions.md" } ] From 0051c738f885243be4e254f5b52bb4b23b552500 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Fri, 28 Jun 2024 11:16:45 -0500 Subject: [PATCH 10/21] Misc styling improvements Improved compile button Improved navigation - Added sidebar for navigating between pages. --- index.html | 1 + src/App/App.fs | 126 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/index.html b/index.html index 0c3da25..3843fda 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ F# Tour + + diff --git a/package-lock.json b/package-lock.json index 5c08486..7379b92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "@fable-org/fable-metadata": "^1.1.0", "@fable-org/fable-standalone": "^1.8.1", "@monaco-editor/react": "^4.6.0", + "lz-string": "^1.5.0", "monaco-editor": "^0.46.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -16,61 +17,61 @@ "use-sync-external-store": "^1.2.0" }, "devDependencies": { - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.0.12" + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.3" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", + "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz", + "integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -86,14 +87,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", + "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.8", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -101,14 +102,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -117,62 +118,66 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", + "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -182,97 +187,98 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -282,12 +288,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -297,12 +303,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -312,33 +318,33 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -347,13 +353,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz", + "integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -361,9 +367,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -377,9 +383,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -393,9 +399,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -409,9 +415,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -425,9 +431,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -441,9 +447,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -457,9 +463,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -473,9 +479,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -489,9 +495,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -505,9 +511,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -521,9 +527,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -537,9 +543,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -553,9 +559,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -569,9 +575,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -585,9 +591,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -601,9 +607,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -617,9 +623,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -633,9 +639,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -649,9 +655,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -665,9 +671,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -681,9 +687,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -697,9 +703,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -713,9 +719,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -742,14 +748,14 @@ "integrity": "sha512-+HTkVzl/+1NmypPSgYouufQJa9BpptCDGLYgJQgruENYpprfGlawa62gC97HohxsWeEZyRn9wok6L4V72zrBjw==" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -765,24 +771,24 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -814,9 +820,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", + "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", "cpu": [ "arm" ], @@ -827,9 +833,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", + "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", "cpu": [ "arm64" ], @@ -840,9 +846,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", + "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", "cpu": [ "arm64" ], @@ -853,9 +859,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", + "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", "cpu": [ "x64" ], @@ -866,9 +872,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", + "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", + "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", "cpu": [ "arm" ], @@ -879,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", + "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", "cpu": [ "arm64" ], @@ -892,9 +911,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", + "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", "cpu": [ "arm64" ], @@ -904,10 +923,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", + "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", + "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", "cpu": [ "riscv64" ], @@ -917,10 +949,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", + "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", + "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", "cpu": [ "x64" ], @@ -931,9 +976,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", + "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", "cpu": [ "x64" ], @@ -944,9 +989,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", + "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", "cpu": [ "arm64" ], @@ -957,9 +1002,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", + "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", "cpu": [ "ia32" ], @@ -970,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", + "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", "cpu": [ "x64" ], @@ -1086,16 +1131,16 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" }, "node_modules/@vitejs/plugin-react": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", - "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", "dev": true, "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-react-jsx-self": "^7.23.3", - "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.0" + "react-refresh": "^0.14.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1126,9 +1171,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -1145,10 +1190,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -1158,9 +1203,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001588", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", - "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", + "version": "1.0.30001641", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz", + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==", "dev": true, "funding": [ { @@ -1294,15 +1339,15 @@ "integrity": "sha512-BWBkv157Cf/z7Hjod2v2JS7vyC36Dk1QQolAtuLjpl8RBlv7Z92X7+Ufc2cjfR/B3iJUiK0QmGZkgtsmmLz0Tw==" }, "node_modules/electron-to-chromium": { - "version": "1.4.678", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.678.tgz", - "integrity": "sha512-NbdGC2p0O5Q5iVhLEsNBSfytaw7wbEFJlIvaF71wi6QDtLAph5/rVogjyOpf/QggJIt8hNK3KdwNJnc2bzckbw==", + "version": "1.4.825", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.825.tgz", + "integrity": "sha512-OCcF+LwdgFGcsYPYC5keEEFC2XT0gBhrYbeGzHCx7i9qRFbzO/AqTmc/C/1xNhJj+JA7rzlN7mpBuStshh96Cg==", "dev": true }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -1312,29 +1357,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -1505,6 +1550,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -2045,15 +2098,15 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -2071,8 +2124,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2161,9 +2214,9 @@ } }, "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -2211,9 +2264,9 @@ } }, "node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", + "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -2226,19 +2279,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.18.1", + "@rollup/rollup-android-arm64": "4.18.1", + "@rollup/rollup-darwin-arm64": "4.18.1", + "@rollup/rollup-darwin-x64": "4.18.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", + "@rollup/rollup-linux-arm-musleabihf": "4.18.1", + "@rollup/rollup-linux-arm64-gnu": "4.18.1", + "@rollup/rollup-linux-arm64-musl": "4.18.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", + "@rollup/rollup-linux-riscv64-gnu": "4.18.1", + "@rollup/rollup-linux-s390x-gnu": "4.18.1", + "@rollup/rollup-linux-x64-gnu": "4.18.1", + "@rollup/rollup-linux-x64-musl": "4.18.1", + "@rollup/rollup-win32-arm64-msvc": "4.18.1", + "@rollup/rollup-win32-ia32-msvc": "4.18.1", + "@rollup/rollup-win32-x64-msvc": "4.18.1", "fsevents": "~2.3.2" } }, @@ -2271,9 +2327,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -2431,9 +2487,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -2450,8 +2506,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -2514,14 +2570,14 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 4979093..8486894 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@fable-org/fable-metadata": "^1.1.0", "@fable-org/fable-standalone": "^1.8.1", "@monaco-editor/react": "^4.6.0", + "lz-string": "^1.5.0", "monaco-editor": "^0.46.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -16,7 +17,7 @@ "use-sync-external-store": "^1.2.0" }, "devDependencies": { - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.0.12" + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.3" } -} \ No newline at end of file +} diff --git a/public/documentation/table-of-contents.json b/public/documentation/table-of-contents.json index ba8a6ed..bf84f72 100644 --- a/public/documentation/table-of-contents.json +++ b/public/documentation/table-of-contents.json @@ -4,7 +4,7 @@ { "title": "Basics", "route_segment": "basics", - "pages": [ + "entries": [ { "title": "Hello, World!", "route_segment": "hello-world", @@ -45,7 +45,7 @@ { "title": "Functions", "route_segment": "functions", - "pages": [ + "entries": [ { "title": "Defining Functions", "route_segment": "definition", @@ -76,7 +76,7 @@ { "title": "Data and Types", "route_segment": "data-and-types", - "pages": [ + "entries": [ { "title": "Tuples", "route_segment": "tuples", @@ -137,7 +137,7 @@ { "title": "Object Programming", "route_segment": "object-programming", - "pages": [ + "entries": [ { "title": "Objects and Members", "route_segment": "objects-and-members", diff --git a/src/App/App.fs b/src/App/App.fs index 8ff86b2..4986817 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -15,9 +15,31 @@ open MonacoEditor open System open Feliz.UseMediaQuery +let fsharpOptions = [| + "--define:FABLE_COMPILER" + "--define:FABLE_COMPILER_4" + "--define:FABLE_COMPILER_JAVASCRIPT" + "--langversion:preview" +|] + + +let monacoEditorOptions = {| + minimap = {| enabled = false |} + fontSize = 16 + fontFamily = "JetBrains Mono" // TODO: I don't think this is working +|} + importSideEffects "react-toastify/dist/ReactToastify.css" importSideEffects "./monaco-vite.js" +[] +type LzString = + [] + static member compressToEncodedURIComponent(input: string) : string = nativeOnly + + [] + static member decompressFromEncodedURIComponent(compressed: string) : string = nativeOnly + [] type MonacoEditor = static member inline onChange(f: string -> unit) = Interop.mkAttr "onChange" f @@ -27,6 +49,8 @@ type MonacoEditor = static member inline width(value: string) = Interop.mkAttr "width" value static member inline height(value: string) = Interop.mkAttr "height" value + static member inline options(value: obj) = Interop.mkAttr "options" value + static member inline onMount(f: System.Func) = Interop.mkAttr "onMount" f @@ -60,33 +84,13 @@ module WebWorker = [ handler ] [] -module EditorInstance = +module EditorUtils = [] type LogLevel = | Log | Warn | Error - [] - module LogLevel = - let toCssColor logLevel = - match logLevel with - | LogLevel.Log -> "inherit" - | LogLevel.Warn -> "darkorange" - | LogLevel.Error -> "red" - - let fromCompilerError (error: Error) = - if error.IsWarning then LogLevel.Warn else LogLevel.Error - - let toastNotificationFromErrors (errors: Error array) = - match errors with - | [||] -> Toastify.success "Compiled Successfully." - | _ -> Toastify.error "Failed to Compile." - - let setModelMarkers (editor: Monaco.Editor.IStandaloneCodeEditor) (markers: Monaco.Editor.IMarkerData array) = - match editor.getModel () with - | None -> () - | Some textModel -> Monaco.editor.setModelMarkers (textModel, "FSharpErrors", ResizeArray markers) type Model = { Logs: (string * LogLevel) list @@ -94,6 +98,7 @@ module EditorInstance = CompiledJavaScript: string IFrameIdentifier: string IFrameUrl: string + PlaygroundUrl: string Worker: ObservableWorker Editor: Monaco.Editor.IStandaloneCodeEditor Markers: Monaco.Editor.IMarkerData array @@ -112,48 +117,55 @@ module EditorInstance = | SetMarkers of Monaco.Editor.IMarkerData array | SetEditor of Monaco.Editor.IStandaloneCodeEditor - let compile model = - let language = "javascript" - let fsharpOptions = [||] - CompileCode(model.FSharpCode, language, fsharpOptions) |> model.Worker.Post + // TODO: move this to navigation??? + let playgroundUrlComponents fsharpCode = + let data = LzString.compressToEncodedURIComponent fsharpCode + [ "playground"; $"?data={data}" ] - let init initialFsharpCode = - fun () -> - let randomIdentifier = Guid.NewGuid().ToString() - let fsharpOptions = [| "--define:FABLE_COMPILER"; "--langversion:preview" |] + let createPlaygroundUrl fsharpCode = + Router.format (playgroundUrlComponents fsharpCode) - let worker = - ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, randomIdentifier) + [] + module LogLevel = + let toCssColor logLevel = + match logLevel with + | LogLevel.Log -> "inherit" + | LogLevel.Warn -> "darkorange" + | LogLevel.Error -> "red" - CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) - |> worker.Post + let fromCompilerError (error: Error) = + if error.IsWarning then LogLevel.Warn else LogLevel.Error - { - Logs = [] - FSharpCode = initialFsharpCode - CompiledJavaScript = "" - IFrameIdentifier = randomIdentifier - IFrameUrl = "" - Worker = worker - Editor = Unchecked.defaultof<_> - Markers = [||] - Debouncer = Debouncer.create () - }, - Cmd.batch [ - WebWorker.command Msg.SetMarkers Msg.Compiled worker - Iframe.command randomIdentifier { - ConsoleLog = fun text -> Msg.AddConsoleLog(text, LogLevel.Log) - ConsoleWarn = fun text -> Msg.AddConsoleLog(text, LogLevel.Warn) - ConsoleError = fun text -> Msg.AddConsoleLog(text, LogLevel.Error) - } - Cmd.ofMsg Msg.ParseCode // NOTE: this may have terrible performance lol - ] + + let setModelMarkers (editor: Monaco.Editor.IStandaloneCodeEditor) (markers: Monaco.Editor.IMarkerData array) = + match editor.getModel () with + | None -> () + | Some textModel -> Monaco.editor.setModelMarkers (textModel, "FSharpErrors", ResizeArray markers) + + let toastNotificationFromErrors (errors: Error array) = + match errors with + | [||] -> Toastify.success "Compiled Successfully." + | _ -> Toastify.error "Failed to Compile." + + let initialCommand model = + Cmd.batch [ + WebWorker.command Msg.SetMarkers Msg.Compiled model.Worker + Iframe.command model.IFrameIdentifier { + ConsoleLog = fun text -> Msg.AddConsoleLog(text, LogLevel.Log) + ConsoleWarn = fun text -> Msg.AddConsoleLog(text, LogLevel.Warn) + ConsoleError = fun text -> Msg.AddConsoleLog(text, LogLevel.Error) + } + Cmd.ofMsg Msg.ParseCode // NOTE: this may have terrible performance lol + ] + + let compile model = + CompileCode(model.FSharpCode, "javascript", fsharpOptions) |> model.Worker.Post let update msg model = match msg with | Msg.Compile -> { model with Logs = [] }, Cmd.ofEffect (fun _ -> compile model) | Msg.ParseCode -> - model, Cmd.ofEffect (fun _ -> WorkerRequest.ParseCode(model.FSharpCode, [||]) |> model.Worker.Post) + model, Cmd.ofEffect (fun _ -> WorkerRequest.ParseCode(model.FSharpCode, fsharpOptions) |> model.Worker.Post) | Msg.SetIFrameUrl url -> { model with IFrameUrl = url }, Cmd.none | Msg.SetEditor editor -> { model with Editor = editor }, Cmd.none | Msg.AddConsoleLog(logText, logLevel) -> @@ -171,6 +183,7 @@ module EditorInstance = { model with FSharpCode = code + PlaygroundUrl = createPlaygroundUrl code // TODO: ??? do we want to do this every time you type? Debouncer = debouncerModel }, Cmd.map Msg.DebouncerSelfMsg debouncerCmd @@ -207,42 +220,129 @@ module EditorInstance = }, debouncerCmd + let init worker initialFsharpCode = + fun () -> + let randomIdentifier = Guid.NewGuid().ToString() + + let model = { + Logs = [] + FSharpCode = initialFsharpCode + CompiledJavaScript = "" + IFrameIdentifier = randomIdentifier + IFrameUrl = "" + PlaygroundUrl = createPlaygroundUrl initialFsharpCode + Worker = worker + Editor = Unchecked.defaultof<_> + Markers = [||] + Debouncer = Debouncer.create () + } + + model, Cmd.batch [ initialCommand model ] + +[] +module Playground = + [] + let Component worker initialFSharpCode = + let model, dispatch = + React.useElmish (EditorUtils.init worker initialFSharpCode, EditorUtils.update) + + Html.div [ + Html.button [ + prop.text "Compile" + prop.onClick (fun _ -> dispatch EditorUtils.Msg.Compile) + ] + Html.button [ + prop.text "Share" + prop.onClick (fun _ -> + Router.nav + (EditorUtils.playgroundUrlComponents model.FSharpCode) + HistoryMode.PushState + RouteMode.Hash) + ] + MonacoEditor.editor [ + MonacoEditor.height "1000px" + MonacoEditor.defaultLanguage "fsharp" + MonacoEditor.value model.FSharpCode + MonacoEditor.theme "vs" + MonacoEditor.onChange (EditorUtils.Msg.SetFSharpCode >> dispatch) + MonacoEditor.options monacoEditorOptions + MonacoEditor.onMount ( + Editor.onFSharpEditorDidMount model.Worker (EditorUtils.Msg.SetEditor >> dispatch) + ) + ] + match model.Logs with + | [] -> Html.none + | logs -> + Html.article [ + prop.style [ style.height (length.percent 30); style.overflow.scroll ] + prop.children [ + for (log, level) in logs do + Html.p [ + prop.style [ style.color (EditorUtils.LogLevel.toCssColor level) ] + prop.text log + ] + ] + ] + Html.iframe [ + prop.id model.IFrameIdentifier + prop.src model.IFrameUrl + prop.style [ + style.position.absolute + style.width 0 + style.height 0 + style.border (0, borderStyle.hidden, "") + ] + ] + ] + +[] +module DocumentationEditorInstance = [] - let Component initialFsharpCode = - let model, dispatch = React.useElmish (init initialFsharpCode, update) + let Component worker initialFsharpCode = + let model, dispatch = + React.useElmish (EditorUtils.init worker initialFsharpCode, EditorUtils.update) Html.div [ Html.i [ prop.style [ style.float'.right; style.color "green" ] prop.className "fa-solid fa-play" - prop.onClick (fun _ -> dispatch Msg.Compile) + prop.onClick (fun _ -> dispatch EditorUtils.Msg.Compile) ] - //Html.button [ prop.text "Compile"; prop.onClick (fun _ -> dispatch Msg.Compile) ] - // Html.a [ - // prop.href "/#/playground/#/" - // prop.children [ Html.i [ prop.className "fa-solid fa-flask" ] ] - // ] MonacoEditor.editor [ MonacoEditor.height "350px" - prop.className "monaco-editor" MonacoEditor.defaultLanguage "fsharp" MonacoEditor.value model.FSharpCode MonacoEditor.theme "vs" - MonacoEditor.onChange (Msg.SetFSharpCode >> dispatch) - MonacoEditor.onMount (Editor.onFSharpEditorDidMount model.Worker (Msg.SetEditor >> dispatch)) + MonacoEditor.onChange (EditorUtils.Msg.SetFSharpCode >> dispatch) + MonacoEditor.options monacoEditorOptions + MonacoEditor.onMount ( + Editor.onFSharpEditorDidMount model.Worker (EditorUtils.Msg.SetEditor >> dispatch) + ) ] match model.Logs with | [] -> Html.none | logs -> - Html.article [ + Html.hr [] + + Html.div [ prop.style [ style.height (length.percent 30); style.overflow.scroll ] prop.children [ - // Html.h4 "Output" for (log, level) in logs do - Html.p [ prop.style [ style.color (LogLevel.toCssColor level) ]; prop.text log ] + Html.p [ + prop.style [ style.color (EditorUtils.LogLevel.toCssColor level) ] + prop.text log + ] ] ] + + Html.hr [] + + Html.a [ + prop.href (EditorUtils.createPlaygroundUrl model.FSharpCode) + prop.text "Open in Playground" + ] + Html.iframe [ prop.id model.IFrameIdentifier prop.src model.IFrameUrl @@ -255,228 +355,218 @@ module EditorInstance = ] ] +[] +module Documentation = + [] + type CurrentEntry = + | Root + | Entry of Documentation.Entry + + let private formatDocRoute (route: string list) = Router.format ("docs" :: route) + + [] + let Component worker currentEntry tableOfContents = + let allEntries = Documentation.TableOfContents.allEntries tableOfContents + + let markdownDocumentation, githubUrl, docEntryNavigation = + match currentEntry with + | CurrentEntry.Root -> + tableOfContents.RootMarkdown, + tableOfContents.RootGitHubUrl, + { + PreviousEntry = None + NextEntry = allEntries |> List.tryHead |> Option.map NavigationEntry.fromDocEntry + } + | CurrentEntry.Entry entry -> + let navigation = + match getDocEntryNavigation entry allEntries with + | { + PreviousEntry = None + NextEntry = nextEntry + } -> { + PreviousEntry = Some { Title = "Cover"; Route = [] } + NextEntry = nextEntry + } + | navigation -> navigation + + entry.MarkdownDocumentation, entry.GitHubUrl, navigation + + Html.div [ + prop.style [ + style.display.grid + style.gridTemplateAreas [| "sidebar"; "markdown" |] + style.gridTemplateRows [| length.percent 100 |] + style.gridTemplateColumns [| length.percent 20; length.percent 80 |] + ] + prop.children [ + Html.aside [ + prop.style [ style.gridArea "sidebar" ] + prop.children [ + Html.nav [ + for category in tableOfContents.Categories do + Html.details [ + Html.summary [ Html.strong category.Title ] + Html.ul [ + for entry in category.Entries do + Html.li [ + Html.a [ prop.href (formatDocRoute entry.Route); prop.text entry.Title ] + ] + ] + ] + ] + ] + ] + Html.section [ + prop.id "markdown-content" + prop.className "container-fluid" + prop.style [ style.gridArea "markdown" ] + prop.children [ + Markdown.markdown [ + markdown.children markdownDocumentation + markdown.components [ + markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` + markdown.components.code (fun props -> + if props.isInline then + Html.code props.children + else + // this is an interesting way to get the value of a code block + props.children + |> Seq.tryHead + |> Option.map (string >> DocumentationEditorInstance.Component worker) + |> Option.defaultValue Html.none) + ] + ] + + Html.nav [ + Html.ul [ + match docEntryNavigation.PreviousEntry with + | None -> Html.none + | Some entry -> + Html.li [ + Html.a [ prop.href (formatDocRoute entry.Route); prop.text $"< {entry.Title}" ] + ] + + match docEntryNavigation.NextEntry with + | None -> Html.none + | Some entry -> + Html.li [ + Html.a [ prop.href (formatDocRoute entry.Route); prop.text $"{entry.Title} >" ] + ] + ] + ] + + Html.a [ + prop.target.blank + prop.href githubUrl + prop.children [ Html.small "Edit This Page on GitHub" ] + ] + + ] + ] + ] + ] + [] module App = type Model = { - Markdown: string - TableOfContents: Documentation.TableOfContents + CurrentUrl: string list CurrentPage: Navigation.Page - DocEntryNavigation: DocEntryNavigation + TableOfContents: Documentation.TableOfContents + Worker: ObservableWorker } [] type Msg = - | SetMarkdown of string - | FetchedTableOfContents of Documentation.TableOfContents | SetUrl of string list - | GetMarkdownValue - | GetDocEntryNavigation - - let private getCurrentPage tableOfContents url = - let docPages = Documentation.TableOfContents.allPages tableOfContents - Page.fromUrl docPages url - - let (|DesktopSize|MobileSize|) (screenSize: ScreenSize) = - match screenSize with - | ScreenSize.Desktop - | ScreenSize.WideScreen -> DesktopSize - | ScreenSize.Tablet - | ScreenSize.Mobile - | ScreenSize.MobileLandscape -> MobileSize - - let calculateMarkdownValue (tableOfContents: Documentation.TableOfContents) currentPage = - match currentPage with - | Page.DocPage docPage -> docPage.MarkdownDocumentation - | Page.NotFound - | Page.Homepage -> tableOfContents.RootMarkdown - | Page.TableOfContents -> Documentation.TableOfContents.toMarkdownString tableOfContents - - let scrollToTopOfMarkdown () = - let markdownElement = document.getElementById "markdown-content" - markdownElement.scrollTo (0, 0) - - let mobileNavbar = - Html.ul [ Html.li [ Html.a [ prop.href (Router.format []); prop.text "F# For You" ] ] ] - - let imageLink href src text = - Html.a [ - prop.href href - prop.target "_blank" - prop.children [ - Html.img [ - prop.src src - prop.width 40 - prop.height 40 - prop.style [ style.marginRight (length.px 5) ] - ] - Html.small (text: string) - ] - ] - - let desktopNavbar = [ - Html.ul [ Html.li [ imageLink (Router.format []) "img/fsharp.png" "F# For You!" ] ] + | FetchedTableOfContents of Documentation.TableOfContents - Html.ul [ - Html.li [ imageLink "https://fable.io" "img/fable.png" "Powered by Fable" ] - Html.li [ - imageLink "https://github.com/fsharpforyou/tour" "img/github.png" "View Source Code" - ] - ] - ] + let getPageFromUrl tableOfContents url = + Page.fromUrl (Documentation.TableOfContents.allEntries tableOfContents) url let init () = - let currentUrl = Router.currentUrl () let tableOfContents = Documentation.emptyTableOfContents - let currentPage = getCurrentPage tableOfContents currentUrl + let worker = ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, "MAIN APP") { - Markdown = "" + CurrentUrl = [] + CurrentPage = Page.Homepage TableOfContents = tableOfContents - CurrentPage = currentPage - DocEntryNavigation = { - PreviousEntry = None - NextEntry = None - } + Worker = worker }, - Cmd.OfPromise.perform Documentation.loadTableOfContents () Msg.FetchedTableOfContents + Cmd.batch [ + Cmd.OfPromise.perform Documentation.loadTableOfContents () Msg.FetchedTableOfContents + Cmd.ofEffect (fun _ -> + CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) + |> worker.Post) + ] let update msg model = match msg with - | Msg.SetMarkdown doc -> { model with Markdown = doc }, Cmd.none - | Msg.FetchedTableOfContents tableOfContents -> - let url = Router.currentUrl () - let currentPage = getCurrentPage tableOfContents url + | Msg.SetUrl url -> + let currentPage = getPageFromUrl model.TableOfContents url { model with + CurrentUrl = url CurrentPage = currentPage - TableOfContents = tableOfContents }, - Cmd.batch [ Cmd.ofMsg Msg.GetMarkdownValue; Cmd.ofMsg Msg.GetDocEntryNavigation ] - | Msg.SetUrl url -> - let currentPage = getCurrentPage model.TableOfContents url - - { model with CurrentPage = currentPage }, - Cmd.batch [ Cmd.ofMsg Msg.GetMarkdownValue; Cmd.ofMsg Msg.GetDocEntryNavigation ] - | Msg.GetMarkdownValue -> - let markdown = calculateMarkdownValue model.TableOfContents model.CurrentPage - { model with Markdown = markdown }, Cmd.batch [ Cmd.ofEffect (fun _ -> scrollToTopOfMarkdown ()) ] - | Msg.GetDocEntryNavigation -> - let allEntries = Documentation.TableOfContents.allPages model.TableOfContents - - let currentEntry = - match model.CurrentPage with - | Page.DocPage docPage -> Entry docPage - | _ -> NotViewingEntry + Cmd.none + | Msg.FetchedTableOfContents tableOfContents -> + let currentPage = getPageFromUrl tableOfContents model.CurrentUrl { model with - DocEntryNavigation = getDocEntryNavigation currentEntry allEntries + CurrentPage = currentPage + TableOfContents = tableOfContents }, Cmd.none + // TODO: Render "Root markdown" if you're on the /docs/ page [] let Component () = let model, dispatch = React.useElmish (init, update) - let screenSize = React.useResponsive Breakpoints.defaults React.router [ router.onUrlChanged (Msg.SetUrl >> dispatch) router.children [ - Html.main [ - Html.header [ + Html.header [ + prop.className "container-fluid" + prop.children [ Html.nav [ - match screenSize with - | MobileSize -> mobileNavbar - | DesktopSize -> yield! desktopNavbar - ] - ] - Html.div [ - prop.style [ - style.display.grid - style.gridTemplateAreas [| "sidebar"; "markdown" |] - style.gridTemplateRows [| length.percent 100 |] - style.gridTemplateColumns [| length.percent 20; length.percent 80 |] - ] - prop.children [ - Html.aside [ - prop.style [ style.gridArea "sidebar" ] - prop.children [ - Html.nav [ - for category in model.TableOfContents.Categories do - Html.details [ - Html.summary [ Html.strong category.Title ] - Html.ul [ - for page in category.Pages do - Html.li [ - Html.a [ - prop.href (Router.format page.Route) - prop.text page.Title - ] - ] - ] - ] - ] - ] - ] - Html.section [ - prop.id "markdown-content" - prop.className "container-fluid" - prop.style [ style.gridArea "markdown" ] - prop.children [ - Markdown.markdown [ - markdown.children model.Markdown - markdown.components [ - markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` - markdown.components.code (fun props -> - if props.isInline then - Html.code props.children - else - // this is an interesting way to get the value of a code block - props.children - |> Seq.tryHead - |> Option.map (string >> EditorInstance.Component) - |> Option.defaultValue Html.none) - ] - ] - - Html.nav [ - Html.ul [ - match model.DocEntryNavigation.PreviousEntry with - | None -> Html.none - | Some entry -> - Html.li [ - Html.a [ - prop.href (Router.format entry.Route) - prop.text $"< {entry.Title}" - ] - ] - Html.li [ - Html.a [ - prop.href (Router.format [ "table-of-contents" ]) - prop.text "Table of Contents" - ] - ] - match model.DocEntryNavigation.NextEntry with - | None -> Html.none - | Some entry -> - Html.li [ - Html.a [ - prop.href (Router.format entry.Route) - prop.text $"{entry.Title} >" - ] - ] - ] - ] + Html.ul [ Html.li [ Html.a [ prop.href (Router.format []); prop.text "F# For You" ] ] ] + Html.ul [ + Html.li [ Html.a [ prop.href (Router.format [ "docs" ]); prop.text "Docs" ] ] + Html.li [ + Html.a [ prop.href (Router.format [ "playground" ]); prop.text "Playground" ] ] ] ] ] ] - Toastify.container [ - ContainerOption.autoClose 2000 - ContainerOption.position Position.BottomRight - ContainerOption.theme Theme.Light + Html.main [ + match model.CurrentPage with + | Page.Docs -> + Documentation.Component model.Worker Documentation.CurrentEntry.Root model.TableOfContents + | Page.DocsEntry entry -> + Documentation.Component + model.Worker + (Documentation.CurrentEntry.Entry entry) + model.TableOfContents + | Page.Homepage -> Html.p "Homepage" + | Page.Playground data -> + let initialCode = + data + |> Option.map LzString.decompressFromEncodedURIComponent + |> Option.defaultValue "" + + Playground.Component model.Worker initialCode + | Page.NotFound -> Html.p "Not found" + + Toastify.container [ + ContainerOption.autoClose 2000 + ContainerOption.position Position.BottomRight + ContainerOption.theme Theme.Light + ] ] ] ] diff --git a/src/App/Documentation.fs b/src/App/Documentation.fs index c9da8a6..3f944a8 100644 --- a/src/App/Documentation.fs +++ b/src/App/Documentation.fs @@ -4,47 +4,31 @@ module Documentation open Feliz.Router open Thoth.Json -type Page = { +type Entry = { Title: string Route: string list MarkdownDocumentation: string + GitHubUrl: string } -type Category = { Title: string; Pages: Page list } +type Category = { Title: string; Entries: Entry list } type TableOfContents = { RootMarkdown: string + RootGitHubUrl: string Categories: Category list } +let makeGitHubUrl relativePath = + $"https://github.com/fsharpforyou/tour/tree/main/public/documentation/{relativePath}" + [] module TableOfContents = - let allPages tableOfContents = - tableOfContents.Categories |> List.collect _.Pages - - let toMarkdownString tableOfContents = - let bulletPoint value = $"* {value}" - let createMarkdownLink value url = $"[{value}]({url})" - - let createHeading level value = - let hashtags = String.replicate level "#" - $"{hashtags} {value}" - - let createUrlForPage page = Router.format page.Route - - let categoryToPageUrls category = - category.Pages - |> List.map (fun page -> page |> createUrlForPage |> createMarkdownLink page.Title |> bulletPoint) - - let createMarkdownForCategory category = - [ createHeading 2 category.Title ] @ categoryToPageUrls category - - [ createHeading 1 "Table of Contents" ] - @ List.collect createMarkdownForCategory tableOfContents.Categories - |> String.concat "\n" + let allEntries tableOfContents = + tableOfContents.Categories |> List.collect _.Entries module private Json = - type PageJson = { + type EntryJson = { Title: string RouteSegment: string MarkdownFile: string @@ -53,7 +37,7 @@ module private Json = type CategoryJson = { Title: string RouteSegment: string - Pages: PageJson list + Entries: EntryJson list } type TableOfContentsJson = { @@ -61,7 +45,7 @@ module private Json = Categories: CategoryJson list } - let pageDecoder = + let entryDecoder = Decode.object (fun get -> { Title = get.Required.Field "title" Decode.string RouteSegment = get.Required.Field "route_segment" Decode.string @@ -72,7 +56,7 @@ module private Json = Decode.object (fun get -> { Title = get.Required.Field "title" Decode.string RouteSegment = get.Required.Field "route_segment" Decode.string - Pages = get.Required.Field "pages" (Decode.list pageDecoder) + Entries = get.Required.Field "entries" (Decode.list entryDecoder) }) let tableOfContentsDecoder = @@ -81,33 +65,39 @@ module private Json = Categories = get.Required.Field "categories" (Decode.list categoryDecoder) }) -let emptyTableOfContents = { RootMarkdown = ""; Categories = [] } +let emptyTableOfContents = { + RootMarkdown = "" + RootGitHubUrl = "" + Categories = [] +} + let private documentationPath path = Constants.documentation + path let private fetchAsString path = Fetch.fetch path [] |> Promise.bind (fun res -> res.text ()) -let private loadPageFromJson (categoryJson: Json.CategoryJson) (pageJson: Json.PageJson) = +let private loadEntryFromJson (categoryJson: Json.CategoryJson) (entryJson: Json.EntryJson) = promise { - let! markdownDoc = fetchAsString (documentationPath pageJson.MarkdownFile) + let! markdownDoc = fetchAsString (documentationPath entryJson.MarkdownFile) return { - Title = pageJson.Title - Route = [ categoryJson.RouteSegment; pageJson.RouteSegment ] + Title = entryJson.Title + Route = [ categoryJson.RouteSegment; entryJson.RouteSegment ] MarkdownDocumentation = markdownDoc + GitHubUrl = makeGitHubUrl entryJson.MarkdownFile } } let private loadCategoryFromJson (categoryJson: Json.CategoryJson) = promise { - let! pages = - categoryJson.Pages - |> List.map (fun page -> loadPageFromJson categoryJson page) + let! entries = + categoryJson.Entries + |> List.map (fun entry -> loadEntryFromJson categoryJson entry) |> Promise.all return { Title = categoryJson.Title - Pages = Seq.toList pages + Entries = Seq.toList entries } } @@ -118,6 +108,7 @@ let private loadTableOfContentsFromJson (tableOfContentsJson: Json.TableOfConten return { RootMarkdown = rootMarkdown + RootGitHubUrl = makeGitHubUrl tableOfContentsJson.RootMarkdownFile Categories = Seq.toList categories } } diff --git a/src/App/Editor.fs b/src/App/Editor.fs index 6d0e59d..537d350 100644 --- a/src/App/Editor.fs +++ b/src/App/Editor.fs @@ -234,6 +234,7 @@ let createTooltipProvider getTooltip = jsOptions (fun h -> h.contents <- lines + |> Seq.distinct // I'm not sure why this is required but I'm getting duplicate entries in the tooltip |> Seq.map Tooltip.replaceXml |> Seq.mapi (fun i line -> {| diff --git a/src/App/Navigation.fs b/src/App/Navigation.fs index 5f1234a..97eb5f3 100644 --- a/src/App/Navigation.fs +++ b/src/App/Navigation.fs @@ -1,49 +1,59 @@ module Navigation +open Feliz.Router + [] type Page = | Homepage - | TableOfContents - | DocPage of Documentation.Page + | Docs + | DocsEntry of Documentation.Entry + | Playground of data: string option | NotFound [] module Page = - let fromUrl docPages url = + let fromUrl docEntries url = match url with | [] -> Page.Homepage - | [ "table-of-contents" ] -> Page.TableOfContents - | routeSegments -> - docPages - |> List.tryFind (fun (docPage: Documentation.Page) -> docPage.Route = routeSegments) - |> Option.map Page.DocPage - |> Option.defaultValue Page.NotFound + | [ "playground" ] -> Page.Playground None + | [ "playground"; Route.Query [ "data", dataUrl ] ] -> Page.Playground(Some dataUrl) + | "docs" :: routeSegments -> + match routeSegments with + | [] -> Page.Docs + | routeSegments -> + printfn "route segments %A" routeSegments + + docEntries + |> List.tryFind (fun (entry: Documentation.Entry) -> entry.Route = routeSegments) + |> Option.map Page.DocsEntry + |> Option.defaultValue Page.NotFound + | _ -> Page.NotFound + +type NavigationEntry = { Title: string; Route: string list } + +[] +module NavigationEntry = + let fromDocEntry (docEntry: Documentation.Entry) = { + Title = docEntry.Title + Route = docEntry.Route + } -type CurrentEntry = - | Entry of Documentation.Page - | NotViewingEntry type DocEntryNavigation = { - PreviousEntry: Documentation.Page option - NextEntry: Documentation.Page option + PreviousEntry: NavigationEntry option + NextEntry: NavigationEntry option } -let getDocEntryNavigation (currentEntry: CurrentEntry) (allEntries: Documentation.Page list) = - let previous, next = - match currentEntry with - | NotViewingEntry -> - let previous = None - let next = List.tryItem 0 allEntries +let getDocEntryNavigation (docPage: Documentation.Entry) (allEntries: Documentation.Entry list) = + let previousDocEntry, nextDocEntry = + match List.tryFindIndex (fun elem -> elem = docPage) allEntries with + | None -> None, None + | Some currentIndex -> + let previous = List.tryItem (currentIndex - 1) allEntries + let next = List.tryItem (currentIndex + 1) allEntries previous, next - | Entry entry -> - match List.tryFindIndex (fun elem -> elem = entry) allEntries with - | None -> None, None - | Some currentIndex -> - let previous = List.tryItem (currentIndex - 1) allEntries - let next = List.tryItem (currentIndex + 1) allEntries - previous, next { - PreviousEntry = previous - NextEntry = next + PreviousEntry = previousDocEntry |> Option.map NavigationEntry.fromDocEntry + NextEntry = nextDocEntry |> Option.map NavigationEntry.fromDocEntry } From 9aae6103945bda7bf6639be9058769990d599f0b Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Thu, 11 Jul 2024 12:37:42 -0500 Subject: [PATCH 12/21] Add null check in iframe logic --- src/App/Iframe.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/Iframe.fs b/src/App/Iframe.fs index 0ec509c..faf0850 100644 --- a/src/App/Iframe.fs +++ b/src/App/Iframe.fs @@ -20,7 +20,7 @@ let command (iframeId: string) (args: MessageArgs<'Msg>) = fun ev -> let iframeElement = document.getElementById iframeId - if ev?source = iframeElement?contentWindow then + if iframeElement <> null && ev?source = iframeElement?contentWindow then let iframeMessageDecoder = Decode.field "type" Decode.string |> Decode.option From 4ee74fd29a74669d0ccfde723277f26e699354af Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Thu, 11 Jul 2024 12:49:06 -0500 Subject: [PATCH 13/21] go back to a single worker per editor instance other version was buggy... i'm not sure how I didn't catch that. it will be refactored again at a later date. --- src/App/App.fs | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/App/App.fs b/src/App/App.fs index 4986817..80cc767 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -155,7 +155,7 @@ module EditorUtils = ConsoleWarn = fun text -> Msg.AddConsoleLog(text, LogLevel.Warn) ConsoleError = fun text -> Msg.AddConsoleLog(text, LogLevel.Error) } - Cmd.ofMsg Msg.ParseCode // NOTE: this may have terrible performance lol + Cmd.ofMsg Msg.ParseCode ] let compile model = @@ -220,10 +220,16 @@ module EditorUtils = }, debouncerCmd - let init worker initialFsharpCode = + let init initialFsharpCode = fun () -> let randomIdentifier = Guid.NewGuid().ToString() + let worker = + ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, randomIdentifier) + + CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) + |> worker.Post + let model = { Logs = [] FSharpCode = initialFsharpCode @@ -242,9 +248,9 @@ module EditorUtils = [] module Playground = [] - let Component worker initialFSharpCode = + let Component initialFSharpCode = let model, dispatch = - React.useElmish (EditorUtils.init worker initialFSharpCode, EditorUtils.update) + React.useElmish (EditorUtils.init initialFSharpCode, EditorUtils.update) Html.div [ Html.button [ @@ -298,9 +304,9 @@ module Playground = [] module DocumentationEditorInstance = [] - let Component worker initialFsharpCode = + let Component initialFsharpCode = let model, dispatch = - React.useElmish (EditorUtils.init worker initialFsharpCode, EditorUtils.update) + React.useElmish (EditorUtils.init initialFsharpCode, EditorUtils.update) Html.div [ Html.i [ @@ -365,7 +371,7 @@ module Documentation = let private formatDocRoute (route: string list) = Router.format ("docs" :: route) [] - let Component worker currentEntry tableOfContents = + let Component currentEntry tableOfContents = let allEntries = Documentation.TableOfContents.allEntries tableOfContents let markdownDocumentation, githubUrl, docEntryNavigation = @@ -432,7 +438,7 @@ module Documentation = // this is an interesting way to get the value of a code block props.children |> Seq.tryHead - |> Option.map (string >> DocumentationEditorInstance.Component worker) + |> Option.map (string >> DocumentationEditorInstance.Component) |> Option.defaultValue Html.none) ] ] @@ -472,7 +478,6 @@ module App = CurrentUrl: string list CurrentPage: Navigation.Page TableOfContents: Documentation.TableOfContents - Worker: ObservableWorker } [] @@ -485,20 +490,13 @@ module App = let init () = let tableOfContents = Documentation.emptyTableOfContents - let worker = ObservableWorker(WebWorker.create (), WorkerAnswer.Decoder, "MAIN APP") { CurrentUrl = [] CurrentPage = Page.Homepage TableOfContents = tableOfContents - Worker = worker }, - Cmd.batch [ - Cmd.OfPromise.perform Documentation.loadTableOfContents () Msg.FetchedTableOfContents - Cmd.ofEffect (fun _ -> - CreateChecker(Constants.metadata, [||], Some ".txt", fsharpOptions) - |> worker.Post) - ] + Cmd.OfPromise.perform Documentation.loadTableOfContents () Msg.FetchedTableOfContents let update msg model = match msg with @@ -545,13 +543,9 @@ module App = ] Html.main [ match model.CurrentPage with - | Page.Docs -> - Documentation.Component model.Worker Documentation.CurrentEntry.Root model.TableOfContents + | Page.Docs -> Documentation.Component Documentation.CurrentEntry.Root model.TableOfContents | Page.DocsEntry entry -> - Documentation.Component - model.Worker - (Documentation.CurrentEntry.Entry entry) - model.TableOfContents + Documentation.Component (Documentation.CurrentEntry.Entry entry) model.TableOfContents | Page.Homepage -> Html.p "Homepage" | Page.Playground data -> let initialCode = @@ -559,7 +553,7 @@ module App = |> Option.map LzString.decompressFromEncodedURIComponent |> Option.defaultValue "" - Playground.Component model.Worker initialCode + Playground.Component initialCode | Page.NotFound -> Html.p "Not found" Toastify.container [ From 6c845102a768b4e7a3532fd4001609cc84ba4fdc Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Tue, 6 Aug 2024 15:19:09 -0500 Subject: [PATCH 14/21] Misc changes 1. Don't use Monaco for non-F# code blocks 2. Remove PicoCSS (will be styling this ourselves) --- index.html | 1 - public/css/pico.min.css | 4 ---- src/App/App.fs | 4 ++-- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 public/css/pico.min.css diff --git a/index.html b/index.html index 3aefbee..f911ae8 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,6 @@ F# Tour - - -
+
diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..9c81fa7 --- /dev/null +++ b/styles.css @@ -0,0 +1,244 @@ +/* +https://blog.logrocket.com/css-utility-classes-library-extendable-styles/ +https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8 +*/ + +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap'); + +:root { + --background-color: #fff; + --header-background-color: #fff; + --header-border-color: #cccccc; + --code-background-color: #dddddd; + --text-color: #000; +} + +[data-theme="dark"] { + --background-color: #1e1e1e; + --header-background-color: #1e1e1e; + --header-border-color: #2d2d2d; + --code-background-color: #dddddd; + --text-color: #fff; +} + +iframe { + position: absolute; + width: 0; + height: 0; + border: hidden; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background-color: var(--background-color); +} + +a:visited { + color: inherit; +} + +/* h1, +h2, +h3, +h4, +h5, +h6, +p, +li, +strong, +small, +a { + color: var(--text-color); +} */ + +nav ul, +details ul { + list-style-type: none; +} + +.edit-this-page { + display: flex; + align-items: center; + column-gap: 5px; +} + +code { + --padding: 0.25em; + + border-radius: 0.25em; + background-color: var(--code-background-color); + padding-top: var(--padding); + padding-bottom: var(--padding); + padding-left: var(--padding); + padding-right: var(--padding); +} + +header { + width: 100%; + border-bottom: 1px solid var(--header-border-color); + background-color: var(--header-background-color); +} + +header nav a, +details ul li a { + display: block; + text-decoration: none; + color: inherit; + +} + +header nav { + display: flex; + justify-content: space-between; +} + +header nav ul { + display: flex; +} + +.branding>li { + display: flex; + flex-direction: row; + align-items: center; + column-gap: 5px; +} + +.branding>li>img { + height: 20px; + width: 20px; +} + +header nav li { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +header nav button { + background: none; + border: none; + /* border: 1px solid black; + border-radius: 0.25rem; */ +} + +/* button i { + font-size: 1.25em; +} */ + +.documentation { + display: grid; + grid-template-columns: 25% 75%; + grid-template-rows: 100%; + grid-template-areas: "sidebar markdown-content"; +} + +.documentation .sidebar { + grid-area: "sidebar"; + padding-left: 0.5em; + padding-top: 0.5em; + background-color: var(--header-background-color); + border-right: 1px solid var(--header-border-color); +} + +.documentation .sidebar li:hover { + background-color: #c3c3c3; +} + +.documentation .markdown-content { + grid-area: "markdown-content"; + padding-left: 25px; + padding-right: 25px; +} + +.documentation .markdown-content nav ul { + display: flex; + justify-content: space-between; +} + +.documentation .markdown-content nav ul li a { + text-decoration: none; +} + +.documentation .editor-group { + border: solid 1px black; +} + +.documentation .output-group .output-logs { + border-top: 1px solid black; + /* padding-left: 10px; */ +} + +.documentation .output-group .output-logs p { + padding-left: 10px; +} + +.documentation .output-group .output-logs .log-level-success { + border-left: 10px solid green; +} + +.documentation .output-group .output-logs .log-level-warning { + border-left: 10px solid yellow; +} + +.documentation .output-group .output-logs .log-level-error { + border-left: 10px solid red; +} + +.documentation .output-group .output-controls { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + border-top: 1px solid black; + background-color: white; + + padding-left: 1em; + padding-right: 1em; +} + +.documentation .output-group .output-controls div { + display: flex; + flex-direction: row; + align-items: center; + column-gap: 10px; +} + +/* .documentation .output-group .output-controls div button { + background: none; + border: none; + font-size: 1em; +} */ + +.documentation .output-group .output-controls div a { + /* text-decoration: none; */ + color: inherit; + font-size: 1em; +} + +.playground { + display: grid; + grid-template-columns: 5% 95%; + grid-template-rows: 100%; + grid-template-areas: "sidebar editor"; +} + +.playground .sidebar { + grid-area: "sidebar"; + display: flex; + flex-direction: column; + row-gap: 0.5em; + padding-top: 10px; + background-color: var(--header-background-color); + border-right: 1px solid var(--header-border-color); +} + +.playground .sidebar button { + background: none; + border: none; + /* border: 1px solid black; */ + /* border-radius: 0.25em; */ +} + +.playground .editor-group { + grid-area: "editor"; +} \ No newline at end of file From 9d463ae02f0392758a4c9c97949dfc90fcade2ca Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Sun, 29 Sep 2024 02:31:57 -0500 Subject: [PATCH 18/21] Improve code block rendering --- src/App/App.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App/App.fs b/src/App/App.fs index f25410c..4a4537c 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -464,8 +464,10 @@ module Documentation = markdown.components.pre (fun props -> React.fragment props.children) // This doesn't wrap our editor instance in a `pre` markdown.components.code (fun props -> - if props.isInline || props.className <> "language-fsharp" then + if props.isInline then Html.code props.children + elif props.className <> "language-fsharp" then + Html.pre [ Html.code props.children ] else // this is an interesting way to get the value of a code block props.children From 07a3ad2d338a434aeccc84e09dbce2b691b02ef0 Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Sun, 29 Sep 2024 06:13:05 -0500 Subject: [PATCH 19/21] Misc improvements to the output logs --- src/App/App.fs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/App/App.fs b/src/App/App.fs index 4a4537c..8836228 100644 --- a/src/App/App.fs +++ b/src/App/App.fs @@ -22,7 +22,6 @@ let fsharpOptions = [| "--langversion:preview" |] - let monacoEditorOptions = {| minimap = {| enabled = false |} fontSize = 16 @@ -327,23 +326,20 @@ module DocumentationEditorInstance = Html.div [ prop.className "output-group" prop.children [ - if isOutputExpanded then + if isOutputExpanded && not (List.isEmpty model.Logs) then Html.div [ prop.className "output-logs" prop.children [ - if List.isEmpty model.Logs then - Html.p "No output." - else - for (log, level) in model.Logs do - Html.p [ - prop.className ( - match level with - | EditorUtils.LogLevel.Log -> "log-level-success" - | EditorUtils.LogLevel.Warn -> "log-level-warning" - | EditorUtils.LogLevel.Error -> "log-level-error" - ) - prop.text log - ] + for (log, level) in model.Logs do + Html.p [ + prop.className ( + match level with + | EditorUtils.LogLevel.Log -> "log-level-success" + | EditorUtils.LogLevel.Warn -> "log-level-warning" + | EditorUtils.LogLevel.Error -> "log-level-error" + ) + prop.text log + ] ] ] else @@ -367,7 +363,9 @@ module DocumentationEditorInstance = Html.div [ Html.button [ prop.text "Compile" - prop.onClick (fun _ -> dispatch EditorUtils.Msg.Compile) + prop.onClick (fun _ -> + dispatch EditorUtils.Msg.Compile + setOutputExpanded true) ] Html.a [ prop.href (EditorUtils.createPlaygroundUrl model.FSharpCode) From e767e6d9a09089d6f68fccb8d03e6f1b1f8d79cc Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 30 Sep 2024 14:25:08 -0500 Subject: [PATCH 20/21] a bit more styling --- styles.css | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/styles.css b/styles.css index 9c81fa7..ddac53a 100644 --- a/styles.css +++ b/styles.css @@ -8,16 +8,16 @@ https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34 :root { --background-color: #fff; --header-background-color: #fff; - --header-border-color: #cccccc; - --code-background-color: #dddddd; + --border-color: #cccccc; + --code-block-background-color: #dddddd; --text-color: #000; } [data-theme="dark"] { --background-color: #1e1e1e; --header-background-color: #1e1e1e; - --header-border-color: #2d2d2d; - --code-background-color: #dddddd; + --border-color: #2d2d2d; + --code-block-background-color: #dddddd; --text-color: #fff; } @@ -62,20 +62,25 @@ details ul { column-gap: 5px; } +pre, code { --padding: 0.25em; border-radius: 0.25em; - background-color: var(--code-background-color); + background-color: var(--code-block-background-color); padding-top: var(--padding); padding-bottom: var(--padding); padding-left: var(--padding); padding-right: var(--padding); } +pre>code { + all: unset; +} + header { width: 100%; - border-bottom: 1px solid var(--header-border-color); + border-bottom: 1px solid var(--border-color); background-color: var(--header-background-color); } @@ -136,7 +141,7 @@ header nav button { padding-left: 0.5em; padding-top: 0.5em; background-color: var(--header-background-color); - border-right: 1px solid var(--header-border-color); + border-right: 1px solid var(--border-color); } .documentation .sidebar li:hover { @@ -159,11 +164,11 @@ header nav button { } .documentation .editor-group { - border: solid 1px black; + border: solid 1px var(--border-color); } .documentation .output-group .output-logs { - border-top: 1px solid black; + border-top: 1px solid var(--border-color); /* padding-left: 10px; */ } @@ -189,8 +194,8 @@ header nav button { justify-content: space-between; align-items: center; - border-top: 1px solid black; - background-color: white; + border-top: 1px solid var(--border-color); + /* background-color: white; */ padding-left: 1em; padding-right: 1em; @@ -229,7 +234,7 @@ header nav button { row-gap: 0.5em; padding-top: 10px; background-color: var(--header-background-color); - border-right: 1px solid var(--header-border-color); + border-right: 1px solid var(--border-color); } .playground .sidebar button { From 8908b63c0a8b3b4eb79da294b3b6cd9271c7891e Mon Sep 17 00:00:00 2001 From: sheridanchris Date: Mon, 30 Sep 2024 18:02:35 -0500 Subject: [PATCH 21/21] start reworking the documentation --- .../basics/conditional-expressions.fsx | 9 --- .../basics/conditional-expressions.md | 2 + public/documentation/basics/expressions.fsx | 5 -- public/documentation/basics/expressions.md | 13 ++-- public/documentation/basics/hello-world.fsx | 1 - public/documentation/basics/hello-world.md | 6 +- public/documentation/basics/operators.fsx | 17 ----- .../basics/patterns-and-match-expressions.fsx | 10 --- .../basics/patterns-and-match-expressions.md | 8 ++- .../documentation/basics/primitive-types.fsx | 6 -- .../documentation/basics/primitive-types.md | 12 ++-- .../basics/string-formatting.fsx | 3 - .../documentation/basics/string-formatting.md | 9 +-- .../data-and-types/abbreviations.fsx | 6 -- .../data-and-types/abbreviations.md | 2 + .../data-and-types/discriminated-unions.fsx | 11 ---- .../data-and-types/discriminated-unions.md | 46 ++++++++++++-- .../data-and-types/generic-types.fsx | 3 - .../data-and-types/generic-types.md | 17 ++++- public/documentation/data-and-types/lists.fsx | 3 - public/documentation/data-and-types/lists.md | 33 +++++----- .../documentation/data-and-types/records.fsx | 7 --- .../documentation/data-and-types/records.md | 38 +++++++++++- .../data-and-types/sequence-expressions.fsx | 5 -- .../data-and-types/sequences.fsx | 1 - .../data-and-types/the-option-type.fsx | 8 --- .../data-and-types/the-result-type.fsx | 38 ------------ .../documentation/data-and-types/tuples.fsx | 2 - .../data-and-types/units-of-measure.fsx | 16 ----- .../currying-and-partial-application.fsx | 9 --- .../functions/defining-functions.fsx | 3 - .../functions/functions-as-values.fsx | 8 --- .../functions/functions-as-values.md | 27 ++++++-- .../functions/pipelines-and-composition.fsx | 11 ---- .../functions/pipelines-and-composition.md | 62 +++++++++++++------ .../functions/recursive-functions.fsx | 4 -- .../functions/recursive-functions.md | 5 +- .../abstract-classes-and-inheritance.fsx | 13 ---- .../object-programming/interfaces.fsx | 18 ------ .../object-programming/object-expressions.fsx | 9 --- .../objects-and-members.fsx | 12 ---- .../object-programming/type-extensions.fsx | 11 ---- 42 files changed, 209 insertions(+), 320 deletions(-) delete mode 100644 public/documentation/basics/conditional-expressions.fsx delete mode 100644 public/documentation/basics/expressions.fsx delete mode 100644 public/documentation/basics/hello-world.fsx delete mode 100644 public/documentation/basics/operators.fsx delete mode 100644 public/documentation/basics/patterns-and-match-expressions.fsx delete mode 100644 public/documentation/basics/primitive-types.fsx delete mode 100644 public/documentation/basics/string-formatting.fsx delete mode 100644 public/documentation/data-and-types/abbreviations.fsx delete mode 100644 public/documentation/data-and-types/discriminated-unions.fsx delete mode 100644 public/documentation/data-and-types/generic-types.fsx delete mode 100644 public/documentation/data-and-types/lists.fsx delete mode 100644 public/documentation/data-and-types/records.fsx delete mode 100644 public/documentation/data-and-types/sequence-expressions.fsx delete mode 100644 public/documentation/data-and-types/sequences.fsx delete mode 100644 public/documentation/data-and-types/the-option-type.fsx delete mode 100644 public/documentation/data-and-types/the-result-type.fsx delete mode 100644 public/documentation/data-and-types/tuples.fsx delete mode 100644 public/documentation/data-and-types/units-of-measure.fsx delete mode 100644 public/documentation/functions/currying-and-partial-application.fsx delete mode 100644 public/documentation/functions/defining-functions.fsx delete mode 100644 public/documentation/functions/functions-as-values.fsx delete mode 100644 public/documentation/functions/pipelines-and-composition.fsx delete mode 100644 public/documentation/functions/recursive-functions.fsx delete mode 100644 public/documentation/object-programming/abstract-classes-and-inheritance.fsx delete mode 100644 public/documentation/object-programming/interfaces.fsx delete mode 100644 public/documentation/object-programming/object-expressions.fsx delete mode 100644 public/documentation/object-programming/objects-and-members.fsx delete mode 100644 public/documentation/object-programming/type-extensions.fsx diff --git a/public/documentation/basics/conditional-expressions.fsx b/public/documentation/basics/conditional-expressions.fsx deleted file mode 100644 index 87a5921..0000000 --- a/public/documentation/basics/conditional-expressions.fsx +++ /dev/null @@ -1,9 +0,0 @@ -let number = 20 - -let fizzBuzz = - if number % 15 = 0 then "FizzBuzz" - elif number % 5 = 0 then "Buzz" - elif number % 3 = 0 then "Fizz" - else string number - -printfn "%s" fizzBuzz diff --git a/public/documentation/basics/conditional-expressions.md b/public/documentation/basics/conditional-expressions.md index a32d03d..5fa60f1 100644 --- a/public/documentation/basics/conditional-expressions.md +++ b/public/documentation/basics/conditional-expressions.md @@ -18,4 +18,6 @@ let fizzBuzz = elif number % 5 = 0 then "Buzz" elif number % 3 = 0 then "Fizz" else string number + +printfn "%s" fizzBuzz ``` diff --git a/public/documentation/basics/expressions.fsx b/public/documentation/basics/expressions.fsx deleted file mode 100644 index 51b8801..0000000 --- a/public/documentation/basics/expressions.fsx +++ /dev/null @@ -1,5 +0,0 @@ -let ten = - let five = 5 - five + five - -let twenty = ten + ten diff --git a/public/documentation/basics/expressions.md b/public/documentation/basics/expressions.md index 7c7e6b0..03d0b16 100644 --- a/public/documentation/basics/expressions.md +++ b/public/documentation/basics/expressions.md @@ -1,6 +1,6 @@ # Expressions -The primary piece of F# syntax is an expression. An expression is simply, a block of code that when evaluated, produces a value. +The most fundamental construct in F# is an expression. An expression is a block of code that produces a value when evaluated. _Let bindings_ allow you to bind the result of an expression to a name. @@ -34,14 +34,11 @@ let ten = 10 The advantage of top-down evaluation is that the flow of your application's code is easier to reason about. Code is read and evaluated from top to bottom sequentially, from a higher to a lower level, from core components to specific implementation details. We can clearly understand and reason about our dependent modules, types, functions and their dependencies. -This top-down evaluation also applies to the ordering of files within a project. You can only use functions, types, and modules defined in other files if the file is ordered above the current definition. +This top-down evaluation also applies to the ordering of files within a project. You can only use functions, types, and modules defined in other files if the file is ordered above the current definition. Assuming you define a project with the structure of: + 1. Logic.fs + 2. Program.fs -``` -1. Logic.fs -2. Program.fs -``` - -Here, any code in `Program.fs` can access modules, types, functions, and bindings in `Logic.fs`, but not the other way around. This is because `Logic.fs` is ordered above `Program.fs`. +Any code in `Program.fs` can access modules, types, functions, and bindings in `Logic.fs`, but not the other way around. This is because `Logic.fs` is ordered above `Program.fs`. F# relies on the level of indentation to determine the beginning and end of an expression. You may be familiar with this if you've programmed in languages with syntatic indentation before. When writing an expression block, the level of indentation must be consistent for each expression within that block. diff --git a/public/documentation/basics/hello-world.fsx b/public/documentation/basics/hello-world.fsx deleted file mode 100644 index 70fa638..0000000 --- a/public/documentation/basics/hello-world.fsx +++ /dev/null @@ -1 +0,0 @@ -printfn "Hello, World!" diff --git a/public/documentation/basics/hello-world.md b/public/documentation/basics/hello-world.md index f27c004..0cbec8d 100644 --- a/public/documentation/basics/hello-world.md +++ b/public/documentation/basics/hello-world.md @@ -2,6 +2,10 @@ Here is a program that prints out the text `"Hello, World!"`. +```fsharp +printfn "Hello, World!" +``` + In a normal F# program, this would be executed by using the command `dotnet run` on the command line. -Try changing the text being printed to `"Hello!"` and click the `Run` button at the top right to see what happens. \ No newline at end of file +Try changing the text being printed and click the `Compile` button to see what happens! \ No newline at end of file diff --git a/public/documentation/basics/operators.fsx b/public/documentation/basics/operators.fsx deleted file mode 100644 index 4431b57..0000000 --- a/public/documentation/basics/operators.fsx +++ /dev/null @@ -1,17 +0,0 @@ -let ten = 5 + 5 -printfn "5 + 5 = %d" ten - -let four = 8 - 4 -printfn "8 - 4 = %d" four - -let five = 10 / 2 -printfn "10 / 2 = %d" five - -let twenty = 10 * 2 -printfn "10 * 2 = %d" twenty - -let remainder = 10 % 2 -printfn "10 %% 2 = %d" remainder - -let squared = 10.0 ** 2.0 -printfn "10.0 ** 2.0 = %f" squared diff --git a/public/documentation/basics/patterns-and-match-expressions.fsx b/public/documentation/basics/patterns-and-match-expressions.fsx deleted file mode 100644 index 010b11d..0000000 --- a/public/documentation/basics/patterns-and-match-expressions.fsx +++ /dev/null @@ -1,10 +0,0 @@ -let number = 15 - -let fizzBuzz = - match number with - | number when number % 15 = 0 -> "FizzBuzz" - | number when number % 5 = 0 -> "Buzz" - | number when number % 3 = 0 -> "Fizz" - | number -> string number - -printfn "%s" fizzBuzz diff --git a/public/documentation/basics/patterns-and-match-expressions.md b/public/documentation/basics/patterns-and-match-expressions.md index 8b82da4..2539fbb 100644 --- a/public/documentation/basics/patterns-and-match-expressions.md +++ b/public/documentation/basics/patterns-and-match-expressions.md @@ -4,7 +4,7 @@ Pattern matching allows you to match a value against patterns, which act as rule To demonstrate pattern matching, let's get started with a pattern you're already familiar with: the _variable_ pattern. This pattern allows you to bind a value to a name like so: `let five = 5`. That's right, you've been using the variable pattern the whole time! Just like how everything on the right-hand side of the equals sign in a binding is an expression, the left-hand side is always a pattern. -```fsharp +``` let = ``` @@ -16,6 +16,8 @@ let result = match number with | 10 -> "The number is Ten" | number -> $"The number is not ten, but instead: {number}" + +printfn "%s" result ``` Here you can see two patterns in action: the _constant_ and _variable_ patterns. The _constant_ pattern will match a value against a constant value like `10` or `"Hello, World!"`. This match expression is exhaustive as the last branch utilizes the _variable_ pattern which will always match against the value. @@ -34,16 +36,20 @@ A branch in a match expression can also include a conditional expression. This i ```fsharp let number = 10 + let result = match number with | number when number % 2 = 0 -> $"{number} is even" | number -> $"{number} is odd" + +printfn "%s" result ``` Two patterns can lead to the same expression being evaluated using the _OR_ pattern. ```fsharp let number = 10 + let result = match number with | pattern1 diff --git a/public/documentation/basics/primitive-types.fsx b/public/documentation/basics/primitive-types.fsx deleted file mode 100644 index e9f5567..0000000 --- a/public/documentation/basics/primitive-types.fsx +++ /dev/null @@ -1,6 +0,0 @@ -let int = 10 -let float = 10.0 -let char = 'a' -let string = "Hello, World!" -let boolean = true -let unit = () diff --git a/public/documentation/basics/primitive-types.md b/public/documentation/basics/primitive-types.md index 15c3407..35bf713 100644 --- a/public/documentation/basics/primitive-types.md +++ b/public/documentation/basics/primitive-types.md @@ -13,10 +13,10 @@ What do these primitive types represent? - `unit`, when passed to a function, means that function has no arguments. When returned from a function, it indicates that the function has no useful return value. Often used when a function e.g. prints to the screen and does nothing else. ```fsharp -10 // int -10.0 // float -'a' // char -"Hello, World!" // string -true // bool -() // unit +let intValue: int = 10 +let floatValue: float = 10.0 +let charValue: char = 'a' +let stringValue: string = "Hello, World!" +let boolValue: bool = true +let unitValue: unit = () ``` diff --git a/public/documentation/basics/string-formatting.fsx b/public/documentation/basics/string-formatting.fsx deleted file mode 100644 index 6fdba51..0000000 --- a/public/documentation/basics/string-formatting.fsx +++ /dev/null @@ -1,3 +0,0 @@ -let name = "John Doe" -let greeting = $"Hello, {name}!" -printfn "%s" greeting diff --git a/public/documentation/basics/string-formatting.md b/public/documentation/basics/string-formatting.md index 2ed04e6..80a4f15 100644 --- a/public/documentation/basics/string-formatting.md +++ b/public/documentation/basics/string-formatting.md @@ -4,7 +4,8 @@ String formatting is the process of integrating additional values into string li ```fsharp let name = "John Doe" -sprintf "Your name is %s" name // "Your name is John Doe" +let nameDescription = sprintf "Your name is %s" name +printfn "%s" nameDescription ``` Here, we specify that the string format contains a single string value, indicated by the `%s` format specifier. @@ -26,12 +27,12 @@ Another method is to use interpolated strings which allow you to bake the values ```fsharp let name = "John Doe" -$"Your name is {name}" // "Your name is John Doe" +printfn $"Your name is {name}" ``` These interpolated strings can also be type checked by providing a format before the template. This will result in a compiler error if the type of the value doesn't match the format specifier. ```fsharp -$"Your name is %s{name}" // this works. -$"Your name is %b{name}" // compiler error. %b = boolean +printfn $"Your name is %s{name}" // this works. +printfn $"Your name is %b{name}" // compiler error. %b = boolean ``` \ No newline at end of file diff --git a/public/documentation/data-and-types/abbreviations.fsx b/public/documentation/data-and-types/abbreviations.fsx deleted file mode 100644 index 7ba4947..0000000 --- a/public/documentation/data-and-types/abbreviations.fsx +++ /dev/null @@ -1,6 +0,0 @@ -type Logger = string -> unit - -let exclaim (logger: Logger) (value: string) = logger (sprintf "%s!!!" value) - -let logger: Logger = printfn "%s" -exclaim logger "Hello" diff --git a/public/documentation/data-and-types/abbreviations.md b/public/documentation/data-and-types/abbreviations.md index 4983b63..54476ab 100644 --- a/public/documentation/data-and-types/abbreviations.md +++ b/public/documentation/data-and-types/abbreviations.md @@ -9,6 +9,8 @@ type Logger = string -> unit Because the `Logger` type is just an abbreviation for the function signature `string -> unit` you can use the two interchangeably. ```fsharp +type Logger = string -> unit + let exclaim (logger: Logger) (value: string) = logger (sprintf "%s!!!" value) let logger: Logger = printfn "%s" diff --git a/public/documentation/data-and-types/discriminated-unions.fsx b/public/documentation/data-and-types/discriminated-unions.fsx deleted file mode 100644 index e6ce142..0000000 --- a/public/documentation/data-and-types/discriminated-unions.fsx +++ /dev/null @@ -1,11 +0,0 @@ -type ContactInfo = - | EmailAddress of string - | PhoneNumber of string - -let contact contactInfo = - match contactInfo with - | EmailAddress email -> sprintf "Sending an email to %s" email - | PhoneNumber number -> sprintf "Sending a text message to %s" number - -let contactInfo = PhoneNumber "000-000-0000" -printfn "%s" (contact contactInfo) diff --git a/public/documentation/data-and-types/discriminated-unions.md b/public/documentation/data-and-types/discriminated-unions.md index df114dc..d4b94db 100644 --- a/public/documentation/data-and-types/discriminated-unions.md +++ b/public/documentation/data-and-types/discriminated-unions.md @@ -7,35 +7,57 @@ type Color = | Red | Green | Blue - | RGB of int * int * int + | Rgb of int * int * int ``` The discriminated union defined above has four cases: `Red`, `Green`, `Blue`, and `Rgb`. Only the `RGB` case has data associated with it. You can construct instances of these cases using the identifier and any data. ```fsharp +type Color = + | Red + | Green + | Blue + | Rgb of int * int * int + let red = Red -let black = RGB (0, 0, 0) +let black = Rgb (0, 0, 0) +printfn "%A" black ``` The case constructor for the `RGB` case is a function with the signature of `int * int * int -> Color`. ```fsharp -let rgb: int * int * int -> Color = RGB +type Color = + | Red + | Green + | Blue + | Rgb of int * int * int + +let rgb: int * int * int -> Color = Rgb +let color: Color = rgb (255, 255, 255) +printfn "%A" color ``` You can match against the cases of a discriminated union by using the _identifier_ pattern. The _identifier_ pattern allows you to match against the case by its identifier and additionally, supply a pattern for any data associated with it. ```fsharp +type Color = + | Red + | Green + | Blue + | Rgb of int * int * int + let rgb color = match color with | Red -> 255, 0, 0 | Green -> 0, 255, 0 | Blue -> 0, 0, 255 - | RGB (r, g, b) -> r, g, b + | Rgb (r, g, b) -> r, g, b let color = Red let (r, g, b) = rgb color +printfn "R: %d, G: %d, B: %d" r g b ``` When dealing with a case that has data in the form of a tuple, it can be difficult to discern which tuple value corresponds to which piece of the data. In these cases, it is good practice to include labels on tuple elements like so: @@ -48,4 +70,20 @@ type Color = | Rgb of r: int * g: int * b: int let color = Rgb (r = 255, g = 255, b = 255) +printfn "%A" color +``` + +Another common option is to use a record for a union case with multiple fields. + +```fsharp +type RgbColor = { R: int; G: int; B: int } + +type Color = + | Red + | Green + | Blue + | Rgb of RgbColor + +let color = Rgb { R = 255; G = 255; B = 255 } +printfn "%A" color ``` \ No newline at end of file diff --git a/public/documentation/data-and-types/generic-types.fsx b/public/documentation/data-and-types/generic-types.fsx deleted file mode 100644 index 5b54a61..0000000 --- a/public/documentation/data-and-types/generic-types.fsx +++ /dev/null @@ -1,3 +0,0 @@ -type Data<'a> = { Value: 'a } -let data: Data = { Value = "Hello, World!" } -printfn "%s" data.Value diff --git a/public/documentation/data-and-types/generic-types.md b/public/documentation/data-and-types/generic-types.md index 76dd14e..1869a02 100644 --- a/public/documentation/data-and-types/generic-types.md +++ b/public/documentation/data-and-types/generic-types.md @@ -6,7 +6,7 @@ For example, you can have an `int list`, a `string list`, or a `float list`. Eac You can define a generic type parameter using an apostrophe followed by the name of the type parameter. -```fsharp +``` type Data<'a> = { Value: 'a } ``` @@ -15,13 +15,24 @@ The `'a` in the above definition denotes a generic type parameter. If you wanted The `Value` property in the Data type can only contain a value of type `'a`. For `Data` the `Value` property must contain a `string` value. ```fsharp +type Data<'a> = { Value: 'a } + let data: Data = { Value = "Hello, World!" } let value: string = data.Value +printfn "%s" value + +let data1: Data = { Value = 10 } +let value1: int = data1.Value +printfn "%d" value1 ``` You can also define generic type parameters in functions and pass them to your desired type. Here we can accept a value of our generic `Data` type, passing a generic type parameter to it in the process. ```fsharp -let printData (data: Data<'a>) = - printfn "%A" data.Value +type Data<'a> = { Value: 'a } + +let printData (data: Data<'a>) = printfn "%A" data.Value + +printData { Value = "Hello, World!" } +printData { Value = 10 } ``` \ No newline at end of file diff --git a/public/documentation/data-and-types/lists.fsx b/public/documentation/data-and-types/lists.fsx deleted file mode 100644 index 75b43d9..0000000 --- a/public/documentation/data-and-types/lists.fsx +++ /dev/null @@ -1,3 +0,0 @@ -let numbers = [ 1; 2; 3 ] -let numbersAsStrings = List.map string numbers -printfn "%A" numbersAsStrings diff --git a/public/documentation/data-and-types/lists.md b/public/documentation/data-and-types/lists.md index f1d3b7a..6e30ea3 100644 --- a/public/documentation/data-and-types/lists.md +++ b/public/documentation/data-and-types/lists.md @@ -4,21 +4,25 @@ In F#, lists are an immutable series of elements of the same type implemented as ```fsharp let numbers = [ 1; 2; 3 ] +printfn "%A" numbers ``` There are two primary ways to add values to a list: You can prepend elements using the `::` operator, and concatenate two lists using the `@` operator. ```fsharp +let numbers = [ 1; 2; 3 ] let numbers2 = 0 :: numbers let numbers3 = numbers2 @ [4; 5; 6] +printfn "%A" numbers3 ``` Each list has a `head` and a `tail`. The `head` is the first element of the list, and the `tail` is every subsequence element. ```fsharp let numbers = [1; 2; 3] -let head = List.head numbers // 1 -let tail = List.tail numbers // [2; 3] +let head = List.head numbers +let tail = List.tail numbers +printfn "Head = %A, Tail = %A" head tail ``` There are two patterns that allow us to match against and deconstruct list values. The _list_ pattern and the _cons_ pattern. @@ -26,11 +30,11 @@ There are two patterns that allow us to match against and deconstruct list value The _list_ pattern allows you to supply a pattern for each value in a list. ```fsharp -let numbers = [1; 2; 3] +let numbers = [1] match numbers with -| [] -> "The list is empty" -| [a] -> $"The list has one element: {a}" -| ... -> ... +| [] -> printfn "The list is empty" +| [a] -> printfn $"The list has one element: {a}" +| _ -> printfn "???" ``` The _cons_ pattern allows you to deconstruct a list into N elements and the tail. @@ -38,8 +42,8 @@ The _cons_ pattern allows you to deconstruct a list into N elements and the tail ```fsharp let numbers = [1; 2; 3] match numbers with -| [] -> "The list is empty" -| head :: tail -> $"Head: {head}, Tail: {tail}" +| [] -> printfn "The list is empty" +| head :: tail -> printfn $"Head: {head}, Tail: {tail}" ``` The `head :: tail` pattern will deconstruct the list `[1; 2; 3]` into `head = 1` and `tail = [2; 3]`. This can also be done for N number of elements: `first :: second :: tail`. The `head :: tail` pattern will match against any list with a single element. While the `first :: second :: tail` pattern will match against any list with at least two elements, and so on. @@ -53,6 +57,8 @@ let rec iter (f: 'a -> unit) (xs: 'a list) = | x :: xs -> f x iter f xs + +iter (printfn "%d") [1; 2; 3] ``` The `List` module contains common functions for operating with lists. These functions include but are not limited to: @@ -62,11 +68,10 @@ The `List` module contains common functions for operating with lists. These func ```fsharp let isEven x = x % 2 = 0 -let numbers = [0; 1; 2; 3; 4; 5;] +let square x = x * x -let evenNumbersAsStrings = - numbers // [0; 1; 2; 3; 4; 5;] - |> List.filter isEven // [0; 2; 4] - |> List.map string // ["0"; "2"; "4"] - |> List.iter (printfn "%s") +[0; 1; 2; 3; 4; 5;] +|> List.filter isEven // only even numbers +|> List.map square // square every number +|> List.iter (printfn "%d") // print every number ``` \ No newline at end of file diff --git a/public/documentation/data-and-types/records.fsx b/public/documentation/data-and-types/records.fsx deleted file mode 100644 index d9bb38f..0000000 --- a/public/documentation/data-and-types/records.fsx +++ /dev/null @@ -1,7 +0,0 @@ -type Person = { FirstName: string; LastName: string } - -let johnDoe = { FirstName = "John"; LastName = "Doe" } -printfn "%A" johnDoe - -let janeDoe = { johnDoe with FirstName = "Jane" } -printfn "%A" janeDoe diff --git a/public/documentation/data-and-types/records.md b/public/documentation/data-and-types/records.md index 743f673..e1ff05a 100644 --- a/public/documentation/data-and-types/records.md +++ b/public/documentation/data-and-types/records.md @@ -33,9 +33,17 @@ As F# is evaluated from top to bottom, the instance of a record value will be in type Person = { FirstName: string; LastName: string } type Customer = { FirstName: string; LastName: string } -let johnDoe = { FirstName = "John"; LastName = "Doe" } // Customer -let johnDoe2: Person = { FirstName: string; LastName: string } // Person -let johnDoe3 = { Person.FirstName = "John"; Person.LastName = "Doe" } // Person +let displayPerson (person: Person) = printfn "Person = %A" person +let displayCustomer (customer: Customer) = printfn "Customer = %A" customer + +let johnDoe = { FirstName = "John"; LastName = "Doe" } +displayCustomer johnDoe + +let johnDoe2: Person = { FirstName = "John"; LastName = "Doe" } +displayPerson johnDoe2 + +let johnDoe3 = { Person.FirstName = "John"; Person.LastName = "Doe" } +displayPerson johnDoe3 ``` You can pattern match a record value using the _record pattern_. This pattern allows you to specify a pattern for one or more properties of a record. @@ -48,4 +56,28 @@ let identify person = | { FirstName = "John"; LastName = "Doe" } | { FirstName = "Jane"; LastName = "Doe" } -> "Could not identify this person." | { FirstName = firstName; LastName = lastName } -> $"Identified as: {firstName} {lastName}" +``` + +```fsharp +type Person = { FirstName: string; LastName: string; Age: int } +type Team = { Name: string; Members: Person list } + +let team = { + Name = "Developers"; + Members = [ + { FirstName = "John"; LastName = "Doe"; Age = 30 } + { FirstName = "Jane"; LastName = "Smith"; Age = 28 } + ] +} + +let describeTeam team = + let memberDescriptions = + team.Members + |> List.map (fun person -> $"{person.FirstName} {person.LastName} ({person.Age} years old)") + |> String.concat ", " + + $"Team: {team.Name}, Members: {memberDescriptions}" + +let teamDescription = describeTeam team +printfn "%s" teamDescription ``` \ No newline at end of file diff --git a/public/documentation/data-and-types/sequence-expressions.fsx b/public/documentation/data-and-types/sequence-expressions.fsx deleted file mode 100644 index 569261e..0000000 --- a/public/documentation/data-and-types/sequence-expressions.fsx +++ /dev/null @@ -1,5 +0,0 @@ -let oneThroughTen = [ 1..10 ] -printfn "%A" oneThroughTen - -let evenNumbers = [ 1..2..10 ] -printfn "%A" evenNumbers diff --git a/public/documentation/data-and-types/sequences.fsx b/public/documentation/data-and-types/sequences.fsx deleted file mode 100644 index 4b06086..0000000 --- a/public/documentation/data-and-types/sequences.fsx +++ /dev/null @@ -1 +0,0 @@ -Seq.initInfinite (fun x -> x * 2) |> Seq.take 10 |> Seq.iter (printfn "%d") diff --git a/public/documentation/data-and-types/the-option-type.fsx b/public/documentation/data-and-types/the-option-type.fsx deleted file mode 100644 index 50c92d9..0000000 --- a/public/documentation/data-and-types/the-option-type.fsx +++ /dev/null @@ -1,8 +0,0 @@ -type User = { Id: int; Name: string } - -let tryFindUserById id users = - List.tryFind (fun user -> user.Id = id) users - -let users = [ { Id = 1; Name = "John Doe" }; { Id = 2; Name = "Jane Doe" } ] -let foundUser = tryFindUserById 1 users -printfn "Found user: %A" foundUser diff --git a/public/documentation/data-and-types/the-result-type.fsx b/public/documentation/data-and-types/the-result-type.fsx deleted file mode 100644 index 9fd8b8e..0000000 --- a/public/documentation/data-and-types/the-result-type.fsx +++ /dev/null @@ -1,38 +0,0 @@ -type Account = - { Username: string - EmailAddress: string } - -type CreateAccountError = - | UsernameNotAvailable - | EmailAddressNotAvailable - -let ensureUsernameIsAvailable existingAccounts account = - if List.exists (fun x -> x.Username = account.Username) existingAccounts then - Error UsernameNotAvailable - else - Ok account - -let ensureEmailAddressIsAvailable existingAccounts account = - if List.exists (fun x -> x.EmailAddress = account.EmailAddress) existingAccounts then - Error EmailAddressNotAvailable - else - Ok account - -let createAccount existingAccounts account = - account - |> ensureUsernameIsAvailable existingAccounts - |> Result.bind (ensureEmailAddressIsAvailable existingAccounts) - -let existingAcounts = - [ { Username = "john" - EmailAddress = "johndoe@site.com" } - { Username = "jane" - EmailAddress = "janedoe@site.com" } ] - -let accountToCreate = - { Username = "chris" - EmailAddress = "chris@site.com" } - -match createAccount existingAcounts accountToCreate with -| Ok account -> printfn "Created account: %A" account -| Error error -> printfn "Failed to create account: %A" error diff --git a/public/documentation/data-and-types/tuples.fsx b/public/documentation/data-and-types/tuples.fsx deleted file mode 100644 index 82031ac..0000000 --- a/public/documentation/data-and-types/tuples.fsx +++ /dev/null @@ -1,2 +0,0 @@ -let (firstName, lastName) = ("John", "Doe") -printfn "Hello %s %s!" firstName lastName diff --git a/public/documentation/data-and-types/units-of-measure.fsx b/public/documentation/data-and-types/units-of-measure.fsx deleted file mode 100644 index 697cd1d..0000000 --- a/public/documentation/data-and-types/units-of-measure.fsx +++ /dev/null @@ -1,16 +0,0 @@ -[] -type mile - -[] -type kilometer - -let miles = 10 -let kilometers = 10 - -let calculateDistanceBetween (startMile: int) (endMile: int) = endMile - startMile - - -let startMile = 10 -let endMile = 20 -let distance = calculateDistanceBetween startMile endMile -printfn "Distance = %d" distance diff --git a/public/documentation/functions/currying-and-partial-application.fsx b/public/documentation/functions/currying-and-partial-application.fsx deleted file mode 100644 index 1a52c90..0000000 --- a/public/documentation/functions/currying-and-partial-application.fsx +++ /dev/null @@ -1,9 +0,0 @@ -let add x y = x + y - -let addFive = add 5 // partially applied -let fifteen = addFive 10 -printfn "%d" fifteen - -let criticalOperation (logger: string -> unit) (value: string) = logger $"{value}!!!" -let criticalOperationWithConsoleLogger = criticalOperation (printfn "%s") -criticalOperationWithConsoleLogger "Hello, World!" diff --git a/public/documentation/functions/defining-functions.fsx b/public/documentation/functions/defining-functions.fsx deleted file mode 100644 index 3599863..0000000 --- a/public/documentation/functions/defining-functions.fsx +++ /dev/null @@ -1,3 +0,0 @@ -let add x y = x + y -let ten = add 5 5 -printfn "%d" ten diff --git a/public/documentation/functions/functions-as-values.fsx b/public/documentation/functions/functions-as-values.fsx deleted file mode 100644 index 5b801a8..0000000 --- a/public/documentation/functions/functions-as-values.fsx +++ /dev/null @@ -1,8 +0,0 @@ -let double x = x * 2 -let apply (f: int -> int) = f 5 - -let ten = apply double -printfn "5 * 2 = %d" ten - -let twentyFive = apply (fun value -> value * 5) -printfn "5 * 5 = %d" twentyFive diff --git a/public/documentation/functions/functions-as-values.md b/public/documentation/functions/functions-as-values.md index 4a51e2f..cf3710f 100644 --- a/public/documentation/functions/functions-as-values.md +++ b/public/documentation/functions/functions-as-values.md @@ -2,20 +2,37 @@ In F#, all functions are values and can be passed around as such. These are called _higher order functions_ or functions that accept other functions as parameters. -Let's define a `double` function which has a single parameter. +Let's define a `double` and `half` function which has a single argument. ```fsharp let double x = x * 2 +let half x = x / 2 + +let fifty = double 25 +let twentyFive = half 50 + +printfn "fifty = %d" fifty +printfn "twentyFive = %d" twentyFive ``` -This function will have a signature of `int -> int`. If we wanted to define a function which takes the `double` function as a parameter, that parameter would have a type of `int -> int`. +These function have a signature of `int -> int`. If we wanted to define a function which takes the `double` and `half` functions as parameter, that parameter would have a type of `int -> int`. ```fsharp -let apply (f: int -> int) = f 5 +let double x = x * 2 +let half x = x / 2 + +let apply (f: int -> int) (x: int) = f x +let fifty = apply double 25 +let twentyFive = apply half 50 + +printfn "fifty = %d" fifty +printfn "twentyFive = %d" twentyFive ``` You don't need to pass named functions to `apply`. Anonymous functions, sometimes called lambda functions, allow you to pass an inline function as a parameter. ```fsharp -apply (fun value -> value * 5) -``` +let apply (f: int -> int) (x: int) = f x +let fifty = apply (fun x -> x * 10) 5 +printfn "fifty = %d" fifty +``` \ No newline at end of file diff --git a/public/documentation/functions/pipelines-and-composition.fsx b/public/documentation/functions/pipelines-and-composition.fsx deleted file mode 100644 index 8790c07..0000000 --- a/public/documentation/functions/pipelines-and-composition.fsx +++ /dev/null @@ -1,11 +0,0 @@ -let negative x = x * - 1 -let double x = x * 2 - -3 |> negative |> double |> printfn "double(negative(3)) = %d" - -let add x y = x + y -let multiply x y = x * y -let operation = add 3 >> multiply 3 - -let result = operation 5 -printfn "multiply 3 (add 3 5) = %d" result diff --git a/public/documentation/functions/pipelines-and-composition.md b/public/documentation/functions/pipelines-and-composition.md index 982f6a1..7d655cd 100644 --- a/public/documentation/functions/pipelines-and-composition.md +++ b/public/documentation/functions/pipelines-and-composition.md @@ -1,32 +1,53 @@ # Pipelines and Composition -The pipeline operator `|>` is a very simple operator that allows you to pipe a value into a function. We can see this by defining our own version of it. +## The Pipeline Operator: `|>` + +The pipeline operator `|>` allows you to easier pass a value to a function, making your code cleaner and more expressive. Let's start by defining our own version of the operator to see how it works. ```fsharp let (|>) value f = f value ``` -With the pipe operator, the function application of `f(g(x))` can be replaced with `x |> g |> f`. First, the value of `x` is applied to the function `g` and the result is applied to the function `f`. +Using the pipeline operator, you can rewrite nested function calls like `f(g(x))` in a more readable way: `x |> g |> f`. First, the value of `x` is applied to the function `g` and the result is then applied to the function `f`. + +```fsharp +let square x = x * x +let addOne x = x + 1 + +let result = + 4 + |> square + |> addOne + +printfn "Result = %d" result +``` -As the pipeline operator applies a value to a single parameter function (all F# functions), often times it's used in conjunction with partial application. +In this case, the numebr `4` is squared to get `16`, and then added to `1` to get the value of `17`. + +Because a single value is piped into a single argument function, which all F# functions are, the pipeline operator is often used in conjunction with partial application. Here's another example that demonstrates this: ```fsharp -let add x y = x + y +let formatCurrency amount = sprintf "$%.2f" amount +let applyDiscount rate price = price - (price * rate) +let applyTax rate price = price + (price * rate) -// produces int -> int function (not what we want) -5 |> add +let finalPrice = + 100.0 + |> applyDiscount 0.1 + |> applyTax 0.05 -// `add 3` is evaluated - producing an int -> int function -// 5 is piped into that function. -5 |> add 3 +printfn "Final Price = %s" (formatCurrency finalPrice) ``` -You can also combine two or more functions into a single function with the composition operator: `>>`. Let's define our own version of it to see how it works. +Pay attention to the order of arguments in the `applyDiscount` and `applyTax` functions. In F#, it's common for the most important argument to be placed last. This design choice enhances the composability of functions. By positioning the `price` argument last, we can partially apply the `rate` argument, resulting in a function of type `float -> float` that can then be piped into or composed with other functions easily. + +## Composing Functions With `>>` + +You can compose two functions into a single function using the composition operator `>>`. Let's define our own version to understand how it works: ```fsharp -let (>>) function1 function2 = - fun value -> - function2(function1(value)) +let (>>) f g = + fun x -> g (f x) ``` As you can see, a value is passed into the left-hand function and the result is passed into the right-hand function as an input. The function definition of `let func x = f(g(x))` can be replaced with `let func = g >> f`. @@ -34,11 +55,14 @@ As you can see, a value is passed into the left-hand function and the result is As you may notice, this operator is also often used in conjunction with partial application. This operator will create an `a -> c` function from the usage `(a -> b) >> (b -> c)` ```fsharp -let add x y = x + y -let multiply x y = x * y -let operation = add 3 >> multiply 3 -// equivalent to: -// let operation x = multiply 3 (add 3 x) +let formatCurrency amount = sprintf "$%.2f" amount +let applyDiscount rate price = price - (price * rate) +let applyTax rate price = price + (price * rate) + +let calculatePrice = applyDiscount 0.1 >> applyTax 0.05 +let finalPrice = calculatePrice 100.0 + +printfn "Final Price = %s" (formatCurrency finalPrice) ``` -The expression `add 3 >> multiply 3` works because the function signature of each function is `int -> int`. This will result in a function with an implicit parameter that will first be passed into `add 3` and the resulting `int` value will be passed into `multiply 3`. \ No newline at end of file +The expression `applyDiscount 0.1 >> applyTax 0.05` works because the function signature of each function is `float -> float`. This will result in a function with an implicit parameter that will be applied to the function `applyDiscount 0.1` and the resulting `float` value will be applied to the function `applyTax 0.05`. \ No newline at end of file diff --git a/public/documentation/functions/recursive-functions.fsx b/public/documentation/functions/recursive-functions.fsx deleted file mode 100644 index 5bc57ee..0000000 --- a/public/documentation/functions/recursive-functions.fsx +++ /dev/null @@ -1,4 +0,0 @@ -let rec fib n = - if n <= 1 then n else fib (n - 1) + fib (n - 2) - -printfn "%d" (fib 10) diff --git a/public/documentation/functions/recursive-functions.md b/public/documentation/functions/recursive-functions.md index 2dd8d9d..37afbfb 100644 --- a/public/documentation/functions/recursive-functions.md +++ b/public/documentation/functions/recursive-functions.md @@ -7,9 +7,8 @@ let rec fib n = if n <= 1 then n else fib (n - 1) + fib (n - 2) -// ^^^ ^^^ -// function is allowed to be recursive -// as the `rec` keyword is present. + +printfn "fib 5 = %d" (fib 5) ``` Mutually recursive functions can be defined using the `and` keyword. diff --git a/public/documentation/object-programming/abstract-classes-and-inheritance.fsx b/public/documentation/object-programming/abstract-classes-and-inheritance.fsx deleted file mode 100644 index 94a3538..0000000 --- a/public/documentation/object-programming/abstract-classes-and-inheritance.fsx +++ /dev/null @@ -1,13 +0,0 @@ -[] -type Drawable() = - member _.Description = "A drawable shape." - abstract member Draw: float * float -> unit - -type Square(size: int) = - inherit Drawable() - - override this.Draw(x: float, y: float) = - printfn $"Drawing a square @ X: {x}, Y: {y}" - -let square = Square(10) -square.Draw(0, 0) diff --git a/public/documentation/object-programming/interfaces.fsx b/public/documentation/object-programming/interfaces.fsx deleted file mode 100644 index 9c2a5a7..0000000 --- a/public/documentation/object-programming/interfaces.fsx +++ /dev/null @@ -1,18 +0,0 @@ -type IShape = - abstract member Name: string - -type IDrawable = - inherit IShape - abstract member Draw: float * float -> unit - -type Square(size: int) = - interface IDrawable with - member this.Name = "Square" - - member this.Draw(x: float, y: float) = - printfn $"Drawing a square with a size of {size} @ X: {x}, Y: {y}" - - -let drawableShape: IDrawable = Square(10) -printfn $"Shape Name: {drawableShape.Name}" -drawableShape.Draw(10, 20) diff --git a/public/documentation/object-programming/object-expressions.fsx b/public/documentation/object-programming/object-expressions.fsx deleted file mode 100644 index 8ab83b9..0000000 --- a/public/documentation/object-programming/object-expressions.fsx +++ /dev/null @@ -1,9 +0,0 @@ -type IDrawable = - abstract member Draw: float * float -> unit - -let square = - { new IDrawable with - member this.Draw(x: float, y: float) = - printfn $"Drawing a square @ X: {x}, Y: {y}" } - -square.Draw(0.0, 0.0) diff --git a/public/documentation/object-programming/objects-and-members.fsx b/public/documentation/object-programming/objects-and-members.fsx deleted file mode 100644 index 7550406..0000000 --- a/public/documentation/object-programming/objects-and-members.fsx +++ /dev/null @@ -1,12 +0,0 @@ -type Person(firstName: string, lastName: string) = - member val FirstName = firstName with get - member val LastName = lastName with get - - member this.Greet(?greeting: string, ?punctuation: string) = - let greeting = Option.defaultValue "Hello" greeting - let punctuation = Option.defaultValue "!" punctuation - $"{greeting} {this.FirstName} {this.LastName}{punctuation}" - -let person = Person("John", "Doe") -let greeting = person.Greet(greeting = "Greetings", punctuation = "!!!") -printfn "%s" greeting diff --git a/public/documentation/object-programming/type-extensions.fsx b/public/documentation/object-programming/type-extensions.fsx deleted file mode 100644 index 768fdcd..0000000 --- a/public/documentation/object-programming/type-extensions.fsx +++ /dev/null @@ -1,11 +0,0 @@ -open System - -type String with - - member string.IsUpperCase() = string = string.ToUpper() - -let uppercaseString = "HELLO WORLD" -let lowercaseStringValue = "hello world" - -printfn "uppercaseString.IsUpperCase() = %b" (uppercaseString.IsUpperCase()) -printfn "lowercaseStringValue.IsUpperCase() = %b" (lowercaseStringValue.IsUpperCase())