diff --git a/.github/README.md b/.github/README.md index 49d2bc5..8c316e4 100644 --- a/.github/README.md +++ b/.github/README.md @@ -15,7 +15,6 @@ The following editors are currently supported: - Markdown - Block Grid - Block List -- Nested Content - Textstring - Textarea @@ -35,10 +34,10 @@ When creating a new data type, the default will be 200 words per minute. ## Security -> [!NOTE] -> This project takes security and support seriously. -> Please visit the [Security](https://github.com/jcdcdev/jcdcdev.Umbraco.ReadingTime?tab=security-ov-file) page for more information. - +> [!NOTE] +> This project takes security and support seriously. +> Please visit the [Security](https://github.com/jcdcdev/jcdcdev.Umbraco.ReadingTime?tab=security-ov-file) page for more information. + ## Contributing @@ -52,4 +51,4 @@ Thank you to the following projects and individuals for their contributions. Hig - LottePitcher - [opinionated-package-starter](https://github.com/LottePitcher/opinionated-package-starter) - + diff --git a/docs/README_nuget.md b/docs/README_nuget.md index 36c319b..6449f8b 100644 --- a/docs/README_nuget.md +++ b/docs/README_nuget.md @@ -15,13 +15,12 @@ The following editors are currently supported: - Markdown - Block Grid - Block List -- Nested Content - Textstring - Textarea ## Security -This project takes security and support seriously. +This project takes security and support seriously. Please visit the [Security](https://github.com/jcdcdev/jcdcdev.Umbraco.ReadingTime?tab=security-ov-file) page for more information. ## Contributing @@ -35,4 +34,4 @@ Thank you to the following projects and individuals for their contributions. Hig - LottePitcher - [opinionated-package-starter](https://github.com/LottePitcher/opinionated-package-starter) - + diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/openapi-ts.config.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/openapi-ts.config.ts deleted file mode 100644 index 2cc145d..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/openapi-ts.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig, defaultPlugins } from '@hey-api/openapi-ts'; - -export default defineConfig({ - input: 'http://localhost:54813/umbraco/swagger/ReadingTime/swagger.json', - plugins: [ - ...defaultPlugins, - { - name: '@hey-api/client-fetch', - exportFromIndex: true, - throwOnError: true, - }, - { - name: '@hey-api/typescript', - enums: 'typescript', - readOnlyWriteOnlyBehavior: 'off', - }, - { - name: '@hey-api/sdk', - asClass: true, - } - ], - output: { - format: 'prettier', - path: './src/api', - } -}); diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/package-lock.json b/src/jcdcdev.Umbraco.ReadingTime.Client/package-lock.json index 0608ced..22448c0 100644 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/package-lock.json +++ b/src/jcdcdev.Umbraco.ReadingTime.Client/package-lock.json @@ -11,7 +11,7 @@ "@umbraco-cms/backoffice": "^17.0.2", "lit": "^3.3.2", "typescript": "^5.9.3", - "vite": "^7.3.0" + "vite": "^7.3.1" } }, "node_modules/@esbuild/aix-ppc64": { @@ -544,20 +544,20 @@ "peer": true }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", - "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", - "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0" + "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "node_modules/@microsoft/signalr": { @@ -584,9 +584,9 @@ "peer": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -598,9 +598,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -612,9 +612,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -626,9 +626,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -640,9 +640,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -654,9 +654,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -668,9 +668,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -682,9 +682,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -696,9 +696,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -710,9 +710,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -724,9 +724,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -738,9 +752,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -752,9 +780,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -766,9 +794,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -780,9 +808,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -794,9 +822,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -808,9 +836,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -821,10 +849,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -836,9 +878,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -850,9 +892,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -864,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -878,9 +920,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -1086,9 +1128,9 @@ } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/core": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.14.0.tgz", - "integrity": "sha512-nm0VWVA1Vq/jaKY3wyRXViL/kf78yMdH7qETpv4qZXDQLU+pdWV3IGoRTQTKESc7d8L1wL/2uCeByLNUJfrSIw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.18.0.tgz", + "integrity": "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ==", "dev": true, "license": "MIT", "peer": true, @@ -1097,13 +1139,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.14.0" + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-blockquote": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.14.0.tgz", - "integrity": "sha512-I7aOqcVLHBgCeRtMaMHA+ILSS8Sli46fjFq8477stOpQ79TPiBd6e4SDuFCAu58M94mVLMvlPKF2Eh5IvbIMyQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.18.0.tgz", + "integrity": "sha512-1HjEoM5vZDfFnq2OodNpW13s56a9pbl7jolUv1V9FrE3X5s7n0HCfDzIVpT7z1HgTdPtlN5oSt5uVyBwuwSUfA==", "dev": true, "license": "MIT", "peer": true, @@ -1112,13 +1154,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-bold": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.14.0.tgz", - "integrity": "sha512-T4ma6VLoHm9JupglidD3CfZXm89A3HMv99gLplXNizvy1mlr4R3uC3aBqKw6lAP+NoqCqbIgjwc4YYsqZClNwA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.18.0.tgz", + "integrity": "sha512-xUgOvHCdGXh9Lfxd7DtgsSr0T/egIwBllWHIBWDjQEQQ0b+ICn+0+i703btHMB4hjdduZtgVDrhK8jAW3U6swA==", "dev": true, "license": "MIT", "peer": true, @@ -1127,13 +1169,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-bullet-list": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.14.0.tgz", - "integrity": "sha512-luqPX4u52hiOAHJ95mYsNE+x+9dZxsM461Xny9d/eTXLjAcnwS7MghjrnpljvyYsSXNiwQtxUyEr4uEZZJ5gIQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.18.0.tgz", + "integrity": "sha512-8sEpY0nxAGGFDYlF+WVFPKX00X2dAAjmoi0+2eWvK990PdQqwXrQsRs7pkUbpE2mDtATV8+GlDXk9KDkK/ZXhA==", "dev": true, "license": "MIT", "peer": true, @@ -1142,13 +1184,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.14.0" + "@tiptap/extension-list": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-code": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.14.0.tgz", - "integrity": "sha512-Sx9yLorzS+oqNmXID4jt0G5tDnsEgU0HtEXPLD3KNt/ltVxWJU0AXwCsp1/Dg0HIDL868vWpJ2jC1t/4oaf9kA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.18.0.tgz", + "integrity": "sha512-0SU53O0NRmdtRM2Hgzm372dVoHjs2F40o/dtB7ls4kocf4W89FyWeC2R6ZsFQqcXisNh9RTzLtYfbNyizGuZIw==", "dev": true, "license": "MIT", "peer": true, @@ -1157,13 +1199,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-code-block": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.14.0.tgz", - "integrity": "sha512-hRSdIhhm3Q9JBMQdKaifRVFnAa4sG+M7l1QcTKR3VSYVy2/oR0U+aiOifi5OvMRBUwhaR71Ro+cMT9FH9s26Kg==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.18.0.tgz", + "integrity": "sha512-fCx1oT95ikGfoizw+XCjeglQxlLK4lWgUcB4Dcn5TdaCoFBQMEaZs7Q0jVajxxxULnyArkg60uarc1ac/IF2Hw==", "dev": true, "license": "MIT", "peer": true, @@ -1172,14 +1214,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0", - "@tiptap/pm": "^3.14.0" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-document": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.14.0.tgz", - "integrity": "sha512-O3D7/GPB3XrWGy0y/b4LMHiY0eTd+dyIbSdiFtmUnbC/E9lqQLw43GiqvD9Gm6AyKhBA+Z45dKMbaOe1c6eTwQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.18.0.tgz", + "integrity": "sha512-e0hOGrjTMpCns8IC5p+c5CEiE1BBmFBFL+RpIxU/fjT2SaZ7q2xsFguBu94lQDT0cD6fdZokFRpGwEMxZNVGCg==", "dev": true, "license": "MIT", "peer": true, @@ -1188,13 +1230,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-dropcursor": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.14.0.tgz", - "integrity": "sha512-IwHyiZKLjV9WSBlQFS+afMjucIML8wFAKkG8UKCu+CVOe/Qd1ImDGyv6rzPlCmefJkDHIUWS+c2STapJlUD1VQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.18.0.tgz", + "integrity": "sha512-pIW/K9fGth221dkfA5SInHcqfnCr0aG9LGkRiEh4gwM4cf6ceUBrvcD+QlemSZ4q9oktNGJmXT+sEXVOQ8QoeQ==", "dev": true, "license": "MIT", "peer": true, @@ -1203,13 +1245,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.14.0" + "@tiptap/extensions": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-gapcursor": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.14.0.tgz", - "integrity": "sha512-hMg2U59+c9FreYtTvzxx5GWKejdZLRITMLEu4OTfrgQok6uF4qkzGEEqmYqPiHk08TBqAg18Y5bbpyqTsuit9A==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.18.0.tgz", + "integrity": "sha512-covioXPPHX3SnlTwC/1rcHUHAc7/JFd4vN0kZQmZmvGHlxqq2dPmtrPh8D7TuDuhG0k/3Z6i8dJFP0phfRAhuA==", "dev": true, "license": "MIT", "peer": true, @@ -1218,13 +1260,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.14.0" + "@tiptap/extensions": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-hard-break": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.14.0.tgz", - "integrity": "sha512-XKxr8usQp+kFevhDK6Ccmnq1CIkLmPClhKwbt7AClGLKLBtEVAS1qUgcmKudkw8cD8Q2/69twI37LXa23sfuLA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.18.0.tgz", + "integrity": "sha512-IXLiOHEmbU2Wn1jFRZC6apMxiJQvSRWhwoiubAvRxyiPSnFTeaEgT8Qgo5DjwB39NckP+o7XX7RrgzlkwdFPQQ==", "dev": true, "license": "MIT", "peer": true, @@ -1233,13 +1275,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-heading": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.14.0.tgz", - "integrity": "sha512-4xpahSo3b1dN2nwA0XKXLQVz9nZ/vE443a/Y5QLWeXiu3v9wkcMs/5kQ5ysFeDZRBTfVUWBqhngI7zhvDUx2zQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.18.0.tgz", + "integrity": "sha512-MTamVnYsFWVndLSq5PRQ7ZmbF6AExsFS9uIvGtUAwuhzvR4of/WHh6wpvWYjA+BLXTWRrfuGHaZTl7UXBN13fg==", "dev": true, "license": "MIT", "peer": true, @@ -1248,13 +1290,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.14.0.tgz", - "integrity": "sha512-65O4T9vPKLUKO1fLowh5jqtfQlH5eaIL7qb/uj5sXMMg8O7TCvBIRkwNuYsFTkJmTk4vBy+fjZ0uwSY3DFkO1g==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.18.0.tgz", + "integrity": "sha512-fEq7DwwQZ496RHNbMQypBVNqoWnhDEERbzWMBqlmfCfc/0FvJrHtsQkk3k4lgqMYqmBwym3Wp0SrRYiyKCPGTw==", "dev": true, "license": "MIT", "peer": true, @@ -1263,14 +1305,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0", - "@tiptap/pm": "^3.14.0" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-italic": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.14.0.tgz", - "integrity": "sha512-Arl5EaG4wdyipwvKjsI7Krlk3OkmqvLfF0YfGwsd5AVDxTiYuiDGgz7RF8J2kttbBeiUTqwME5xpkryQK3F+fg==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.18.0.tgz", + "integrity": "sha512-1C4nB08psiRo0BPxAbpYq8peUOKnjQWtBCLPbE6B9ToTK3vmUk0AZTqLO11FvokuM1GF5l2Lg3sKrKFuC2hcjQ==", "dev": true, "license": "MIT", "peer": true, @@ -1279,13 +1321,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-link": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.14.0.tgz", - "integrity": "sha512-xaeJIktD42rJ4t9fbQpKe+yYNZ+YFIK96cp1Kdm0hZHv/8MPMNRiF85TRY+9U1aoyh5uRcspgCj7EKQb2Hs7qg==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.18.0.tgz", + "integrity": "sha512-1J28C4+fKAMQi7q/UsTjAmgmKTnzjExXY98hEBneiVzFDxqF69n7+Vb7nVTNAIhmmJkZMA0DEcMhSiQC/1/u4A==", "dev": true, "license": "MIT", "peer": true, @@ -1297,14 +1339,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0", - "@tiptap/pm": "^3.14.0" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.14.0.tgz", - "integrity": "sha512-rsjFH0Vd/4UbDsjwMLay7oz72VVu1r35t8ofAzy5587jn5JAjflaZs05XbRRMD2imUTK41dyajVSh8CqSnDEJw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.18.0.tgz", + "integrity": "sha512-9lQBo45HNqIFcLEHAk+CY3W51eMMxIJjWbthm2CwEWr4PB3+922YELlvq8JcLH1nVFkBVpmBFmQe/GxgnCkzwQ==", "dev": true, "license": "MIT", "peer": true, @@ -1313,14 +1355,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0", - "@tiptap/pm": "^3.14.0" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list-item": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.14.0.tgz", - "integrity": "sha512-19Dcp8HCFdhINmRy0KQLFfz9ZEuVwFWGAAjYG7BvMvkd9k4sJ5vCv5fej59G99rhsc+tCmik77w+SLksOcxwKQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.18.0.tgz", + "integrity": "sha512-auTSt+NXoUnT0xofzFa+FnXsrW1TPdT1OB3U1OqQCIWkumZqL45A8OK9kpvyQsWj/xJ8fy1iZwFlKXPtxjLd2w==", "dev": true, "license": "MIT", "peer": true, @@ -1329,13 +1371,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.14.0" + "@tiptap/extension-list": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list-keymap": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.14.0.tgz", - "integrity": "sha512-1oPbvNnQjeOxkHZcUbWPx/IY9o4fT3QGk/9A9cIjFrJRD2AHzbYfPDHNHINtg7Bj0jWz74cHvAHcaxP+M27jkA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.18.0.tgz", + "integrity": "sha512-ZzO5r/cW7G0zpL/eM69WPnMpzb0YsSjtI60CYGA0iQDRJnK9INvxu0RU0ewM2faqqwASmtjuNJac+Fjk6scdXg==", "dev": true, "license": "MIT", "peer": true, @@ -1344,13 +1386,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.14.0" + "@tiptap/extension-list": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-ordered-list": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.14.0.tgz", - "integrity": "sha512-/fXjVL4JajkJQoc213iiput0bCXC4ztUPUpvNuI62VcgFKHcTvX4eYxED1VflotCx0OdkyY9yYD8PtvyO5lkmA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.18.0.tgz", + "integrity": "sha512-5bUAfklYLS5o6qvLLfreGyGvD1JKXqOQF0YntLyPuCGrXv7+XjPWQL2BmEf59fOn2UPT2syXLQ1WN5MHTArRzg==", "dev": true, "license": "MIT", "peer": true, @@ -1359,13 +1401,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.14.0" + "@tiptap/extension-list": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-paragraph": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.14.0.tgz", - "integrity": "sha512-NFxk2yNo3Cvh9g8evea+yTLNV48se7MbMcVizTnVhobqtBKv793qsb5FM5Hu30Y72FQPNfH+LRoap4XZyBPfVw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.18.0.tgz", + "integrity": "sha512-uvFhdwiur4NhhUdBmDsajxjGAIlg5qga55fYag2DzOXxIQE2M7/aVMRkRpuJzb88GY4EHSh8rY34HgMK2FJt2Q==", "dev": true, "license": "MIT", "peer": true, @@ -1374,13 +1416,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-strike": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.14.0.tgz", - "integrity": "sha512-R8BbAhnWpisBml6okMKl98hY4tJjedTTgyTkx8tPabIJ92nS9IURKEk3foWB9uHxdTOBUqTvVT+2ScDf9r6QHg==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.18.0.tgz", + "integrity": "sha512-kl/fa68LZg8NWUqTkRTfgyCx+IGqozBmzJxQDc1zxurrIU+VFptDV9UuZim587sbM2KGjCi/PNPjPGk1Uu0PVg==", "dev": true, "license": "MIT", "peer": true, @@ -1389,13 +1431,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-text": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.14.0.tgz", - "integrity": "sha512-XlpnD87LQ7lLcDcBenHgzxv3uivQzPdVHM16CY4lXR4aKDIp2mxjPZr4twHT+cOnRQHc8VYpRgkEo6LLX6VylA==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.18.0.tgz", + "integrity": "sha512-9TvctdnBCwK/zyTi9kS7nGFNl5OvGM8xE0u38ZmQw5t79JOqJHgOroyqMjw8LHK/1PWrozfNCmsZbpq4IZuKXw==", "dev": true, "license": "MIT", "peer": true, @@ -1404,13 +1446,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-underline": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.14.0.tgz", - "integrity": "sha512-zmnWlsi2g/tMlThHby0Je9O+v24j4d+qcXF3nuzLUUaDsGCEtOyC9RzwITft59ViK+Nc2PD2W/J14rsB0j+qoQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.18.0.tgz", + "integrity": "sha512-009IeXURNJ/sm1pBqbj+2YQgjQaBtNlJR3dbl6xu49C+qExqCmI7klhKQuwsVVGLR7ahsYlp7d9RlftnhCXIcQ==", "dev": true, "license": "MIT", "peer": true, @@ -1419,13 +1461,13 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0" + "@tiptap/core": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extensions": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.14.0.tgz", - "integrity": "sha512-qQBVKqzU4ZVjRn8W0UbdfE4LaaIgcIWHOMrNnJ+PutrRzQ6ZzhmD/kRONvRWBfG9z3DU7pSKGwVYSR2hztsGuQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.18.0.tgz", + "integrity": "sha512-uSRIE9HGshBN6NRFR3LX2lZqBLvX92SgU5A9AvUbJD4MqU63E+HdruJnRjsVlX3kPrmbIDowxrzXlUcg3K0USQ==", "dev": true, "license": "MIT", "peer": true, @@ -1434,14 +1476,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.14.0", - "@tiptap/pm": "^3.14.0" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/pm": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.14.0.tgz", - "integrity": "sha512-xrZmqI5jl4yMeAsu8p8gVP9S3An5h2MBi8BQHNnZmpyzkUrlpd40vlT6u13SWIqVi5ZWhBZ6U3rL7mkVLZuRKg==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.18.0.tgz", + "integrity": "sha512-8RoI5gW0xBVCsuxahpK8vx7onAw6k2/uR3hbGBBnH+HocDMaAZKot3nTyY546ij8ospIC1mnQ7k4BhVUZesZDQ==", "dev": true, "license": "MIT", "peer": true, @@ -1529,9 +1571,9 @@ "license": "MIT" }, "node_modules/@umbraco-cms/backoffice": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/@umbraco-cms/backoffice/-/backoffice-17.0.2.tgz", - "integrity": "sha512-RMzfjLyIeIzlLPSMOn4nRdFQvNbPlhG6sdoJYPAg501WT/QuKejRR6ugc8z/vNFUAyH33nfdShI2o/beMs05hw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@umbraco-cms/backoffice/-/backoffice-17.1.0.tgz", + "integrity": "sha512-pWtvuQ8VIt5pZzRqJDyFWs3IfIJYm8xAreg6DPhTdOqhgbGgOkfEqz4V0wv939pjw8LmxGMuPDzH6vci/Hm6zQ==", "dev": true, "license": "MIT", "engines": { @@ -1556,12 +1598,12 @@ "@umbraco-ui/uui": "^1.16.0", "@umbraco-ui/uui-css": "^1.16.0", "diff": "^7.0.0", - "dompurify": "^3.2.7", + "dompurify": "^3.3.0", "element-internals-polyfill": "^3.0.2", "lit": "^3.3.1", "luxon": "^3.7.2", "marked": "^17.0.1", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.55.1", "rxjs": "^7.8.2", "uuid": "^13.0.0" } @@ -2806,9 +2848,9 @@ "peer": true }, "node_modules/default-browser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", - "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "peer": true, @@ -3209,21 +3251,21 @@ } }, "node_modules/lit-element": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", - "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0", + "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "node_modules/lit-html": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", - "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3231,9 +3273,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT", "peer": true @@ -3302,24 +3344,27 @@ } }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "dompurify": "3.1.7", + "dompurify": "3.2.7", "marked": "14.0.0" } }, "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "dev": true, "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/monaco-editor/node_modules/marked": { "version": "14.0.0", @@ -3393,26 +3438,32 @@ "peer": true }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", + "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", + "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -3458,9 +3509,9 @@ "peer": true }, "node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT", "peer": true @@ -3628,9 +3679,9 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", - "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.3.tgz", + "integrity": "sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ==", "dev": true, "license": "MIT", "peer": true, @@ -3703,9 +3754,9 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.4.tgz", - "integrity": "sha512-CGr2BK5sLdZx+ARbeLO4HBZYa3qSG3FmwOVmzYs0Zp7n5SkrGqj+1CeNuubFNZEr64yMAQ20SanbFyIyHWZc8w==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", "dev": true, "license": "MIT", "peer": true, @@ -3735,9 +3786,9 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz", - "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", "dev": true, "license": "MIT", "peer": true, @@ -3746,9 +3797,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.41.4", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", - "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", + "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", "dev": true, "license": "MIT", "peer": true, @@ -3838,9 +3889,9 @@ "peer": true }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -3854,28 +3905,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -4092,9 +4146,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/package.json b/src/jcdcdev.Umbraco.ReadingTime.Client/package.json index 83d7419..87cfa7f 100644 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/package.json +++ b/src/jcdcdev.Umbraco.ReadingTime.Client/package.json @@ -6,16 +6,15 @@ "scripts": { "dev": "vite build --watch --emptyOutDir", "build": "tsc && vite build --emptyOutDir", - "preview": "vite preview", - "generate": "openapi-ts" + "preview": "vite preview" }, "devDependencies": { "lit": "^3.3.2", "@umbraco-cms/backoffice": "^17.0.2", "typescript": "^5.9.3", - "vite": "^7.3.0" + "vite": "^7.3.1" }, "volta": { - "node": "22.17.1" + "node": "22.22.0" } } \ No newline at end of file diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client.gen.ts deleted file mode 100644 index eadcc39..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig({ - baseUrl: 'http://localhost:54813', - throwOnError: true -})); diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/client.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/client.gen.ts deleted file mode 100644 index a439d27..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/client.gen.ts +++ /dev/null @@ -1,268 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { - Client, - Config, - RequestOptions, - ResolvedRequestOptions, -} from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); - - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - - return { opts, url }; - }; - - const request: Client['request'] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'json': - case 'text': - data = await response[parseAs](); - break; - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - const error = jsonError ?? textError; - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - const makeMethodFn = - (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = - (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - url, - }); - }; - - return { - buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/index.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/index.ts deleted file mode 100644 index cbf8dfe..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - OptionsLegacyParser, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/types.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/types.gen.ts deleted file mode 100644 index 1a005b5..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/types.gen.ts +++ /dev/null @@ -1,268 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { - Client as CoreClient, - Config as CoreConfig, -} from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: - | 'arrayBuffer' - | 'auto' - | 'blob' - | 'formData' - | 'json' - | 'stream' - | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick< - Required>, - 'method' - >, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: Pick & Options, -) => string; - -export type Client = CoreClient< - RequestFn, - Config, - MethodFn, - BuildUrlFn, - SseFn -> & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - Omit; - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'body' | 'headers' | 'url' - > & - TData - : OmitKeys< - RequestOptions, - 'body' | 'url' - > & - TData & - Pick, 'headers'> - : TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'headers' | 'url' - > & - TData & - Pick, 'body'> - : OmitKeys, 'url'> & - TData; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/utils.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/utils.gen.ts deleted file mode 100644 index b4bcc4d..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/client/utils.gen.ts +++ /dev/null @@ -1,331 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - allowReserved, - array, - object, -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved, - explode: true, - name, - style: 'form', - value, - ...array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = - header instanceof Headers - ? headersEntries(header) - : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update( - id: number | Interceptor, - fn: Interceptor, - ): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/auth.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/auth.gen.ts deleted file mode 100644 index f8a7326..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/auth.gen.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/bodySerializer.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/bodySerializer.gen.ts deleted file mode 100644 index 49cd892..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/bodySerializer.gen.ts +++ /dev/null @@ -1,92 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/params.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/params.gen.ts deleted file mode 100644 index 71c88e8..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/params.gen.ts +++ /dev/null @@ -1,153 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/pathSerializer.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/pathSerializer.gen.ts deleted file mode 100644 index 8d99931..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/pathSerializer.gen.ts +++ /dev/null @@ -1,181 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/queryKeySerializer.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/queryKeySerializer.gen.ts deleted file mode 100644 index d3bb683..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,136 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = ( - value: unknown, -): JsonValue | undefined => { - if (value === null) { - return null; - } - - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return value; - } - - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if ( - typeof URLSearchParams !== 'undefined' && - value instanceof URLSearchParams - ) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/serverSentEvents.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/serverSentEvents.gen.ts deleted file mode 100644 index f8fd78e..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,264 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit< - RequestInit, - 'method' -> & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult< - TData = unknown, - TReturn = void, - TNext = unknown, -> = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export const createSseClient = ({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined; - - const sleep = - sseSleepFn ?? - ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) - throw new Error( - `SSE failed: ${response.status} ${response.statusText}`, - ); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt( - line.replace(/^retry:\s*/, ''), - 10, - ); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if ( - sseMaxRetryAttempts !== undefined && - attempt >= sseMaxRetryAttempts - ) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min( - retryDelay * 2 ** (attempt - 1), - sseMaxRetryDelay ?? 30000, - ); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/types.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/types.gen.ts deleted file mode 100644 index 643c070..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/types.gen.ts +++ /dev/null @@ -1,118 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] - ? { sse?: never } - : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; -}; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/utils.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/utils.gen.ts deleted file mode 100644 index 0b5389d..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/core/utils.gen.ts +++ /dev/null @@ -1,143 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e. client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/index.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/index.ts deleted file mode 100644 index 3731393..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type * from './types.gen'; -export * from './client.gen'; -export * from './sdk.gen'; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/sdk.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/sdk.gen.ts deleted file mode 100644 index 9f8401c..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/sdk.gen.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { GetUmbracoReadingTimeApiV1Data, GetUmbracoReadingTimeApiV1Responses } from './types.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export class ReadingTime { - public static getUmbracoReadingTimeApiV1(options?: Options) { - return (options?.client ?? client).get({ - url: '/umbraco/ReadingTime/api/v1', - ...options - }); - } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/types.gen.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/types.gen.ts deleted file mode 100644 index 55ef9a5..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/api/types.gen.ts +++ /dev/null @@ -1,30 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: 'http://localhost:54813' | (string & {}); -}; - -export type ReadingTimeResponse = { - updateDate: string; - readingTime: string; -}; - -export type GetUmbracoReadingTimeApiV1Data = { - body?: never; - path?: never; - query?: { - contentKey?: string; - dataTypeKey?: string; - culture?: string; - }; - url: '/umbraco/ReadingTime/api/v1'; -}; - -export type GetUmbracoReadingTimeApiV1Responses = { - /** - * OK - */ - 200: ReadingTimeResponse; -}; - -export type GetUmbracoReadingTimeApiV1Response = GetUmbracoReadingTimeApiV1Responses[keyof GetUmbracoReadingTimeApiV1Responses]; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/context/reading-time.context.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/context/reading-time.context.ts deleted file mode 100644 index bbbb1b2..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/context/reading-time.context.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {UmbControllerBase} from "@umbraco-cms/backoffice/class-api"; -import {UmbControllerHost} from "@umbraco-cms/backoffice/controller-api"; -import {UmbDataSourceResponse} from "@umbraco-cms/backoffice/repository"; -import {UmbContextToken} from "@umbraco-cms/backoffice/context-api"; -import {ReadingTimeResponse} from "../api"; -import {ReadingTimeRepository} from "../repository/reading-time.repository.ts"; - -export class ReadingTimeContext extends UmbControllerBase { - #repository: ReadingTimeRepository; - - constructor(host: UmbControllerHost) { - super(host); - this.#repository = new ReadingTimeRepository(this); - this.provideContext(READING_TIME_CONTEXT_TOKEN, this); - } - - async getReadingTime(contentKey: string, dataTypeKey: string, culture?: string): Promise> { - return await this.#repository.getReadingTime(contentKey, dataTypeKey, culture); - } -} - -export const READING_TIME_CONTEXT_TOKEN = - new UmbContextToken("ReadingTimeContext"); diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/manifest.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/manifest.ts index 3da72d3..c42c820 100644 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/manifest.ts +++ b/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/manifest.ts @@ -1,6 +1,4 @@ -import {ManifestPropertyEditorUi} from "@umbraco-cms/backoffice/property-editor"; - -const editors: Array = [ +const editors: Array = [ { type: "propertyEditorUi", alias: "jcdcdev.ReadingTime", @@ -9,7 +7,7 @@ const editors: Array = [ elementName: "reading-time-property-editor-ui", meta: { label: "Reading Time", - icon: "icon-list", + icon: "icon-timer", group: "common", propertyEditorSchemaAlias: "jcdcdev.ReadingTime", settings: { @@ -72,7 +70,7 @@ const editors: Array = [ }, { alias: "maxUnit", - value: "Minute" + value: "Hour" }, { alias: "hideVariationWarning", diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/reading-time.editor.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/reading-time.editor.ts index 10e4917..11bfa59 100644 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/reading-time.editor.ts +++ b/src/jcdcdev.Umbraco.ReadingTime.Client/src/editors/reading-time.editor.ts @@ -1,176 +1,173 @@ -import {LitElement, html, customElement, property, state} from "@umbraco-cms/backoffice/external/lit"; -import {UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement} from "@umbraco-cms/backoffice/property-editor"; -import {UmbElementMixin} from "@umbraco-cms/backoffice/element-api"; -import {UMB_ENTITY_CONTEXT} from "@umbraco-cms/backoffice/entity"; -import {UMB_PROPERTY_CONTEXT} from "@umbraco-cms/backoffice/property"; -import {UMB_CONTENT_PROPERTY_CONTEXT} from "@umbraco-cms/backoffice/content"; -import {ReadingTimeResponse} from "../api"; -import {READING_TIME_CONTEXT_TOKEN, ReadingTimeContext} from "../context/reading-time.context.ts"; -import {css, nothing, PropertyValues} from "lit"; -import {UMB_ACTION_EVENT_CONTEXT} from "@umbraco-cms/backoffice/action"; -import {UmbRequestReloadStructureForEntityEvent} from "@umbraco-cms/backoffice/entity-action"; +import { LitElement, html, css, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; + +const UNIT_ORDER = ['second', 'minute', 'hour', 'day'] as const; +type TimeUnit = (typeof UNIT_ORDER)[number]; @customElement('reading-time-property-editor-ui') export default class ReadingTimePropertyEditorUi extends UmbElementMixin(LitElement) implements UmbPropertyEditorUiElement { + @property({ type: Number }) + public value?: number; - @property({type: String}) - public value = ""; - - #readingTimeContext?: ReadingTimeContext; - - @state() - private hideVariationWarning: boolean = false; - @state() - private loading: boolean = false; - @state() - private contentKey?: string; - @state() - private dataTypeKey?: string; - @state() - private culture?: string; - @state() - private data?: ReadingTimeResponse; - @state() - private initialised: boolean = false - static styles = [css` - .alert { - background-color: darkgoldenrod; - padding: 5px; - } + @state() + private _minUnit: TimeUnit = 'minute'; - .icon-container { - display: flex; - align-items: center; - } + @state() + private _maxUnit: TimeUnit = 'hour'; - .icon { - margin-right: 5px; - } - `] - - constructor() { - super(); - - this.consumeContext(READING_TIME_CONTEXT_TOKEN, (context) => { - this.#readingTimeContext = context; - }); - - this.consumeContext(UMB_ENTITY_CONTEXT, (context) => { - this.contentKey = context?.getUnique() ?? undefined; - }); - - this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { - this.culture = context?.getVariantId()?.culture ?? undefined; - }); - - this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => { - context?.addEventListener(UmbRequestReloadStructureForEntityEvent.TYPE, () => { - if (!this.initialised) { - return; - } - this.loading = true; - const interval = setInterval(async () => { - if (!(this.contentKey && this.dataTypeKey)) { - return; - } - - const response = await this.#readingTimeContext?.getReadingTime(this.contentKey, this.dataTypeKey, this.culture); - if (!response || !response.data?.updateDate) { - return; - } - - if (response.data.updateDate === this.data?.updateDate) { - return; - } - - this.data = response.data; - this.loading = false; - clearInterval(interval); - }, 2500); - }); - }); - - this.consumeContext(UMB_CONTENT_PROPERTY_CONTEXT, (context) => { - context?.dataType.subscribe((dataType) => { - this.dataTypeKey = dataType?.unique - }).unsubscribe(); - }); - } + @state() + private _hideVariationWarning: boolean = false; - @property({attribute: false}) - public set config(config: UmbPropertyEditorConfigCollection) { - this.hideVariationWarning = config.getValueByAlias("hideVariationWarning") ?? false; - } + @state() + private _culture?: string; - render() { - if (this.loading) { - return html - ` - - `; - } + constructor() { + super(); + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._culture = context?.getVariantId()?.culture ?? undefined; + }); + } - if (!this.data) { - return html - ` -
Save and publish to calculate reading time
- `; - } + @property({ attribute: false }) + public set config(config: UmbPropertyEditorConfigCollection) { + const minVal = config.getValueByAlias('minUnit'); + const maxVal = config.getValueByAlias('maxUnit'); + this._hideVariationWarning = config.getValueByAlias('hideVariationWarning') ?? false; - const alert = this.renderVariationAlert(); - return html - ` -
- ${alert} - ${this.data.readingTime} -
- `; - } + const resolvedMin = Array.isArray(minVal) ? minVal[0] : minVal; + const resolvedMax = Array.isArray(maxVal) ? maxVal[0] : maxVal; - renderVariationAlert() { - if (this.hideVariationWarning || this.culture) { - return nothing; - } + const normalizedMin = resolvedMin?.toLowerCase() as TimeUnit | undefined; + const normalizedMax = resolvedMax?.toLowerCase() as TimeUnit | undefined; - return html - ` -
-
- - Language specific properties are not used in this calculation -
-
- `; + if (normalizedMin && UNIT_ORDER.includes(normalizedMin)) { + this._minUnit = normalizedMin; } + if (normalizedMax && UNIT_ORDER.includes(normalizedMax)) { + this._maxUnit = normalizedMax; + } + } - protected updated(_changedProperties: PropertyValues) { - if (!this.initialised) { - if (this.contentKey && this.dataTypeKey) { - this.init(); - } - } + #formatTime(totalSeconds: number): string { + if (totalSeconds <= 0) { + return this._minUnit === 'second' ? 'Less than a second' : 'Less than a minute'; } - private async init() { - if (this.initialised) { - return; + const minIdx = UNIT_ORDER.indexOf(this._minUnit); + const maxIdx = UNIT_ORDER.indexOf(this._maxUnit); + + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const allUnits: { unit: TimeUnit; value: number }[] = [ + { unit: 'day', value: days }, + { unit: 'hour', value: hours }, + { unit: 'minute', value: minutes }, + { unit: 'second', value: seconds }, + ]; + + // Filter to only units within the min/max range + const filtered = allUnits.filter((u) => { + const idx = UNIT_ORDER.indexOf(u.unit); + return idx >= minIdx && idx <= maxIdx; + }); + + // If min unit is above seconds, round up the smallest visible unit + if (minIdx > 0 && filtered.length > 0) { + const belowMinUnits = allUnits.filter((u) => UNIT_ORDER.indexOf(u.unit) < minIdx); + const hasRemainder = belowMinUnits.some((u) => u.value > 0); + if (hasRemainder) { + const smallest = filtered[filtered.length - 1]; + smallest.value += 1; + // Handle carry-over + for (let i = filtered.length - 1; i > 0; i--) { + const current = filtered[i]; + const parent = filtered[i - 1]; + const limit = current.unit === 'second' ? 60 : current.unit === 'minute' ? 60 : current.unit === 'hour' ? 24 : Infinity; + if (current.value >= limit) { + current.value -= limit; + parent.value += 1; + } } + } + } - this.loading = true; - const result = await this.#readingTimeContext?.getReadingTime(this.contentKey!, this.dataTypeKey!, this.culture!); - this.loading = false; - this.initialised = true; + const labels: Record = { + day: ['day', 'days'], + hour: ['hour', 'hours'], + minute: ['minute', 'minutes'], + second: ['second', 'seconds'], + }; - if (!result) { - return; - } + const parts = filtered + .filter((u) => u.value > 0) + .map((u) => `${u.value} ${u.value === 1 ? labels[u.unit][0] : labels[u.unit][1]}`); + + if (parts.length === 0) { + const minLabel = this._minUnit === 'second' ? 'a second' : this._minUnit === 'minute' ? 'a minute' : this._minUnit === 'hour' ? 'an hour' : 'a day'; + return `Less than ${minLabel}`; + } + + return parts.join(', '); + } - this.data = result.data; + #renderVariationAlert() { + if (this._hideVariationWarning || this._culture) { + return nothing; } + + return html` +
+
+ + Language specific properties are not used in this calculation +
+
+ `; + } + + render() { + if (this.value == null) { + return html`Reading time will be calculated on save.`; + } + + return html` +
+ ${this.#renderVariationAlert()} + ${this.#formatTime(this.value)} +
+ `; + } + + static styles = css` + :host { + display: block; + } + em { + color: var(--uui-color-text-alt); + } + .alert { + background-color: darkgoldenrod; + padding: 5px; + margin-bottom: 5px; + } + .icon-container { + display: flex; + align-items: center; + } + .icon { + margin-right: 5px; + } + `; } declare global { - interface HTMLElementTagNameMap { - 'reading-time-property-editor-ui': ReadingTimePropertyEditorUi; - } + interface HTMLElementTagNameMap { + 'reading-time-property-editor-ui': ReadingTimePropertyEditorUi; + } } diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/index.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/index.ts index aa2c816..9afc159 100644 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/index.ts +++ b/src/jcdcdev.Umbraco.ReadingTime.Client/src/index.ts @@ -1,33 +1,8 @@ -import {manifests as editors} from './editors/manifest.ts'; -import {UMB_AUTH_CONTEXT} from "@umbraco-cms/backoffice/auth"; -import {UmbEntryPointOnInit} from "@umbraco-cms/backoffice/extension-api"; -import {ReadingTimeContext} from "./context/reading-time.context.ts"; -import {client} from './api'; +import { manifests as editors } from './editors/manifest.ts'; +import { UmbEntryPointOnInit } from "@umbraco-cms/backoffice/extension-api"; export const onInit: UmbEntryPointOnInit = (_host, extensionRegistry) => { extensionRegistry.registerMany([ ...editors, ]); - - _host.consumeContext(UMB_AUTH_CONTEXT, (_auth) => { - if (!_auth) { - console.error('No auth context found'); - return; - } - - const config = _auth.getOpenApiConfiguration(); - client.setConfig({ - auth: config.token, - baseUrl: config.base, - credentials: config.credentials, - }); - - client.interceptors.request.use(async (request, _options) => { - const token = await _auth.getLatestToken(); - request.headers.set('Authorization', `Bearer ${token}`); - return request; - }); - - new ReadingTimeContext(_host); - }); }; diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.datasource.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.datasource.ts deleted file mode 100644 index 7941e2d..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.datasource.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api"; -import { UmbDataSourceResponse } from "@umbraco-cms/backoffice/repository"; -import { tryExecute } from "@umbraco-cms/backoffice/resources"; -import { ReadingTimeResponse, ReadingTime } from "../api"; - -export class ReadingTimeDataSource implements IReadingTimeDataSource { - - #host: UmbControllerHost; - - constructor(host: UmbControllerHost) { - this.#host = host; - } - - async getReadingTime(contentKey: string, dataTypeKey: string, culture?: string): Promise> { - return await tryExecute(this.#host, ReadingTime.getUmbracoReadingTimeApiV1({ - query: { - contentKey: contentKey, - dataTypeKey: dataTypeKey, - culture: culture, - } - })) - } - -} - -export interface IReadingTimeDataSource { - getReadingTime(contentKey: string, dataTypeKey: string, culture?: string): Promise>; -} - diff --git a/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.repository.ts b/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.repository.ts deleted file mode 100644 index addd8e9..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime.Client/src/repository/reading-time.repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {UmbControllerHost} from "@umbraco-cms/backoffice/controller-api"; -import {UmbDataSourceResponse} from "@umbraco-cms/backoffice/repository"; -import {UmbControllerBase} from "@umbraco-cms/backoffice/class-api"; -import {ReadingTimeResponse} from "../api"; -import {IReadingTimeDataSource, ReadingTimeDataSource} from "./reading-time.datasource.ts"; - -export class ReadingTimeRepository extends UmbControllerBase { - #resource: IReadingTimeDataSource; - - constructor(host: UmbControllerHost) { - super(host); - this.#resource = new ReadingTimeDataSource(host); - } - - async getReadingTime(contentKey: string, dataTypeKey: string, culture?: string): Promise> { - return this.#resource.getReadingTime(contentKey, dataTypeKey, culture); - } -} - diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/Constants.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/Constants.cs index b31de31..abba37a 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/Constants.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/Constants.cs @@ -1,4 +1,4 @@ -namespace jcdcdev.Umbraco.ReadingTime.Core; +namespace jcdcdev.Umbraco.ReadingTime.Core; public static class Constants { @@ -19,11 +19,11 @@ public static class Configuration public const string HideVariationWarning = "hideVariationWarning"; } - public static class Api + public static class HealthCheck { - public const string ApiName = "ReadingTime"; - public const string Title = "Reading Time"; - public const string Description = "Reading Time API"; - public const string GroupName = "Reading Time"; + public const string Id = "E1F5B4A2-3C6D-4E8F-9A0B-1C2D3E4F5A6B"; + public const string Name = "Reading Time Data"; + public const string Description = "Checks that all content items with Reading Time properties have calculated values."; + public const string Group = "Content"; } } diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/Extensions/UmbracoBuilderExtensions.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/Extensions/UmbracoBuilderExtensions.cs index 2fa7d71..20afaaf 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/Extensions/UmbracoBuilderExtensions.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/Extensions/UmbracoBuilderExtensions.cs @@ -2,8 +2,6 @@ using jcdcdev.Umbraco.ReadingTime.Infrastructure; using jcdcdev.Umbraco.ReadingTime.Infrastructure.Indexing; using jcdcdev.Umbraco.ReadingTime.Infrastructure.Migrations; -using jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; -using jcdcdev.Umbraco.ReadingTime.Web; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; @@ -17,14 +15,10 @@ public static class UmbracoBuilderExtensions public static IUmbracoBuilder AddReadingTime(this IUmbracoBuilder builder) { builder.PackageMigrationPlans().Add(); - builder.Services.AddSingleton(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.Services.AddScoped(); + builder.AddNotificationAsyncHandler(); builder.ReadingTimeValueProviders().Append(); - builder.Services.AddSingleton(); - builder.ReadingTimeValueProviders().Append(); - builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); return builder; diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/IReadingTimeService.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/IReadingTimeService.cs index d920fd9..2066257 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/IReadingTimeService.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/IReadingTimeService.cs @@ -1,14 +1,8 @@ -using jcdcdev.Umbraco.ReadingTime.Core.Models; using Umbraco.Cms.Core.Models; namespace jcdcdev.Umbraco.ReadingTime.Core; public interface IReadingTimeService { - Task ScanTree(int homeId); - Task ScanAll(); - Task Process(IContent item); - Task DeleteAsync(Guid key); - Task GetAsync(Guid key, Guid dataTypeKey); - Task GetAsync(Guid key, int dataTypeId); + Task CalculateAndSetReadingTime(IContent content); } diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeDto.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeDto.cs deleted file mode 100644 index 8f10afc..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Umbraco.Extensions; - -namespace jcdcdev.Umbraco.ReadingTime.Core.Models; - -public class ReadingTimeDto -{ - public List Data { get; init; } = new(); - public int Id { get; init; } - public Guid Key { get; init; } - public int DataTypeId { get; init; } - public Guid DataTypeKey { get; set; } - public DateTime UpdateDate { get; set; } - - public ReadingTimeVariantDto? Value(string? culture = null) => Data.FirstOrDefault(x => x?.Culture.InvariantEquals(culture) ?? false); -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeVariantDto.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeVariantDto.cs deleted file mode 100644 index 2f1523e..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/Models/ReadingTimeVariantDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Runtime.Serialization; - -namespace jcdcdev.Umbraco.ReadingTime.Core.Models; - -public class ReadingTimeVariantDto -{ - [DataMember(Name = "culture")] public string? Culture { get; set; } - - [DataMember(Name = "readingTime")] public TimeSpan? ReadingTime { get; set; } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimeConfiguration.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimeConfiguration.cs index 5138024..4c19a0e 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimeConfiguration.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimeConfiguration.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Humanizer; using Umbraco.Cms.Core.PropertyEditors; diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimePropertyValueConverter.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimePropertyValueConverter.cs index f8751d3..6da80a0 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimePropertyValueConverter.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/PropertyEditors/ReadingTimePropertyValueConverter.cs @@ -1,4 +1,4 @@ -using jcdcdev.Umbraco.ReadingTime.Core.Models; +using jcdcdev.Umbraco.ReadingTime.Core.Models; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; @@ -6,12 +6,37 @@ namespace jcdcdev.Umbraco.ReadingTime.Core.PropertyEditors; public class ReadingTimePropertyValueConverter( - IReadingTimeService readingTimeService, IVariationContextAccessor variationContextAccessor, ILogger logger) : PropertyValueConverterBase { - private readonly ILogger _logger = logger; + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias == Constants.PropertyEditorAlias; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => + typeof(ReadingTimeValueModel); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => + PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate( + IPublishedElement owner, + IPublishedPropertyType propertyType, + object? source, + bool preview) + { + if (source is int seconds) + { + return TimeSpan.FromSeconds(seconds); + } + + if (source is string str && int.TryParse(str, out var parsed)) + { + return TimeSpan.FromSeconds(parsed); + } + + return null; + } public override object? ConvertIntermediateToObject( IPublishedElement owner, @@ -20,36 +45,19 @@ public class ReadingTimePropertyValueConverter( object? inter, bool preview) { - if (inter is not Guid key) + if (inter is not TimeSpan readingTime) { return null; } - var model = readingTimeService.GetAsync(key, propertyType.DataType.Id).GetAwaiter().GetResult(); - var culture = variationContextAccessor.VariationContext?.Culture; var config = propertyType.DataType.ConfigurationAs(); if (config is null) { - _logger.LogError("ReadingTime configuration is missing."); + logger.LogError("ReadingTime configuration is missing."); return null; } - var output = model?.Value(culture) ?? model?.Value(); - if (output is null) - { - return null; - } - - return new ReadingTimeValueModel(output.ReadingTime, config.Min, config.Max, output.Culture); + var culture = variationContextAccessor.VariationContext?.Culture; + return new ReadingTimeValueModel(readingTime, config.Min, config.Max, culture); } - - public override object? ConvertSourceToIntermediate( - IPublishedElement owner, - IPublishedPropertyType propertyType, - object? source, - bool preview) => owner.Key; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(ReadingTimeValueModel); - - public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditorAlias; } diff --git a/src/jcdcdev.Umbraco.ReadingTime/Core/ReadingTimeNotificationHandler.cs b/src/jcdcdev.Umbraco.ReadingTime/Core/ReadingTimeNotificationHandler.cs index ba7ff40..f186b55 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Core/ReadingTimeNotificationHandler.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Core/ReadingTimeNotificationHandler.cs @@ -1,26 +1,16 @@ -using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; namespace jcdcdev.Umbraco.ReadingTime.Core; -public class ReadingTimeNotificationHandler( - IReadingTimeService readingTimeService) : - INotificationAsyncHandler, - INotificationAsyncHandler +public class ReadingTimeNotificationHandler(IReadingTimeService calculationService) + : INotificationAsyncHandler { - public async Task HandleAsync(ContentDeletingNotification notification, CancellationToken cancellationToken) + public async Task HandleAsync(ContentSavingNotification notification, CancellationToken cancellationToken) { - foreach (var content in notification.DeletedEntities) + foreach (var content in notification.SavedEntities) { - await readingTimeService.DeleteAsync(content.Key); - } - } - - public async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) - { - foreach (var item in notification.PublishedEntities) - { - await readingTimeService.Process(item); + await calculationService.CalculateAndSetReadingTime(content); } } } diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/HealthChecks/ReadingTimeHealthCheck.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/HealthChecks/ReadingTimeHealthCheck.cs new file mode 100644 index 0000000..838e195 --- /dev/null +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/HealthChecks/ReadingTimeHealthCheck.cs @@ -0,0 +1,300 @@ +using jcdcdev.Umbraco.ReadingTime.Core; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.HealthChecks; + +[HealthCheck( + Constants.HealthCheck.Id, + Constants.HealthCheck.Name, + Description = Constants.HealthCheck.Description, + Group = Constants.HealthCheck.Group)] +public class ReadingTimeHealthCheck : HealthCheck +{ + private const int BatchSize = 300; + private const int PageSize = 100; + + private readonly IContentService _contentService; + private readonly IReadingTimeService _readingTimeService; + private readonly ILogger _logger; + + public ReadingTimeHealthCheck( + IContentService contentService, + IReadingTimeService readingTimeService, + ILogger logger) + { + _contentService = contentService; + _readingTimeService = readingTimeService; + _logger = logger; + } + + public override Task> GetStatusAsync() + { + var (total, missing) = CountContentWithMissingReadingTime(); + + if (total == 0) + { + var noContent = new HealthCheckStatus("No content types use a Reading Time property.") + { + ResultType = StatusResultType.Info + }; + return Task.FromResult>([noContent]); + } + + if (missing == 0) + { + var allGood = new HealthCheckStatus($"All {total} content items with Reading Time properties have calculated values.") + { + ResultType = StatusResultType.Success + }; + return Task.FromResult>([allGood]); + } + + var status = new HealthCheckStatus($"{missing} of {total} content items with Reading Time properties have missing values.") + { + ResultType = StatusResultType.Warning, + Actions = new List + { + new("recalculate-batch", Id) + { + Name = "Recalculate next batch", + Description = $"Recalculates reading time for the next {BatchSize} content items with missing values." + } + } + }; + + return Task.FromResult>([status]); + } + + public override async Task ExecuteActionAsync(HealthCheckAction action) + { + if (action.Alias != "recalculate-batch") + { + return new HealthCheckStatus("Unknown action.") + { + ResultType = StatusResultType.Error + }; + } + + try + { + var (processed, remaining) = await RecalculateBatch(); + + if (remaining > 0) + { + return new HealthCheckStatus($"Recalculated {processed} items. {remaining} remaining.") + { + ResultType = StatusResultType.Warning, + Actions = new List + { + new("recalculate-batch", Id) + { + Name = "Recalculate next batch", + Description = $"Recalculates reading time for the next {BatchSize} content items with missing values." + } + } + }; + } + + return new HealthCheckStatus($"Recalculated {processed} items. All items now have values.") + { + ResultType = StatusResultType.Success + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error recalculating reading times"); + return new HealthCheckStatus($"Error recalculating: {ex.Message}") + { + ResultType = StatusResultType.Error + }; + } + } + + private async Task<(int processed, int remaining)> RecalculateBatch() + { + var processed = 0; + var remaining = 0; + var batchComplete = false; + var rootContent = _contentService.GetRootContent().ToList(); + + foreach (var root in rootContent) + { + if (batchComplete) + { + remaining += CountMissingInTree(root); + continue; + } + + (processed, remaining, batchComplete) = await ProcessTree(root, processed); + } + + return (processed, remaining); + } + + private async Task<(int processed, int remaining, bool batchComplete)> ProcessTree(IContent content, int processed) + { + var remaining = 0; + var batchComplete = false; + + if (!batchComplete) + { + var hasReadingTimeProperty = content.Properties + .Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias); + + if (hasReadingTimeProperty && IsMissingValue(content)) + { + if (processed >= BatchSize) + { + batchComplete = true; + remaining++; + } + else + { + var wasPublished = content.Published; + await _readingTimeService.CalculateAndSetReadingTime(content); + + _contentService.Save(content); + if (wasPublished) + { + _contentService.Publish(content, content.AvailableCultures.ToArray()); + } + + processed++; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Recalculated reading time for {ContentName} (Id: {ContentId})", content.Name, content.Id); + } + } + } + } + + var page = 0; + var moreRecords = true; + while (moreRecords) + { + var children = _contentService + .GetPagedChildren(content.Id, page, PageSize, out var totalRecords) + .ToList(); + + foreach (var child in children) + { + if (batchComplete) + { + remaining += CountMissingInTree(child); + } + else + { + var result = await ProcessTree(child, processed); + processed = result.processed; + remaining += result.remaining; + batchComplete = result.batchComplete; + } + } + + page++; + moreRecords = (page + 1) * PageSize <= totalRecords; + } + + return (processed, remaining, batchComplete); + } + + private int CountMissingInTree(IContent content) + { + var missing = 0; + + var hasReadingTimeProperty = content.Properties + .Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias); + + if (hasReadingTimeProperty && IsMissingValue(content)) + { + missing++; + } + + var page = 0; + var moreRecords = true; + while (moreRecords) + { + var children = _contentService + .GetPagedChildren(content.Id, page, PageSize, out var totalRecords) + .ToList(); + + foreach (var child in children) + { + missing += CountMissingInTree(child); + } + + page++; + moreRecords = (page + 1) * PageSize <= totalRecords; + } + + return missing; + } + + private (int total, int missing) CountContentWithMissingReadingTime() + { + var total = 0; + var missing = 0; + + var rootContent = _contentService.GetRootContent().ToList(); + foreach (var root in rootContent) + { + CountInTree(root, ref total, ref missing); + } + + return (total, missing); + } + + private void CountInTree(IContent content, ref int total, ref int missing) + { + var readingTimeProperties = content.Properties + .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias) + .ToList(); + + if (readingTimeProperties.Count > 0) + { + total++; + if (IsMissingValue(content)) + { + missing++; + } + } + + var page = 0; + var moreRecords = true; + while (moreRecords) + { + var children = _contentService + .GetPagedChildren(content.Id, page, PageSize, out var totalRecords) + .ToList(); + + foreach (var child in children) + { + CountInTree(child, ref total, ref missing); + } + + page++; + moreRecords = (page + 1) * PageSize <= totalRecords; + } + } + + private static bool IsMissingValue(IContent content) + { + var readingTimeProperties = content.Properties + .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias) + .ToList(); + + return !readingTimeProperties.Any(p => + { + if (p.PropertyType.VariesByCulture()) + { + return content.AvailableCultures.Any(c => p.GetValue(c) != null); + } + + return p.GetValue() != null; + }); + } +} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Indexing/LegacyNestedContentReadingTimeValueProvider.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Indexing/LegacyNestedContentReadingTimeValueProvider.cs deleted file mode 100644 index f36cea4..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Indexing/LegacyNestedContentReadingTimeValueProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using jcdcdev.Umbraco.ReadingTime.Core.PropertyEditors; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Services; - -namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Indexing; - -public class LegacyNestedContentReadingTimeValueProvider(IContentTypeService contentTypeService) : ReadingTimeValueProviderBase -{ - private readonly DefaultPropertyIndexValueFactory _converter = new(); - - public override bool CanConvert(IPropertyType type) => type.PropertyEditorAlias is Constants.PropertyEditors.Aliases.NestedContent; - - public override TimeSpan? GetReadingTime(IProperty property, string? culture, string? segment, IEnumerable availableCultures, ReadingTimeConfiguration config) - { - // TODO - Improve this - var contentTypeDictionary = contentTypeService.GetAll().ToDictionary(x => x.Key, x => x); - var values = _converter.GetIndexValues(property, culture, segment, true, availableCultures, contentTypeDictionary); - return ProcessIndexValues(values, config.WordsPerMinute); - } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/0.3.1/RebuildDatabase.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/0.3.1/RebuildDatabase.cs index 1b6dfd0..1841184 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/0.3.1/RebuildDatabase.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/0.3.1/RebuildDatabase.cs @@ -1,48 +1,5 @@ -using jcdcdev.Umbraco.ReadingTime.Core; -using jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; -using Microsoft.Extensions.Logging; -using NPoco; using Umbraco.Cms.Infrastructure.Migrations; -using Umbraco.Cms.Infrastructure.Persistence; namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Migrations; -public class RebuildDatabase(IMigrationContext context) : AsyncMigrationBase(context) -{ - protected override Task MigrateAsync() - { - Logger.LogInformation("Rebuilding ReadingTime database"); - if (TableExists(Constants.TableName)) - { - // Check if foreign key exists - var fak = "FK_jcdcdevReadingTime_umbracoNode_uniqueId"; - var tableName = Constants.TableName; - if (ConstraintExists(Context.Database, tableName, fak)) - { - Delete.ForeignKey(fak).OnTable(Constants.TableName).Do(); - } - - Delete.Table(Constants.TableName).Do(); - } - - Create.Table().Do(); - - return Task.CompletedTask; - } - - private static bool ConstraintExists(IUmbracoDatabase database, string tableName, string key) - { - string sql; - if (database.SqlContext.DatabaseType == DatabaseType.SQLite) - { - sql = $"SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = '{key}' AND tbl_name = '{tableName}'"; - } - else - { - sql = $"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '{key}' AND TABLE_NAME = '{tableName}'"; - } - - var count = database.ExecuteScalar(sql); - return count > 0; - } -} +public class RebuildDatabase(IMigrationContext context) : NoopMigration(context); diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/14.0.0/AddUpdateDate.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/14.0.0/AddUpdateDate.cs index 4660b45..9e115d3 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/14.0.0/AddUpdateDate.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/14.0.0/AddUpdateDate.cs @@ -1,28 +1,5 @@ -using jcdcdev.Umbraco.ReadingTime.Core; -using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Migrations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Migrations; -public class AddUpdateDate(IMigrationContext context) : AsyncMigrationBase(context) -{ - protected override Task MigrateAsync() - { - Logger.LogInformation("Adding updateDate column to table {Table}", Constants.TableName); - - if (ColumnExists(Constants.TableName, "updateDate")) - { - return Task.CompletedTask; - } - - Alter.Table(Constants.TableName) - .AddColumn("updateDate") - .AsDateTime() - .NotNullable() - .WithDefault(SystemMethods.CurrentDateTime) - .Do(); - - return Task.CompletedTask; - } -} +public class AddUpdateDate(IMigrationContext context) : NoopMigration(context); diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/17.0.0/DropReadingTimeTable.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/17.0.0/DropReadingTimeTable.cs new file mode 100644 index 0000000..cb07206 --- /dev/null +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/17.0.0/DropReadingTimeTable.cs @@ -0,0 +1,58 @@ +using jcdcdev.Umbraco.ReadingTime.Core; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Migrations; + +public class DropReadingTimeTable(IMigrationContext context) : AsyncMigrationBase(context) +{ + protected override Task MigrateAsync() + { + if (!TableExists(Constants.TableName)) + { + Logger.LogInformation("Table {TableName} does not exist, nothing to drop", Constants.TableName); + return Task.CompletedTask; + } + + Logger.LogInformation("Dropping table {TableName}", Constants.TableName); + + var foreignKeys = new[] + { + "FK_jcdcdevReadingTime_content_umbracoNode_uniqueId", + "FK_jcdcdevReadingTime_dataTypeKey_umbracoNode_uniqueId", + "FK_jcdcdevReadingTime_dataTypeId_umbracoNode_uniqueId", + "FK_jcdcdevReadingTime_umbracoNode_uniqueId" + }; + + foreach (var fk in foreignKeys) + { + if (ConstraintExists(Context.Database, Constants.TableName, fk)) + { + Delete.ForeignKey(fk).OnTable(Constants.TableName).Do(); + } + } + + Delete.Table(Constants.TableName).Do(); + + Logger.LogInformation("Table {TableName} dropped successfully", Constants.TableName); + return Task.CompletedTask; + } + + private static bool ConstraintExists(IUmbracoDatabase database, string tableName, string key) + { + string sql; + if (database.SqlContext.DatabaseType == DatabaseType.SQLite) + { + sql = $"SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = '{key}' AND tbl_name = '{tableName}'"; + } + else + { + sql = $"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '{key}' AND TABLE_NAME = '{tableName}'"; + } + + var count = database.ExecuteScalar(sql); + return count > 0; + } +} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/MigrationPlan.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/MigrationPlan.cs index ee874a6..e20f1d6 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/MigrationPlan.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Migrations/MigrationPlan.cs @@ -1,4 +1,4 @@ -using jcdcdev.Umbraco.ReadingTime.Core; +using jcdcdev.Umbraco.ReadingTime.Core; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Infrastructure.Migrations; @@ -13,6 +13,7 @@ protected override void DefinePlan() To(); To(); To(); + To(); } private void To() where T : AsyncMigrationBase diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/IReadingTimeRepository.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/IReadingTimeRepository.cs deleted file mode 100644 index 0495ef9..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/IReadingTimeRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using jcdcdev.Umbraco.ReadingTime.Core.Models; -using Umbraco.Cms.Core.Models; - -namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; - -public interface IReadingTimeRepository -{ - Task DeleteAsync(Guid key); - Task GetOrCreate(Guid key, IDataType dataType); - Task PersistAsync(ReadingTimeDto dto); - Task Get(Guid key, int dataTypeId); - Task Get(Guid key, Guid dataTypeKey); -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimePoco.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimePoco.cs deleted file mode 100644 index 3580694..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimePoco.cs +++ /dev/null @@ -1,43 +0,0 @@ -using jcdcdev.Umbraco.ReadingTime.Core; -using NPoco; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; - -namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; - -[TableName(Constants.TableName)] -[PrimaryKey("id")] -[ExplicitColumns] -public class ReadingTimePoco -{ - [Column(Name = "id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column(Name = "key")] - [ForeignKey(typeof(NodeDto), Column = "uniqueId", Name = "FK_jcdcdevReadingTime_content_umbracoNode_uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public Guid Key { get; set; } - - [Column(Name = "dataTypeKey")] - [ForeignKey(typeof(NodeDto), Column = "uniqueId", Name = "FK_jcdcdevReadingTime_dataTypeKey_umbracoNode_uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public Guid DataTypeKey { get; set; } - - [Column("data")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - public string? TextData { get; set; } - - [Column(Name = "dataTypeId")] - [ForeignKey(typeof(NodeDto), Column = "id", Name = "FK_jcdcdevReadingTime_dataTypeId_umbracoNode_uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int DataTypeId { get; set; } - - [Column(Name = "updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - [ComputedColumn(ComputedColumnType.ComputedOnInsert)] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime UpdateDate { get; set; } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimeRepository.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimeRepository.cs deleted file mode 100644 index c06350a..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/Persistence/ReadingTimeRepository.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Text.Json; -using jcdcdev.Umbraco.ReadingTime.Core.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Extensions; - -namespace jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; - -public class ReadingTimeRepository(IScopeProvider scopeProvider) : IReadingTimeRepository -{ - public Task DeleteAsync(Guid key) - { - using var scope = scopeProvider.CreateScope(); - - var sql = scope.SqlContext - .Sql() - .Delete() - .Where(x => x.Key == key); - - var data = scope.Database.Execute(sql); - - scope.Complete(); - - return Task.FromResult(data); - } - - public async Task GetOrCreate(Guid key, IDataType dataType) - { - var dto = await Get(key, dataType.Id); - if (dto != null) - { - return dto; - } - - return new ReadingTimeDto - { - Key = key, - DataTypeId = dataType.Id, - DataTypeKey = dataType.Key, - UpdateDate = DateTime.UtcNow - }; - } - - public async Task PersistAsync(ReadingTimeDto dto) - { - var poco = new ReadingTimePoco - { - Id = dto.Id, - Key = dto.Key, - TextData = JsonSerializer.Serialize(dto.Data), - DataTypeId = dto.DataTypeId, - DataTypeKey = dto.DataTypeKey, - UpdateDate = dto.UpdateDate - }; - - using var scope = scopeProvider.CreateScope(); - - await scope.Database.SaveAsync(poco); - - scope.Complete(); - } - - public async Task Get(Guid key, int dataTypeId) - { - using var scope = scopeProvider.CreateScope(); - - var sql = scope.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == key && x.DataTypeId == dataTypeId); - - var result = await scope.Database.FetchAsync(sql); - - scope.Complete(); - - return Map(result.FirstOrDefault()); - } - - public async Task Get(Guid key, Guid dataTypeKey) - { - using var scope = scopeProvider.CreateScope(); - - var sql = scope.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == key && x.DataTypeKey == dataTypeKey); - - var result = await scope.Database.FetchAsync(sql); - - scope.Complete(); - - return Map(result.FirstOrDefault()); - } - - private static ReadingTimeDto? Map(ReadingTimePoco? result) - { - var record = result; - if (record == null) - { - return null; - } - - var data = new List(); - if (!record.TextData.IsNullOrWhiteSpace()) - { - var attempt = JsonSerializer.Deserialize>(record.TextData); - if (attempt != null) - { - data = attempt; - } - } - - return new ReadingTimeDto - { - Id = record.Id, - Key = record.Key, - DataTypeId = record.DataTypeId, - DataTypeKey = record.DataTypeKey, - Data = data, - UpdateDate = record.UpdateDate - }; - } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/ReadingTimeService.cs b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/ReadingTimeService.cs index 4b71325..3ee1c44 100644 --- a/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/ReadingTimeService.cs +++ b/src/jcdcdev.Umbraco.ReadingTime/Infrastructure/ReadingTimeService.cs @@ -1,8 +1,6 @@ -using jcdcdev.Umbraco.ReadingTime.Core; +using jcdcdev.Umbraco.ReadingTime.Core; using jcdcdev.Umbraco.ReadingTime.Core.Composing; -using jcdcdev.Umbraco.ReadingTime.Core.Models; using jcdcdev.Umbraco.ReadingTime.Core.PropertyEditors; -using jcdcdev.Umbraco.ReadingTime.Infrastructure.Persistence; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -10,176 +8,106 @@ namespace jcdcdev.Umbraco.ReadingTime.Infrastructure; -public class ReadingTimeService( - IContentService contentService, - ReadingTimeValueProviderCollection convertors, - IReadingTimeRepository readingTimeRepository, - IDataTypeService dataTypeService, - ILogger logger) - : IReadingTimeService +public class ReadingTimeService : IReadingTimeService { - private readonly ILogger _logger = logger; - - public async Task GetAsync(Guid key, Guid dataTypeKey) => await readingTimeRepository.Get(key, dataTypeKey); - - public async Task GetAsync(Guid key, int dataTypeId) => await readingTimeRepository.Get(key, dataTypeId); - - public async Task DeleteAsync(Guid key) + private readonly ReadingTimeValueProviderCollection _valueProviders; + private readonly IDataTypeService _dataTypeService; + private readonly ILogger _logger; + + public ReadingTimeService( + ReadingTimeValueProviderCollection valueProviders, + IDataTypeService dataTypeService, + ILogger logger) { - _logger.LogDebug("Deleting reading time for {Key}", key); - return await readingTimeRepository.DeleteAsync(key); + _valueProviders = valueProviders; + _dataTypeService = dataTypeService; + _logger = logger; } - public async Task ScanTree(int homeId) + public async Task CalculateAndSetReadingTime(IContent content) { - var content = contentService.GetById(homeId); - if (content == null) - { - _logger.LogWarning("Content with id {HomeId} not found", homeId); - return; - } - - var queue = new Queue(); - queue.Enqueue(content); - - while (queue.TryDequeue(out var current)) - { - var moreRecords = true; - var page = 0; - while (moreRecords) - { - var children = contentService - .GetPagedChildren(current.Id, page, 100, out var totalRecords) - .ToList(); - - foreach (var child in children) - { - queue.Enqueue(child); - } - - page++; - moreRecords = (page + 1) * 100 <= totalRecords; - } + var readingTimeProperties = content.Properties + .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias) + .ToList(); - if (current.Published) - { - await Process(current); - } - } - } - - public async Task ScanAll() - { - var root = contentService.GetRootContent().ToList(); - _logger.LogInformation("Scanning {Count} root content items", root.Count); - foreach (var content in root) - { - await ScanTree(content.Id); - } - } - - public async Task Process(IContent item) - { - var props = item.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias).ToList(); - if (!props.Any()) + if (readingTimeProperties.Count == 0) { return; } - _logger.LogDebug("Processing {Id}:{Item}", item.Id, item.Name); - foreach (var property in props) + foreach (var readingTimeProperty in readingTimeProperties) { - await ProcessPropertyEditor(item, property); + await ProcessReadingTimeProperty(content, readingTimeProperty); } } - private async Task ProcessPropertyEditor(IContent item, IProperty readingTimeProperty) + private async Task ProcessReadingTimeProperty(IContent content, IProperty readingTimeProperty) { - var dataType = await dataTypeService.GetAsync(readingTimeProperty.PropertyType.DataTypeKey); + var dataType = await _dataTypeService.GetAsync(readingTimeProperty.PropertyType.DataTypeKey); if (dataType == null) { - _logger.LogWarning("DataType not found for property {PropertyId}", readingTimeProperty.Id); + _logger.LogWarning("DataType not found for property {PropertyAlias}", readingTimeProperty.Alias); return; } var config = dataType.ConfigurationAs(); if (config == null) { - _logger.LogWarning("Configuration not found for property {PropertyId}", readingTimeProperty.Id); + _logger.LogWarning("Configuration not found for property {PropertyAlias}", readingTimeProperty.Alias); return; } - var dto = await readingTimeRepository.GetOrCreate(item.Key, dataType); - dto.UpdateDate = DateTime.UtcNow; - var models = new List(); var propertyType = readingTimeProperty.PropertyType; if (propertyType.VariesByCulture()) { - _logger.LogDebug("Processing culture variants for {Id}:{Item}", item.Id, item.Name); - foreach (var culture in item.AvailableCultures) + foreach (var culture in content.AvailableCultures) { - _logger.LogDebug("Processing culture {Culture}", culture); - var model = GetModel(item, culture, null, config); - models.Add(model); + var totalSeconds = CalculateTotalSeconds(content, culture, null, config); + content.SetValue(readingTimeProperty.Alias, totalSeconds, culture); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set reading time for {ContentName} ({Culture}): {Seconds}s", + content.Name, culture, totalSeconds); + } } } - - _logger.LogDebug("Processing invariant variant for {Id}:{Item}", item.Id, item.Name); - var invariant = GetModel(item, null, null, config); - models.Add(invariant); - - var merge = dto.Data.Where(x => !models.Select(y => y?.Culture).Contains(x?.Culture)).ToList(); - if (merge.Any()) + else { - models.AddRange(merge); - _logger.LogDebug("Merging {Count} existing models", merge.Count()); + var totalSeconds = CalculateTotalSeconds(content, null, null, config); + content.SetValue(readingTimeProperty.Alias, totalSeconds); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set reading time for {ContentName}: {Seconds}s", content.Name, totalSeconds); + } } - - dto.Data.Clear(); - dto.Data.AddRange(models); - - await readingTimeRepository.PersistAsync(dto); - } - - private ReadingTimeVariantDto GetModel(IContent item, string? culture, string? segment, ReadingTimeConfiguration config) - { - var readingTime = GetReadingTime(item, culture, segment, config); - var model = new ReadingTimeVariantDto - { - Culture = culture, - ReadingTime = readingTime - }; - - return model; } - private TimeSpan? GetReadingTime(IContent item, string? culture, string? segment, ReadingTimeConfiguration config) + private int CalculateTotalSeconds(IContent content, string? culture, string? segment, ReadingTimeConfiguration config) { var time = TimeSpan.Zero; - foreach (var property in item.Properties) + + foreach (var property in content.Properties) { - var convertor = convertors.FirstOrDefault(x => x.CanConvert(property.PropertyType)); - if (convertor == null) + if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditorAlias) { - _logger.LogDebug("No convertor found for {PropertyId}:{PropertyEditorAlias}", property.Id, property.PropertyType.PropertyEditorAlias); continue; } - _logger.LogDebug("Processing property {PropertyId}:{PropertyEditorAlias}", property.Id, property.PropertyType.PropertyEditorAlias); - - var cCulture = property.PropertyType.VariesByCulture() ? culture : null; - var cSegment = property.PropertyType.VariesBySegment() ? segment : null; - var readingTime = convertor?.GetReadingTime(property, cCulture, cSegment, item.AvailableCultures, config); - if (!readingTime.HasValue) + var provider = _valueProviders.FirstOrDefault(x => x.CanConvert(property.PropertyType)); + if (provider == null) { - _logger.LogDebug("No reading time found for {PropertyId}:{PropertyEditorAlias}", property.Id, property.PropertyType.PropertyEditorAlias); continue; } - _logger.LogDebug("Reading time found for {PropertyId}:{PropertyEditorAlias} ({Time})", property.Id, property.PropertyType.PropertyEditorAlias, readingTime.Value); - time += readingTime.Value; + var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null; + var propertySegment = property.PropertyType.VariesBySegment() ? segment : null; + var readingTime = provider.GetReadingTime(property, propertyCulture, propertySegment, content.AvailableCultures, config); + if (readingTime.HasValue) + { + time += readingTime.Value; + } } - return time; + return (int)time.TotalSeconds; } } diff --git a/src/jcdcdev.Umbraco.ReadingTime/Web/ConfigApiSwaggerGenOptions.cs b/src/jcdcdev.Umbraco.ReadingTime/Web/ConfigApiSwaggerGenOptions.cs deleted file mode 100644 index 57988bd..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Web/ConfigApiSwaggerGenOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using jcdcdev.Umbraco.ReadingTime.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace jcdcdev.Umbraco.ReadingTime.Web; - -public class ConfigApiSwaggerGenOptions : IConfigureOptions -{ - public void Configure(SwaggerGenOptions options) - { - options.SwaggerDoc(Constants.Api.ApiName, - new OpenApiInfo - { - Title = Constants.Api.Title, - Version = "Latest", - Description = Constants.Api.Description - }); - } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Web/Controllers/ReadingTimeController.cs b/src/jcdcdev.Umbraco.ReadingTime/Web/Controllers/ReadingTimeController.cs deleted file mode 100644 index d20ea1d..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Web/Controllers/ReadingTimeController.cs +++ /dev/null @@ -1,52 +0,0 @@ -using jcdcdev.Umbraco.ReadingTime.Core; -using jcdcdev.Umbraco.ReadingTime.Core.Extensions; -using jcdcdev.Umbraco.ReadingTime.Core.PropertyEditors; -using jcdcdev.Umbraco.ReadingTime.Web.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Common.Attributes; -using Umbraco.Cms.Api.Common.Filters; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.Authorization; -using Umbraco.Extensions; - -namespace jcdcdev.Umbraco.ReadingTime.Web.Controllers; - -[ApiExplorerSettings(GroupName = Constants.Api.GroupName)] -[ReadingTimeRoute("")] -[MapToApi(Constants.Api.ApiName)] -[JsonOptionsName(global::Umbraco.Cms.Core.Constants.JsonOptionsNames.BackOffice)] -[ApiController] -[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] -[Produces("application/json")] -public class ReadingTimeController(IReadingTimeService service, IDataTypeService dataTypeService) : ControllerBase -{ - [HttpGet] - [Produces(typeof(ReadingTimeResponse))] - public async Task Get(string contentKey, string dataTypeKey, string? culture = null) - { - Guid.TryParse(contentKey, out var contentGuid); - Guid.TryParse(dataTypeKey, out var dataTypeGuid); - var readingTime = await service.GetAsync(contentGuid, dataTypeGuid); - if (readingTime == null) - { - return NoContent(); - } - - var value = readingTime.Value(culture); - if (value == null) - { - return NoContent(); - } - - var dataType = await dataTypeService.GetAsync(dataTypeGuid); - var config = dataType?.ConfigurationAs(); - if (config == null) - { - return BadRequest(); - } - - var model = new ReadingTimeResponse(value.ReadingTime.DisplayTime(config.Min, config.Max, culture), readingTime.UpdateDate); - return Ok(model); - } -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Web/Models/ReadingTimeResponse.cs b/src/jcdcdev.Umbraco.ReadingTime/Web/Models/ReadingTimeResponse.cs deleted file mode 100644 index c2dbdce..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Web/Models/ReadingTimeResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace jcdcdev.Umbraco.ReadingTime.Web.Models; - -public class ReadingTimeResponse(string readingTime, DateTime updateDate) -{ - public DateTime UpdateDate { get; } = updateDate; - public string ReadingTime { get; } = readingTime; -} diff --git a/src/jcdcdev.Umbraco.ReadingTime/Web/ReadingTimeRouteAttribute.cs b/src/jcdcdev.Umbraco.ReadingTime/Web/ReadingTimeRouteAttribute.cs deleted file mode 100644 index 694e37a..0000000 --- a/src/jcdcdev.Umbraco.ReadingTime/Web/ReadingTimeRouteAttribute.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Umbraco.Cms.Web.Common.Routing; - -namespace jcdcdev.Umbraco.ReadingTime.Web; - -public class ReadingTimeRouteAttribute(string template) : BackOfficeRouteAttribute($"ReadingTime/api/v{{version:apiVersion}}/{template.TrimStart('/')}");