From 8f40d5d3fd02479b23e622fff3f50e5fe78bc48b Mon Sep 17 00:00:00 2001 From: msynk Date: Wed, 2 Sep 2026 09:01:47 +0330 Subject: [PATCH 1/2] add missing CLAUDE.md and apply rules #13118 --- .../Bit.Butil.Build/ButilConsumerScan.cs | 21 -- src/Butil/Bit.Butil.Build/ButilScanMode.cs | 27 ++ .../Bit.Butil.Build/ButilScriptBundler.cs | 8 - .../Bit.Butil.Build/ButilScriptManifest.cs | 15 ++ src/Butil/Bit.Butil.Build/MetadataHeap.cs | 15 ++ src/Butil/Bit.Butil.Build/MetadataTables.cs | 46 ---- src/Butil/Bit.Butil.Build/MetadataToken.cs | 13 + src/Butil/Bit.Butil.Build/PeImage.cs | 10 - src/Butil/Bit.Butil.Build/TypeDefinition.cs | 20 ++ src/Butil/Bit.Butil.Build/TypeName.cs | 22 ++ .../Bit.Butil.Demo/Client/Docs/ApiNeeds.cs | 25 ++ .../Bit.Butil.Demo/Client/Docs/ApiSupport.cs | 30 +++ .../Bit.Butil.Demo/Client/Docs/DocGroup.cs | 12 + .../Bit.Butil.Demo/Client/Docs/DocLink.cs | 23 ++ .../Bit.Butil.Demo/Client/Docs/DocsNav.cs | 74 ------ .../Client/Docs/DocsSearchEntry.cs | 34 +++ .../Client/Docs/DocsSearchIndex.cs | 33 --- .../Client/Services/PageOutline.cs | 3 - .../Client/Services/PageOutlineItem.cs | 4 + .../Server/Dtos/ButilApiDetailsResultDto.cs | 18 ++ .../Server/Dtos/ButilApiInspectionDto.cs | 44 ++++ .../Server/Dtos/ButilApiMemberDto.cs | 24 ++ .../Server/Dtos/ButilApiTypeDetailsDto.cs | 25 ++ .../Server/Dtos/ButilApiTypeDto.cs | 15 ++ .../Server/Dtos/ButilCapabilityDto.cs | 22 ++ .../Server/Dtos/ButilFeaturePlanDto.cs | 29 +++ .../Server/Dtos/ButilGuideSectionDto.cs | 17 ++ .../Server/Dtos/ButilMcpDtos.cs | 234 ------------------ .../Server/Dtos/ButilSearchHitDto.cs | 19 ++ .../Server/Dtos/ButilSearchResultDto.cs | 15 ++ .../Server/Dtos/ButilSourceFileDto.cs | 16 ++ src/Butil/Bit.Butil/Bit.Butil.csproj | 5 + src/Butil/Bit.Butil/BitButil.cs | 4 + .../Extensions/JSRuntimeExtensions.cs | 5 + src/Butil/Bit.Butil/Extensions/LinkerFlags.cs | 4 + .../Publics/Animation/AnimationHandle.cs | 1 + .../BarcodeDetector/DetectedBarcode.cs | 2 + .../Bit.Butil/Publics/BroadcastChannel.cs | 1 + src/Butil/Bit.Butil/Publics/CacheStorage.cs | 1 + .../Publics/CacheStorage/CachedResponse.cs | 7 + .../Publics/Clipboard/ClipboardFormats.cs | 12 + .../Publics/Clipboard/ClipboardItem.cs | 19 ++ .../Bit.Butil/Publics/Contacts/ContactInfo.cs | 5 + .../Bit.Butil/Publics/Cookie/ButilCookie.cs | 59 +++++ .../Publics/Cookie/CookieStoreItem.cs | 14 ++ .../Bit.Butil/Publics/Cookie/SameSite.cs | 8 +- .../Crypto/AesCbcCryptoAlgorithmParams.cs | 1 + .../Crypto/AesCtrCryptoAlgorithmParams.cs | 1 + .../Crypto/AesGcmCryptoAlgorithmParams.cs | 1 + .../Publics/Crypto/AesGcmTagLength.cs | 13 + .../Publics/Crypto/CryptoAlgorithm.cs | 14 ++ .../Bit.Butil/Publics/Crypto/CryptoKeyHash.cs | 11 + .../Publics/Crypto/ICryptoAlgorithmParams.cs | 9 + .../Crypto/RsaOaepCryptoAlgorithmParams.cs | 1 + src/Butil/Bit.Butil/Publics/Document.cs | 17 ++ .../Bit.Butil/Publics/Document/CompatMode.cs | 5 + .../Bit.Butil/Publics/Document/DesignMode.cs | 5 + .../Bit.Butil/Publics/Document/DocumentDir.cs | 5 + .../Publics/Element/ContentEditable.cs | 6 +- .../Bit.Butil/Publics/Element/ElementDir.cs | 5 + .../Bit.Butil/Publics/Element/EnterKeyHint.cs | 5 + .../Publics/Element/FullScreenNavigationUI.cs | 20 ++ ...lscreenOptions.cs => FullScreenOptions.cs} | 9 + .../Publics/Element/FullscreenNavigationUI.cs | 8 - src/Butil/Bit.Butil/Publics/Element/Hidden.cs | 5 + .../Bit.Butil/Publics/Element/InputMode.cs | 5 + .../Publics/Element/MediaElementState.cs | 4 + src/Butil/Bit.Butil/Publics/Element/Rect.cs | 12 + .../Publics/Element/ScrollBehavior.cs | 13 + .../Publics/Element/ScrollIntoViewOptions.cs | 16 ++ .../Publics/Element/ScrollLogicalPosition.cs | 13 + .../Publics/Element/ScrollOptions.cs | 10 + .../Publics/Events/ButilDragEventArgs.cs | 33 ++- .../Bit.Butil/Publics/Events/ButilEvents.cs | 140 ++++++++++- .../Bit.Butil/Publics/Events/ButilKeyCodes.cs | 118 +++++++++ .../Publics/Events/ButilModifiers.cs | 16 ++ .../Publics/Events/ButilPointerEventArgs.cs | 37 ++- .../Publics/Events/ButilTouchEventArgs.cs | 32 +-- .../Publics/Events/ButilTouchPoint.cs | 42 ++++ .../Publics/Events/ButilWheelEventArgs.cs | 29 ++- src/Butil/Bit.Butil/Publics/Fetch.cs | 1 + .../Bit.Butil/Publics/Fetch/AbortableFetch.cs | 1 + .../Bit.Butil/Publics/Fetch/FetchRequest.cs | 1 + .../Bit.Butil/Publics/Fetch/FetchResponse.cs | 1 + .../Publics/FileSystem/FileSystemFileInfo.cs | 1 + .../Bit.Butil/Publics/Gamepad/GamepadState.cs | 1 + src/Butil/Bit.Butil/Publics/Geolocation.cs | 1 + ...cationError.cs => GeolocationErrorCode.cs} | 13 - .../Geolocation/GeolocationException.cs | 18 ++ src/Butil/Bit.Butil/Publics/History.cs | 6 + .../Publics/History/ScrollRestoration.cs | 5 + src/Butil/Bit.Butil/Publics/IdleDetector.cs | 1 + .../IndexedDb/IndexedDbDatabaseInfo.cs | 17 ++ .../Publics/IndexedDb/IndexedDbDurability.cs | 17 ++ .../Publics/IndexedDb/IndexedDbHandle.cs | 1 + .../Publics/IndexedDb/IndexedDbIndexInfo.cs | 17 ++ .../Publics/IndexedDb/IndexedDbIndexSchema.cs | 26 ++ .../Publics/IndexedDb/IndexedDbInfo.cs | 59 ----- .../Publics/IndexedDb/IndexedDbKeyRecord.cs | 15 ++ .../Publics/IndexedDb/IndexedDbOpenInfo.cs | 12 + .../Publics/IndexedDb/IndexedDbRecord.cs | 12 - .../Publics/IndexedDb/IndexedDbStoreInfo.cs | 20 ++ .../Publics/IndexedDb/IndexedDbStoreSchema.cs | 22 +- .../IndexedDb/IndexedDbTransactionMode.cs | 16 -- .../IntersectionObserverEntry.cs | 5 + src/Butil/Bit.Butil/Publics/Keyboard.cs | 20 ++ .../Bit.Butil/Publics/Locks/WebLockInfo.cs | 14 ++ .../Publics/Locks/WebLockSnapshot.cs | 11 +- .../MediaDevices/DisplayMediaSettings.cs | 2 + .../Publics/MediaDevices/MediaDeviceInfo.cs | 4 + .../Publics/MediaDevices/MediaStreamHandle.cs | 1 + .../Publics/MediaSession/MediaMetadata.cs | 3 + .../MediaSession/MediaSessionAction.cs | 8 + .../Publics/Navigation/NavigationEntry.cs | 28 --- .../Publics/Navigation/NavigationEventInfo.cs | 16 ++ .../Navigation/NavigationHistoryBehavior.cs | 14 ++ .../Bit.Butil/Publics/Navigator/ShareData.cs | 18 ++ src/Butil/Bit.Butil/Publics/Nfc.cs | 1 + .../Bit.Butil/Publics/Nfc/NdefMessage.cs | 11 + src/Butil/Bit.Butil/Publics/Nfc/NdefRecord.cs | 7 - src/Butil/Bit.Butil/Publics/Notification.cs | 1 + .../Notification/NotificationDirection.cs | 11 +- .../Notification/NotificationHandle.cs | 1 + .../Notification/NotificationPermission.cs | 5 + src/Butil/Bit.Butil/Publics/ObjectUrls.cs | 1 + src/Butil/Bit.Butil/Publics/Performance.cs | 1 + .../Publics/Performance/PerformanceMemory.cs | 5 + src/Butil/Bit.Butil/Publics/Reporting.cs | 1 + .../ResizeObserver/ResizeObserverBox.cs | 5 + .../ResizeObserver/ResizeObserverEntry.cs | 9 + src/Butil/Bit.Butil/Publics/Screen.cs | 6 + .../Bit.Butil/Publics/ScreenOrientation.cs | 6 + .../ScreenOrientation/OrientationLockType.cs | 21 ++ .../ScreenOrientation/OrientationState.cs | 17 ++ .../ScreenOrientationType.cs | 14 ++ src/Butil/Bit.Butil/Publics/ServiceWorker.cs | 1 + .../Bit.Butil/Publics/Speech/SpeechVoice.cs | 1 + .../Bit.Butil/Publics/SpeechRecognition.cs | 1 + .../Bit.Butil/Publics/Storage/ButilStorage.cs | 1 + .../Publics/StorageManager/StorageEstimate.cs | 12 +- .../StorageManager/StorageUsageDetail.cs | 11 + .../Publics/UserAgent/HighEntropyUserAgent.cs | 19 ++ .../Publics/UserAgent/UserAgentBrand.cs | 3 + .../Publics/UserAgent/UserAgentProperties.cs | 32 +++ src/Butil/Bit.Butil/Publics/VisualViewport.cs | 6 + src/Butil/Bit.Butil/Publics/WakeLock.cs | 1 + .../Publics/WebAudio/AudioPlaybackHandle.cs | 1 + .../WebAuthn/WebAuthnVerifyAllowCredential.cs | 8 + .../WebAuthnVerifyAuthenticatorSelection.cs | 9 + .../Publics/WebAuthn/WebAuthnVerifyOptions.cs | 35 +++ .../WebAuthn/WebAuthnVerifyPubKeyCredParam.cs | 11 + .../Publics/WebAuthn/WebAuthnVerifyRp.cs | 7 + .../Publics/WebAuthn/WebAuthnVerifyUser.cs | 15 ++ src/Butil/Bit.Butil/Publics/Window.cs | 15 ++ src/Butil/Bit.Butil/Publics/Window/BarProp.cs | 9 + .../Publics/Window/MediaQueryList.cs | 10 + .../Publics/Window/WindowFeatures.cs | 28 +++ src/Butil/CLAUDE.md | 105 ++++++++ .../Bit.Butil.Tests.Manual/InteropContract.cs | 58 ----- .../Bit.Butil.Tests.Manual/InteropManifest.cs | 14 ++ .../Bit.Butil.Tests.Manual/TypeContract.cs | 58 +++++ .../Infrastructure/ApiDetailsResult.cs | 8 + .../Infrastructure/ApiInspection.cs | 18 ++ .../Infrastructure/ApiMember.cs | 8 + .../Infrastructure/ApiType.cs | 8 + .../Infrastructure/ApiTypeDetails.cs | 17 ++ .../{WireContracts.cs => ButilMcp.cs} | 116 --------- .../Infrastructure/DocsIndexRow.cs | 90 +++++++ .../Infrastructure/FeaturePlan.cs | 20 ++ .../Infrastructure/SearchHit.cs | 8 + .../Infrastructure/SearchResult.cs | 8 + .../Infrastructure/ToolCallReference.cs | 34 +++ .../Infrastructure/WireDtos.cs | 53 ---- 173 files changed, 2267 insertions(+), 864 deletions(-) create mode 100644 src/Butil/Bit.Butil.Build/ButilScanMode.cs create mode 100644 src/Butil/Bit.Butil.Build/ButilScriptManifest.cs create mode 100644 src/Butil/Bit.Butil.Build/MetadataHeap.cs create mode 100644 src/Butil/Bit.Butil.Build/MetadataToken.cs create mode 100644 src/Butil/Bit.Butil.Build/TypeDefinition.cs create mode 100644 src/Butil/Bit.Butil.Build/TypeName.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Docs/ApiNeeds.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Docs/ApiSupport.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Docs/DocGroup.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Docs/DocLink.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchEntry.cs create mode 100644 src/Butil/Bit.Butil.Demo/Client/Services/PageOutlineItem.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiDetailsResultDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiInspectionDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiMemberDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDetailsDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilCapabilityDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilFeaturePlanDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilGuideSectionDto.cs delete mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilMcpDtos.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchHitDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchResultDto.cs create mode 100644 src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSourceFileDto.cs create mode 100644 src/Butil/Bit.Butil/Publics/Element/FullScreenNavigationUI.cs rename src/Butil/Bit.Butil/Publics/Element/{FullscreenOptions.cs => FullScreenOptions.cs} (54%) delete mode 100644 src/Butil/Bit.Butil/Publics/Element/FullscreenNavigationUI.cs create mode 100644 src/Butil/Bit.Butil/Publics/Events/ButilTouchPoint.cs rename src/Butil/Bit.Butil/Publics/Geolocation/{GeolocationError.cs => GeolocationErrorCode.cs} (64%) create mode 100644 src/Butil/Bit.Butil/Publics/Geolocation/GeolocationException.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDatabaseInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDurability.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexSchema.cs delete mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbKeyRecord.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbOpenInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/Locks/WebLockInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/Navigation/NavigationEventInfo.cs create mode 100644 src/Butil/Bit.Butil/Publics/Navigation/NavigationHistoryBehavior.cs create mode 100644 src/Butil/Bit.Butil/Publics/Nfc/NdefMessage.cs create mode 100644 src/Butil/Bit.Butil/Publics/StorageManager/StorageUsageDetail.cs create mode 100644 src/Butil/CLAUDE.md create mode 100644 src/Butil/tests/Bit.Butil.Tests.Manual/InteropManifest.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Manual/TypeContract.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiDetailsResult.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiInspection.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiMember.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiType.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiTypeDetails.cs rename src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/{WireContracts.cs => ButilMcp.cs} (50%) create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/DocsIndexRow.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchHit.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchResult.cs create mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ToolCallReference.cs delete mode 100644 src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireDtos.cs diff --git a/src/Butil/Bit.Butil.Build/ButilConsumerScan.cs b/src/Butil/Bit.Butil.Build/ButilConsumerScan.cs index 0f7946b1503..d64e8dff292 100644 --- a/src/Butil/Bit.Butil.Build/ButilConsumerScan.cs +++ b/src/Butil/Bit.Butil.Build/ButilConsumerScan.cs @@ -5,27 +5,6 @@ namespace Bit.Butil.Build; -/// How an untrimmed publish works out which Bit.Butil types the app it is publishing uses. -public enum ButilScanMode -{ - /// Do not scan. The default: without ILLink there is then no signal but an explicit list. - None, - - /// - /// Match Bit.Butil type names against the names in each assembly's #Strings heap. Needs no table - /// parsing at all, and over-includes whenever an app has a type of its own by the same name - which, - /// with names like Window, Console and Storage in the library, is often. - /// - TypeNames, - - /// - /// Match each assembly's TypeRef rows, which name the namespace as well, so only real references - /// to Bit.Butil types count. Costs no more at publish than and is the mode - /// to use. - /// - TypeReferences, -} - /// /// Reads a publish's own assemblies to find the Bit.Butil types the app references, and turns those into the /// set of JavaScript modules it can reach. diff --git a/src/Butil/Bit.Butil.Build/ButilScanMode.cs b/src/Butil/Bit.Butil.Build/ButilScanMode.cs new file mode 100644 index 00000000000..70ccece8522 --- /dev/null +++ b/src/Butil/Bit.Butil.Build/ButilScanMode.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Bit.Butil.Build; + +/// How an untrimmed publish works out which Bit.Butil types the app it is publishing uses. +public enum ButilScanMode +{ + /// Do not scan. The default: without ILLink there is then no signal but an explicit list. + None, + + /// + /// Match Bit.Butil type names against the names in each assembly's #Strings heap. Needs no table + /// parsing at all, and over-includes whenever an app has a type of its own by the same name - which, + /// with names like Window, Console and Storage in the library, is often. + /// + TypeNames, + + /// + /// Match each assembly's TypeRef rows, which name the namespace as well, so only real references + /// to Bit.Butil types count. Costs no more at publish than and is the mode + /// to use. + /// + TypeReferences, +} diff --git a/src/Butil/Bit.Butil.Build/ButilScriptBundler.cs b/src/Butil/Bit.Butil.Build/ButilScriptBundler.cs index fcc14a6bbab..758ae7d94e4 100644 --- a/src/Butil/Bit.Butil.Build/ButilScriptBundler.cs +++ b/src/Butil/Bit.Butil.Build/ButilScriptBundler.cs @@ -239,11 +239,3 @@ public static void WriteBundle(string chunksDirectory, IEnumerable modul } } } - -/// Module names in dependency-first order, and each module's direct dependencies. -public sealed class ButilScriptManifest(IReadOnlyList order, IReadOnlyDictionary dependencies) -{ - public IReadOnlyList Order { get; } = order; - - public IReadOnlyDictionary Dependencies { get; } = dependencies; -} diff --git a/src/Butil/Bit.Butil.Build/ButilScriptManifest.cs b/src/Butil/Bit.Butil.Build/ButilScriptManifest.cs new file mode 100644 index 00000000000..3c69600dce0 --- /dev/null +++ b/src/Butil/Bit.Butil.Build/ButilScriptManifest.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Bit.Butil.Build; + +/// Module names in dependency-first order, and each module's direct dependencies. +public sealed class ButilScriptManifest(IReadOnlyList order, IReadOnlyDictionary dependencies) +{ + public IReadOnlyList Order { get; } = order; + + public IReadOnlyDictionary Dependencies { get; } = dependencies; +} diff --git a/src/Butil/Bit.Butil.Build/MetadataHeap.cs b/src/Butil/Bit.Butil.Build/MetadataHeap.cs new file mode 100644 index 00000000000..cb217e4e3ad --- /dev/null +++ b/src/Butil/Bit.Butil.Build/MetadataHeap.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; +using System.Text; + +namespace Bit.Butil.Build; + +/// The file offset and size of one metadata stream. A stream the image does not have is empty. +public readonly struct MetadataHeap(int offset, int size) +{ + public int Offset { get; } = offset; + + public int Size { get; } = size; + + public bool IsEmpty => Size == 0; +} diff --git a/src/Butil/Bit.Butil.Build/MetadataTables.cs b/src/Butil/Bit.Butil.Build/MetadataTables.cs index f04c663a459..4be955760a2 100644 --- a/src/Butil/Bit.Butil.Build/MetadataTables.cs +++ b/src/Butil/Bit.Butil.Build/MetadataTables.cs @@ -338,49 +338,3 @@ private MetadataToken ReadCodedIndex(int position, int tagBits, params int[] tab : new MetadataToken(tables[tag], (int)(raw >> tagBits)); } } - -/// A type's namespace and name, as the metadata spells them. -public readonly struct TypeName(string @namespace, string name) : IEquatable -{ - public string Namespace { get; } = @namespace ?? string.Empty; - - public string Name { get; } = name ?? string.Empty; - - public string FullName => Namespace.Length == 0 ? Name : Namespace + "." + Name; - - public bool Equals(TypeName other) - => string.Equals(Namespace, other.Namespace, StringComparison.Ordinal) && string.Equals(Name, other.Name, StringComparison.Ordinal); - - public override bool Equals(object? obj) => obj is TypeName other && Equals(other); - - public override int GetHashCode() => (Namespace.GetHashCode() * 397) ^ Name.GetHashCode(); - - public override string ToString() => FullName; -} - -/// A metadata table and a one-based row in it. Row 0 means "nothing". -public readonly struct MetadataToken(int table, int row) -{ - public int Table { get; } = table; - - public int Row { get; } = row; - - public bool IsNil => Row == 0; -} - -/// One TypeDef row, in the terms the module map needs. -public readonly struct TypeDefinition(int row, TypeName name, MetadataToken extends, int fieldListStart, int methodListStart) -{ - public int Row { get; } = row; - - public TypeName Name { get; } = name; - - /// The base type. Nil for System.Object and for interfaces. - public MetadataToken Extends { get; } = extends; - - /// The one-based Field row this type's fields start at. - public int FieldListStart { get; } = fieldListStart; - - /// The one-based MethodDef row this type's methods start at. - public int MethodListStart { get; } = methodListStart; -} diff --git a/src/Butil/Bit.Butil.Build/MetadataToken.cs b/src/Butil/Bit.Butil.Build/MetadataToken.cs new file mode 100644 index 00000000000..aa7841a8ef1 --- /dev/null +++ b/src/Butil/Bit.Butil.Build/MetadataToken.cs @@ -0,0 +1,13 @@ +using System; + +namespace Bit.Butil.Build; + +/// A metadata table and a one-based row in it. Row 0 means "nothing". +public readonly struct MetadataToken(int table, int row) +{ + public int Table { get; } = table; + + public int Row { get; } = row; + + public bool IsNil => Row == 0; +} diff --git a/src/Butil/Bit.Butil.Build/PeImage.cs b/src/Butil/Bit.Butil.Build/PeImage.cs index c78d2f636dc..38630ffb3ed 100644 --- a/src/Butil/Bit.Butil.Build/PeImage.cs +++ b/src/Butil/Bit.Butil.Build/PeImage.cs @@ -217,13 +217,3 @@ public int ReadInt32(int position) public BadImageFormatException Invalid(string reason) => new($"'{Path}' could not be read as a managed assembly: {reason}."); } - -/// The file offset and size of one metadata stream. A stream the image does not have is empty. -public readonly struct MetadataHeap(int offset, int size) -{ - public int Offset { get; } = offset; - - public int Size { get; } = size; - - public bool IsEmpty => Size == 0; -} diff --git a/src/Butil/Bit.Butil.Build/TypeDefinition.cs b/src/Butil/Bit.Butil.Build/TypeDefinition.cs new file mode 100644 index 00000000000..d6601d2236e --- /dev/null +++ b/src/Butil/Bit.Butil.Build/TypeDefinition.cs @@ -0,0 +1,20 @@ +using System; + +namespace Bit.Butil.Build; + +/// One TypeDef row, in the terms the module map needs. +public readonly struct TypeDefinition(int row, TypeName name, MetadataToken extends, int fieldListStart, int methodListStart) +{ + public int Row { get; } = row; + + public TypeName Name { get; } = name; + + /// The base type. Nil for System.Object and for interfaces. + public MetadataToken Extends { get; } = extends; + + /// The one-based Field row this type's fields start at. + public int FieldListStart { get; } = fieldListStart; + + /// The one-based MethodDef row this type's methods start at. + public int MethodListStart { get; } = methodListStart; +} diff --git a/src/Butil/Bit.Butil.Build/TypeName.cs b/src/Butil/Bit.Butil.Build/TypeName.cs new file mode 100644 index 00000000000..d8bdfbf0964 --- /dev/null +++ b/src/Butil/Bit.Butil.Build/TypeName.cs @@ -0,0 +1,22 @@ +using System; + +namespace Bit.Butil.Build; + +/// A type's namespace and name, as the metadata spells them. +public readonly struct TypeName(string @namespace, string name) : IEquatable +{ + public string Namespace { get; } = @namespace ?? string.Empty; + + public string Name { get; } = name ?? string.Empty; + + public string FullName => Namespace.Length == 0 ? Name : Namespace + "." + Name; + + public bool Equals(TypeName other) + => string.Equals(Namespace, other.Namespace, StringComparison.Ordinal) && string.Equals(Name, other.Name, StringComparison.Ordinal); + + public override bool Equals(object? obj) => obj is TypeName other && Equals(other); + + public override int GetHashCode() => (Namespace.GetHashCode() * 397) ^ Name.GetHashCode(); + + public override string ToString() => FullName; +} diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/ApiNeeds.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/ApiNeeds.cs new file mode 100644 index 00000000000..1de7af4337e --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/ApiNeeds.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; +using Bit.Butil.Demo.Client.Pages; + +namespace Bit.Butil.Demo.Client.Docs; + +/// +/// The preconditions an API imposes on the calling page, beyond simply being implemented. +/// +[Flags] +public enum ApiNeeds +{ + None = 0, + + /// Only available over HTTPS or on localhost. + SecureContext = 1, + + /// The browser prompts the user, and the call fails if permission is denied. + Permission = 2, + + /// Must be called from a user-gesture handler such as a click. + UserGesture = 4, + + /// Behind an experimental or origin-trial flag in at least one shipping engine. + Experimental = 8, +} diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/ApiSupport.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/ApiSupport.cs new file mode 100644 index 00000000000..aef6aeaa8ef --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/ApiSupport.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components; +using Bit.Butil.Demo.Client.Pages; + +namespace Bit.Butil.Demo.Client.Docs; + +/// +/// How widely the underlying browser API is implemented. This is about the web platform, not about +/// Butil: every wrapper on this site works everywhere Blazor does, but it can only expose what the +/// browser underneath it implements. +/// +public enum ApiSupport +{ + /// Not a browser API at all - a guide page. + Guide, + + /// Implemented by every current engine. + Broad, + + /// Implemented everywhere, but with members or behaviour that differ between engines. + Partial, + + /// Chromium only (Chrome, Edge, Opera and friends). + Chromium, + + /// Chromium on desktop only. + ChromiumDesktop, + + /// Chromium on Android only. + ChromiumMobile, +} diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocGroup.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocGroup.cs new file mode 100644 index 00000000000..e09634fbbfa --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocGroup.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components; +using Bit.Butil.Demo.Client.Pages; + +namespace Bit.Butil.Demo.Client.Docs; + +/// +/// The key of the mark that stands for this area of the platform, resolved by Shared/Icon.razor. +/// It is declared per group rather than per page on purpose: sixty-six glyphs in one list is +/// decoration a reader has to look past to find a name, while ten of them are landmarks that say +/// which part of the browser they are now in. +/// +public record DocGroup(string Title, string Icon, DocLink[] Links); diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocLink.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocLink.cs new file mode 100644 index 00000000000..37f090a121d --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocLink.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Components; +using Bit.Butil.Demo.Client.Pages; + +namespace Bit.Butil.Demo.Client.Docs; + +/// +/// The component routed at . Naming it here is what lets the MCP server +/// (Server/Controllers/McpController.cs) render a page's documentation on demand, so an agent reads +/// the same text a human does instead of a second copy that could go stale. +/// +/// +/// The Bit.Butil public types the page documents, when they are not simply the title without its +/// spaces. Only the pages whose title is not a type name ("Local & Session Storage") or whose +/// API is a set of extension methods ("Element", "Animation") need to state them. +/// +public record DocLink( + string Title, + string Url, + string Summary, + Type PageType, + ApiSupport Support = ApiSupport.Broad, + ApiNeeds Needs = ApiNeeds.None, + string[]? Services = null); diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs index 54633470222..b19d10a5723 100644 --- a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsNav.cs @@ -3,80 +3,6 @@ namespace Bit.Butil.Demo.Client.Docs; -/// -/// How widely the underlying browser API is implemented. This is about the web platform, not about -/// Butil: every wrapper on this site works everywhere Blazor does, but it can only expose what the -/// browser underneath it implements. -/// -public enum ApiSupport -{ - /// Not a browser API at all - a guide page. - Guide, - - /// Implemented by every current engine. - Broad, - - /// Implemented everywhere, but with members or behaviour that differ between engines. - Partial, - - /// Chromium only (Chrome, Edge, Opera and friends). - Chromium, - - /// Chromium on desktop only. - ChromiumDesktop, - - /// Chromium on Android only. - ChromiumMobile, -} - -/// -/// The preconditions an API imposes on the calling page, beyond simply being implemented. -/// -[Flags] -public enum ApiNeeds -{ - None = 0, - - /// Only available over HTTPS or on localhost. - SecureContext = 1, - - /// The browser prompts the user, and the call fails if permission is denied. - Permission = 2, - - /// Must be called from a user-gesture handler such as a click. - UserGesture = 4, - - /// Behind an experimental or origin-trial flag in at least one shipping engine. - Experimental = 8, -} - -/// -/// The component routed at . Naming it here is what lets the MCP server -/// (Server/Controllers/McpController.cs) render a page's documentation on demand, so an agent reads -/// the same text a human does instead of a second copy that could go stale. -/// -/// -/// The Bit.Butil public types the page documents, when they are not simply the title without its -/// spaces. Only the pages whose title is not a type name ("Local & Session Storage") or whose -/// API is a set of extension methods ("Element", "Animation") need to state them. -/// -public record DocLink( - string Title, - string Url, - string Summary, - Type PageType, - ApiSupport Support = ApiSupport.Broad, - ApiNeeds Needs = ApiNeeds.None, - string[]? Services = null); - -/// -/// The key of the mark that stands for this area of the platform, resolved by Shared/Icon.razor. -/// It is declared per group rather than per page on purpose: sixty-six glyphs in one list is -/// decoration a reader has to look past to find a name, while ten of them are landmarks that say -/// which part of the browser they are now in. -/// -public record DocGroup(string Title, string Icon, DocLink[] Links); - /// /// The single source of truth for the site taxonomy: the sidebar, the home page feature grid, /// the browser-support matrix and the prev/next pager are all rendered from this list. diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchEntry.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchEntry.cs new file mode 100644 index 00000000000..3a09178e17d --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchEntry.cs @@ -0,0 +1,34 @@ +namespace Bit.Butil.Demo.Client.Docs; + +/// +/// One searchable piece of the site: a whole page, one of its sections, or one row of its API +/// reference table. +/// +/// The fields are held apart rather than concatenated into a single haystack so a hit on a name can +/// outrank a hit buried in prose - see Shared/SearchBox.razor, which scores them. +/// +/// +/// What the hit is called: the page's title, the section's heading, the member's name. +/// +/// The title of the page this entry lives on, or null when the entry IS the page. It is what the +/// result list shows beside a section so the reader knows where it would take them. +/// +/// The href, including the fragment that scrolls to the section. +/// The nav group the page belongs to. +/// +/// Names that are not in the title but that someone would search by: the type the page documents, +/// the member badge on a section, the signature of an API row, the words of the slug. +/// +/// The one sentence shown under the title in the result list. +/// +/// The rest of the entry's text - code samples, the labels and prose of the live demo, the callouts. +/// It is matched but never shown in full: a hit in it is quoted as a window around the match. +/// +public record DocsSearchEntry( + string Title, + string? Page, + string Url, + string Group, + string Keywords, + string Summary, + string Body); diff --git a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchIndex.cs b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchIndex.cs index f9f1f529f6b..eb21ccb2d7b 100644 --- a/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchIndex.cs +++ b/src/Butil/Bit.Butil.Demo/Client/Docs/DocsSearchIndex.cs @@ -1,38 +1,5 @@ namespace Bit.Butil.Demo.Client.Docs; -/// -/// One searchable piece of the site: a whole page, one of its sections, or one row of its API -/// reference table. -/// -/// The fields are held apart rather than concatenated into a single haystack so a hit on a name can -/// outrank a hit buried in prose - see Shared/SearchBox.razor, which scores them. -/// -/// -/// What the hit is called: the page's title, the section's heading, the member's name. -/// -/// The title of the page this entry lives on, or null when the entry IS the page. It is what the -/// result list shows beside a section so the reader knows where it would take them. -/// -/// The href, including the fragment that scrolls to the section. -/// The nav group the page belongs to. -/// -/// Names that are not in the title but that someone would search by: the type the page documents, -/// the member badge on a section, the signature of an API row, the words of the slug. -/// -/// The one sentence shown under the title in the result list. -/// -/// The rest of the entry's text - code samples, the labels and prose of the live demo, the callouts. -/// It is matched but never shown in full: a hit in it is quoted as a window around the match. -/// -public record DocsSearchEntry( - string Title, - string? Page, - string Url, - string Group, - string Keywords, - string Summary, - string Body); - /// /// The corpus behind the site's search box, served by the host at /api/docs/search-index and /// fetched once by the browser. diff --git a/src/Butil/Bit.Butil.Demo/Client/Services/PageOutline.cs b/src/Butil/Bit.Butil.Demo/Client/Services/PageOutline.cs index 2290a78b3b7..1f604d9ef4a 100644 --- a/src/Butil/Bit.Butil.Demo/Client/Services/PageOutline.cs +++ b/src/Butil/Bit.Butil.Demo/Client/Services/PageOutline.cs @@ -1,8 +1,5 @@ namespace Bit.Butil.Demo.Client.Services; -/// An entry in the "On this page" rail: one . -public record PageOutlineItem(string Title, string Slug); - /// /// Backs the side rail. Sections are the pages' own content, so they cannot be enumerated up /// front: every registers itself here as it initializes, and diff --git a/src/Butil/Bit.Butil.Demo/Client/Services/PageOutlineItem.cs b/src/Butil/Bit.Butil.Demo/Client/Services/PageOutlineItem.cs new file mode 100644 index 00000000000..8aebc6f539e --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Client/Services/PageOutlineItem.cs @@ -0,0 +1,4 @@ +namespace Bit.Butil.Demo.Client.Services; + +/// An entry in the "On this page" rail: one . +public record PageOutlineItem(string Title, string Slug); diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiDetailsResultDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiDetailsResultDto.cs new file mode 100644 index 00000000000..5a6d969cf34 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiDetailsResultDto.cs @@ -0,0 +1,18 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// What GetButilApiDetails answers: the type's reference, the list to pick one from, or why there is none. +public record ButilApiDetailsResultDto +{ + /// The full reference of the type, when a public type goes by the requested name. + public ButilApiTypeDetailsDto? Details { get; init; } + + /// + /// Every public type, set instead of Details when the call named none. A listing does not earn + /// a tool of its own - it is what the retrieval tool answers when asked for nothing in + /// particular, which is the one moment a caller wants it. + /// + public ButilApiTypeDto[]? Types { get; init; } + + /// Set instead of Details when nothing matched - it names the closest candidates. + public string? Message { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiInspectionDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiInspectionDto.cs new file mode 100644 index 00000000000..32bec3f4f87 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiInspectionDto.cs @@ -0,0 +1,44 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// +/// What using one Butil API entails, beyond the signatures of its members. +/// +/// Data about the API, and nothing that reads as advice: the prose telling a caller what to do +/// about any of it is the plan's , written once for the +/// whole set and naming the APIs each item applies to. This carried its own copy of that advice +/// once - the same paragraph about prerendering, permissions and disposal repeated per API, in a +/// plan that then said all of it again in the checklist, about a client that had already read the +/// same rules in the server's instructions. Four copies of a paragraph is four times the tokens and +/// no more likely to be followed. +/// +/// +public record ButilApiInspectionDto +{ + /// The name that was looked up. + public required string Query { get; init; } + + /// False when nothing in Bit.Butil goes by that name - Message then says what to try. + public required bool IsKnown { get; init; } + + public string? Message { get; init; } + + /// The documented API this resolved to, e.g. "Clipboard". + public string? Api { get; init; } + + /// The Bit.Butil types behind it. + public string[]? Services { get; init; } + + /// The injection lines to put in a component, one per injectable service behind the API. + public string[]? Inject { get; init; } + + public string? BrowserSupport { get; init; } + + /// The preconditions the calling page has to satisfy before the call can succeed. + public string[]? Requires { get; init; } + + /// Members whose result has to be disposed - a subscription or a handle on real hardware. + public string[]? Disposables { get; init; } + + /// The follow-up calls that return the full text: the API reference and the docs page. + public string[]? NextCalls { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiMemberDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiMemberDto.cs new file mode 100644 index 00000000000..a80446a7cbb --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiMemberDto.cs @@ -0,0 +1,24 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// A member (property, method, field or enum value) of a public Bit.Butil type. +public record ButilApiMemberDto +{ + public required string Name { get; init; } + + /// Property, Method, Field, Event or EnumValue. + public required string Kind { get; init; } + + /// The C# type of the member, or the return type for a method. + public string? Type { get; init; } + + /// The method's parameter list, e.g. "(string key, string value)". + public string? Signature { get; init; } + + /// The constant value of a const field, or the value a property has on a fresh instance. + public string? Default { get; init; } + + public string? Summary { get; init; } + + /// The XML remarks, when the member has any - they carry the caveats. + public string? Remarks { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDetailsDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDetailsDto.cs new file mode 100644 index 00000000000..a997339e668 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDetailsDto.cs @@ -0,0 +1,25 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// The full reference of one public Bit.Butil type. +public record ButilApiTypeDetailsDto +{ + public required string Name { get; init; } + + public required string FullName { get; init; } + + public required string Kind { get; init; } + + /// How to obtain one, e.g. "@inject Bit.Butil.Clipboard clipboard" - null when it is not a service. + public string? Inject { get; init; } + + public string[]? Implements { get; init; } + + public string? Summary { get; init; } + + public string? Remarks { get; init; } + + /// The documentation page covering this type, when the site has one. + public string? DocsUrl { get; init; } + + public required ButilApiMemberDto[] Members { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDto.cs new file mode 100644 index 00000000000..7be8f3799c1 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilApiTypeDto.cs @@ -0,0 +1,15 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// A public type of the Bit.Butil assembly. +public record ButilApiTypeDto +{ + public required string Name { get; init; } + + /// Service, Static class, Interface, Enum, Attribute, Delegate, Class, Struct or Record. + public required string Kind { get; init; } + + /// True for a class marked [ButilService] - the ones you inject by their own name. + public required bool IsInjectable { get; init; } + + public string? Summary { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilCapabilityDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilCapabilityDto.cs new file mode 100644 index 00000000000..1ff7d973513 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilCapabilityDto.cs @@ -0,0 +1,22 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// One row of the browser-support matrix: an API, and what it needs from the page. +public record ButilCapabilityDto +{ + /// The documented API, e.g. "Clipboard" or "Local & Session Storage". + public required string Api { get; init; } + + /// The Bit.Butil types behind it - what you actually inject or call. + public required string[] Services { get; init; } + + /// Which engines implement it: "All engines", "Varies by engine", "Chromium only", ... + public required string BrowserSupport { get; init; } + + /// The preconditions the calling page has to satisfy, one sentence each. + public required string[] Requires { get; init; } + + public required string Summary { get; init; } + + /// The documentation page for the API, on the live site. + public required string DocsUrl { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilFeaturePlanDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilFeaturePlanDto.cs new file mode 100644 index 00000000000..39a2f9f2b23 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilFeaturePlanDto.cs @@ -0,0 +1,29 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// The combined consequences of building a feature on a set of Butil APIs. +public record ButilFeaturePlanDto +{ + /// One entry per requested API, in the order they were passed. + public required ButilApiInspectionDto[] Apis { get; init; } + + /// Names that matched nothing - a typo, or an API this library does not wrap. + public required string[] Unknown { get; init; } + + /// True when at least one API only works over HTTPS or on localhost. + public required bool RequiresSecureContext { get; init; } + + /// True when at least one API prompts the user, so a denial has to be a supported outcome. + public required bool RequiresPermission { get; init; } + + /// True when at least one API only works from inside a click handler. + public required bool RequiresUserGesture { get; init; } + + /// The APIs that do not work in every engine, each with the engines that do implement it. + public required string[] EngineLimited { get; init; } + + /// The ordered checklist for shipping this feature. + public required string[] Checklist { get; init; } + + /// Names past the per-plan cap, which were not inspected. Pass them in a second call. + public string[]? Ignored { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilGuideSectionDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilGuideSectionDto.cs new file mode 100644 index 00000000000..52ab023d956 --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilGuideSectionDto.cs @@ -0,0 +1,17 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// One heading of the library's README, which doubles as its reference guide. +public record ButilGuideSectionDto +{ + /// The heading text, e.g. "Subscriptions are disposable". Pass it to GetButilGuideSection. + public required string Heading { get; init; } + + /// Markdown heading level: 2 for a top-level section, 3 for a sub-section. + public required int Level { get; init; } + + /// The owning level-2 section, or null when this entry is itself level 2. + public string? Parent { get; init; } + + /// Number of markdown lines in the section (including its sub-sections). + public required int Lines { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilMcpDtos.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilMcpDtos.cs deleted file mode 100644 index 0d7db350323..00000000000 --- a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilMcpDtos.cs +++ /dev/null @@ -1,234 +0,0 @@ -namespace Bit.Butil.Demo.Server.Dtos; - -/// One heading of the library's README, which doubles as its reference guide. -public record ButilGuideSectionDto -{ - /// The heading text, e.g. "Subscriptions are disposable". Pass it to GetButilGuideSection. - public required string Heading { get; init; } - - /// Markdown heading level: 2 for a top-level section, 3 for a sub-section. - public required int Level { get; init; } - - /// The owning level-2 section, or null when this entry is itself level 2. - public string? Parent { get; init; } - - /// Number of markdown lines in the section (including its sub-sections). - public required int Lines { get; init; } -} - -/// A public type of the Bit.Butil assembly. -public record ButilApiTypeDto -{ - public required string Name { get; init; } - - /// Service, Static class, Interface, Enum, Attribute, Delegate, Class, Struct or Record. - public required string Kind { get; init; } - - /// True for a class marked [ButilService] - the ones you inject by their own name. - public required bool IsInjectable { get; init; } - - public string? Summary { get; init; } -} - -/// A member (property, method, field or enum value) of a public Bit.Butil type. -public record ButilApiMemberDto -{ - public required string Name { get; init; } - - /// Property, Method, Field, Event or EnumValue. - public required string Kind { get; init; } - - /// The C# type of the member, or the return type for a method. - public string? Type { get; init; } - - /// The method's parameter list, e.g. "(string key, string value)". - public string? Signature { get; init; } - - /// The constant value of a const field, or the value a property has on a fresh instance. - public string? Default { get; init; } - - public string? Summary { get; init; } - - /// The XML remarks, when the member has any - they carry the caveats. - public string? Remarks { get; init; } -} - -/// The full reference of one public Bit.Butil type. -public record ButilApiTypeDetailsDto -{ - public required string Name { get; init; } - - public required string FullName { get; init; } - - public required string Kind { get; init; } - - /// How to obtain one, e.g. "@inject Bit.Butil.Clipboard clipboard" - null when it is not a service. - public string? Inject { get; init; } - - public string[]? Implements { get; init; } - - public string? Summary { get; init; } - - public string? Remarks { get; init; } - - /// The documentation page covering this type, when the site has one. - public string? DocsUrl { get; init; } - - public required ButilApiMemberDto[] Members { get; init; } -} - -/// What GetButilApiDetails answers: the type's reference, the list to pick one from, or why there is none. -public record ButilApiDetailsResultDto -{ - /// The full reference of the type, when a public type goes by the requested name. - public ButilApiTypeDetailsDto? Details { get; init; } - - /// - /// Every public type, set instead of Details when the call named none. A listing does not earn - /// a tool of its own - it is what the retrieval tool answers when asked for nothing in - /// particular, which is the one moment a caller wants it. - /// - public ButilApiTypeDto[]? Types { get; init; } - - /// Set instead of Details when nothing matched - it names the closest candidates. - public string? Message { get; init; } -} - -/// One row of the browser-support matrix: an API, and what it needs from the page. -public record ButilCapabilityDto -{ - /// The documented API, e.g. "Clipboard" or "Local & Session Storage". - public required string Api { get; init; } - - /// The Bit.Butil types behind it - what you actually inject or call. - public required string[] Services { get; init; } - - /// Which engines implement it: "All engines", "Varies by engine", "Chromium only", ... - public required string BrowserSupport { get; init; } - - /// The preconditions the calling page has to satisfy, one sentence each. - public required string[] Requires { get; init; } - - public required string Summary { get; init; } - - /// The documentation page for the API, on the live site. - public required string DocsUrl { get; init; } -} - -/// -/// What using one Butil API entails, beyond the signatures of its members. -/// -/// Data about the API, and nothing that reads as advice: the prose telling a caller what to do -/// about any of it is the plan's , written once for the -/// whole set and naming the APIs each item applies to. This carried its own copy of that advice -/// once - the same paragraph about prerendering, permissions and disposal repeated per API, in a -/// plan that then said all of it again in the checklist, about a client that had already read the -/// same rules in the server's instructions. Four copies of a paragraph is four times the tokens and -/// no more likely to be followed. -/// -/// -public record ButilApiInspectionDto -{ - /// The name that was looked up. - public required string Query { get; init; } - - /// False when nothing in Bit.Butil goes by that name - Message then says what to try. - public required bool IsKnown { get; init; } - - public string? Message { get; init; } - - /// The documented API this resolved to, e.g. "Clipboard". - public string? Api { get; init; } - - /// The Bit.Butil types behind it. - public string[]? Services { get; init; } - - /// The injection lines to put in a component, one per injectable service behind the API. - public string[]? Inject { get; init; } - - public string? BrowserSupport { get; init; } - - /// The preconditions the calling page has to satisfy before the call can succeed. - public string[]? Requires { get; init; } - - /// Members whose result has to be disposed - a subscription or a handle on real hardware. - public string[]? Disposables { get; init; } - - /// The follow-up calls that return the full text: the API reference and the docs page. - public string[]? NextCalls { get; init; } -} - -/// The combined consequences of building a feature on a set of Butil APIs. -public record ButilFeaturePlanDto -{ - /// One entry per requested API, in the order they were passed. - public required ButilApiInspectionDto[] Apis { get; init; } - - /// Names that matched nothing - a typo, or an API this library does not wrap. - public required string[] Unknown { get; init; } - - /// True when at least one API only works over HTTPS or on localhost. - public required bool RequiresSecureContext { get; init; } - - /// True when at least one API prompts the user, so a denial has to be a supported outcome. - public required bool RequiresPermission { get; init; } - - /// True when at least one API only works from inside a click handler. - public required bool RequiresUserGesture { get; init; } - - /// The APIs that do not work in every engine, each with the engines that do implement it. - public required string[] EngineLimited { get; init; } - - /// The ordered checklist for shipping this feature. - public required string[] Checklist { get; init; } - - /// Names past the per-plan cap, which were not inspected. Pass them in a second call. - public string[]? Ignored { get; init; } -} - -/// A source file of the demo/samples, retrievable through GetButilSourceFile. -public record ButilSourceFileDto -{ - /// The path to pass to GetButilSourceFile, e.g. "Demo/Client/Pages/ClipboardPage.razor". - public required string Path { get; init; } - - /// Demo (this documentation site) or Sample (the minimal hosting-model samples). - public required string Kind { get; init; } - - /// The file's own header comment or page title, when it has one. - public string? Description { get; init; } - - public required int Lines { get; init; } -} - -/// One result of a search across everything this MCP server knows about Bit.Butil. -public record ButilSearchHitDto -{ - /// What was found: "Guide section", "Docs page", "API service", "API method", "Source file", ... - public required string Kind { get; init; } - - public required string Title { get; init; } - - /// Where the hit sits: the owning section, type or group. - public string? Context { get; init; } - - /// The tool call that returns the full text of this hit - call it verbatim. - public required string Tool { get; init; } - - /// The matching text, with a little of what surrounds it. - public required string Snippet { get; init; } -} - -/// What a search answered with: its hits, or the reason there are none. -public record ButilSearchResultDto -{ - /// The best matches, most relevant first. - public required ButilSearchHitDto[] Hits { get; init; } - - /// - /// Set only when Hits is empty. A query can come back empty two different ways - nothing - /// matched, or the query was phrased entirely in words too common to search on - and an agent - /// cannot tell them apart from an empty list. - /// - public string? Message { get; init; } -} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchHitDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchHitDto.cs new file mode 100644 index 00000000000..6bfa5469abb --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchHitDto.cs @@ -0,0 +1,19 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// One result of a search across everything this MCP server knows about Bit.Butil. +public record ButilSearchHitDto +{ + /// What was found: "Guide section", "Docs page", "API service", "API method", "Source file", ... + public required string Kind { get; init; } + + public required string Title { get; init; } + + /// Where the hit sits: the owning section, type or group. + public string? Context { get; init; } + + /// The tool call that returns the full text of this hit - call it verbatim. + public required string Tool { get; init; } + + /// The matching text, with a little of what surrounds it. + public required string Snippet { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchResultDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchResultDto.cs new file mode 100644 index 00000000000..292a653d0fb --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSearchResultDto.cs @@ -0,0 +1,15 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// What a search answered with: its hits, or the reason there are none. +public record ButilSearchResultDto +{ + /// The best matches, most relevant first. + public required ButilSearchHitDto[] Hits { get; init; } + + /// + /// Set only when Hits is empty. A query can come back empty two different ways - nothing + /// matched, or the query was phrased entirely in words too common to search on - and an agent + /// cannot tell them apart from an empty list. + /// + public string? Message { get; init; } +} diff --git a/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSourceFileDto.cs b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSourceFileDto.cs new file mode 100644 index 00000000000..8cb1ef2c12c --- /dev/null +++ b/src/Butil/Bit.Butil.Demo/Server/Dtos/ButilSourceFileDto.cs @@ -0,0 +1,16 @@ +namespace Bit.Butil.Demo.Server.Dtos; + +/// A source file of the demo/samples, retrievable through GetButilSourceFile. +public record ButilSourceFileDto +{ + /// The path to pass to GetButilSourceFile, e.g. "Demo/Client/Pages/ClipboardPage.razor". + public required string Path { get; init; } + + /// Demo (this documentation site) or Sample (the minimal hosting-model samples). + public required string Kind { get; init; } + + /// The file's own header comment or page title, when it has one. + public string? Description { get; init; } + + public required int Lines { get; init; } +} diff --git a/src/Butil/Bit.Butil/Bit.Butil.csproj b/src/Butil/Bit.Butil/Bit.Butil.csproj index 2672cf50b68..423789bc293 100644 --- a/src/Butil/Bit.Butil/Bit.Butil.csproj +++ b/src/Butil/Bit.Butil/Bit.Butil.csproj @@ -7,6 +7,11 @@ true $(NoWarn);NU5100 + + $(NoWarn.Replace('CS1591', '')) diff --git a/src/Butil/Bit.Butil/BitButil.cs b/src/Butil/Bit.Butil/BitButil.cs index 887de1424dd..4b7cce764a5 100644 --- a/src/Butil/Bit.Butil/BitButil.cs +++ b/src/Butil/Bit.Butil/BitButil.cs @@ -6,6 +6,10 @@ namespace Bit.Butil; +/// +/// The entry point a consuming app touches directly: service registration, and the two global +/// switches (script loading mode and fast invoke) that apply to every Butil service at once. +/// public static class BitButil { /// diff --git a/src/Butil/Bit.Butil/Extensions/JSRuntimeExtensions.cs b/src/Butil/Bit.Butil/Extensions/JSRuntimeExtensions.cs index 62491124e49..b00ca5c70e2 100644 --- a/src/Butil/Bit.Butil/Extensions/JSRuntimeExtensions.cs +++ b/src/Butil/Bit.Butil/Extensions/JSRuntimeExtensions.cs @@ -8,6 +8,11 @@ namespace Bit.Butil; +/// +/// The synchronous fast path over , used when +/// is on. It only applies under Blazor WebAssembly, where the runtime is in-process; everywhere else +/// these fall through to the ordinary async calls. +/// public static class JSRuntimeExtensions { /// diff --git a/src/Butil/Bit.Butil/Extensions/LinkerFlags.cs b/src/Butil/Bit.Butil/Extensions/LinkerFlags.cs index df68ac1221d..0586252cf0e 100644 --- a/src/Butil/Bit.Butil/Extensions/LinkerFlags.cs +++ b/src/Butil/Bit.Butil/Extensions/LinkerFlags.cs @@ -2,6 +2,10 @@ namespace Bit.Butil; +/// +/// The combinations this library annotates with, named +/// once so a call site says what it is preserving rather than spelling out the flags again. +/// public static class LinkerFlags { /// diff --git a/src/Butil/Bit.Butil/Publics/Animation/AnimationHandle.cs b/src/Butil/Bit.Butil/Publics/Animation/AnimationHandle.cs index 9b5f09242de..7a4e528d834 100644 --- a/src/Butil/Bit.Butil/Publics/Animation/AnimationHandle.cs +++ b/src/Butil/Bit.Butil/Publics/Animation/AnimationHandle.cs @@ -42,6 +42,7 @@ internal AnimationHandle(IJSRuntime js, Guid id) /// Sets the playback rate (1 = normal speed; -1 = reverse at normal speed). public ValueTask SetPlaybackRate(double rate) => _js.InvokeVoid("BitButil.animation.setPlaybackRate", _id, rate); + /// Cancels the animation and releases the browser-side handle. Calling it again does nothing. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Butil/Bit.Butil/Publics/BarcodeDetector/DetectedBarcode.cs b/src/Butil/Bit.Butil/Publics/BarcodeDetector/DetectedBarcode.cs index 3ca684518c7..e2e98ebcdbe 100644 --- a/src/Butil/Bit.Butil/Publics/BarcodeDetector/DetectedBarcode.cs +++ b/src/Butil/Bit.Butil/Publics/BarcodeDetector/DetectedBarcode.cs @@ -25,7 +25,9 @@ public class DetectedBarcode /// The bounding box's top edge, in the source element's pixel coordinates. public double Y { get; set; } + /// The bounding box's width, in the source element's pixel coordinates. public double Width { get; set; } + /// The bounding box's height, in the source element's pixel coordinates. public double Height { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/BroadcastChannel.cs b/src/Butil/Bit.Butil/Publics/BroadcastChannel.cs index c60d23f6dbd..4e11e2fc3a1 100644 --- a/src/Butil/Bit.Butil/Publics/BroadcastChannel.cs +++ b/src/Butil/Bit.Butil/Publics/BroadcastChannel.cs @@ -97,6 +97,7 @@ public async Task Subscribe(string channelName, }); } + /// Closes every channel opened through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/CacheStorage.cs b/src/Butil/Bit.Butil/Publics/CacheStorage.cs index 9b3c4e5b7e3..ce488596176 100644 --- a/src/Butil/Bit.Butil/Publics/CacheStorage.cs +++ b/src/Butil/Bit.Butil/Publics/CacheStorage.cs @@ -85,6 +85,7 @@ public ValueTask PutText(string cacheName, string url, string text, public ValueTask MatchAny(string url) => js.Invoke("BitButil.cacheStorage.matchAny", url); + /// Looks one URL up in a single named cache. Faster than , which has to search them all. [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CachedResponse))] public ValueTask Match(string cacheName, string url) => js.Invoke("BitButil.cacheStorage.match", cacheName, url); diff --git a/src/Butil/Bit.Butil/Publics/CacheStorage/CachedResponse.cs b/src/Butil/Bit.Butil/Publics/CacheStorage/CachedResponse.cs index 077feea58bf..bcead2d0405 100644 --- a/src/Butil/Bit.Butil/Publics/CacheStorage/CachedResponse.cs +++ b/src/Butil/Bit.Butil/Publics/CacheStorage/CachedResponse.cs @@ -10,9 +10,16 @@ public class CachedResponse /// True when a response was found. public bool Found { get; set; } + /// The HTTP status the response was cached with. 0 when nothing was found. public int Status { get; set; } + + /// The status text that went with . public string StatusText { get; set; } = string.Empty; + + /// The URL the response was cached against. public string Url { get; set; } = string.Empty; + + /// The response headers. Empty for an opaque (cross-origin, no-cors) response, which hides them. public Dictionary Headers { get; set; } = new(); /// Body bytes. Empty for 204/304 or when the cache stored an opaque response. diff --git a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs index 8b881706bb8..322cbe8e4fa 100644 --- a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs +++ b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardFormats.cs @@ -1,6 +1,18 @@ namespace Bit.Butil; +/// +/// The options bag passed to a clipboard read: the MIME types whose payload should be handed over +/// exactly as it was written, instead of being sanitized by the browser first. +///
+/// Clipboard.read() +///
public class ClipboardFormats { + /// + /// MIME types to read unsanitized - in practice only "text/html", the one format the + /// browser otherwise rewrites. Empty means every item is sanitized. + ///
+ /// Clipboard.read() unsanitized + ///
public string[] Unsanitized { get; set; } = []; } diff --git a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs index 3e1a29b109a..d14d5030c95 100644 --- a/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs +++ b/src/Butil/Bit.Butil/Publics/Clipboard/ClipboardItem.cs @@ -1,8 +1,27 @@ namespace Bit.Butil; +/// +/// One item on the system clipboard: a single MIME type and the bytes recorded under it. The +/// clipboard holds a list of these, so a copy can offer the same content as plain text, as HTML +/// and as an image at once. +///
+/// ClipboardItem +///
public class ClipboardItem { + /// + /// The MIME type this item's is in - "text/plain", "text/html" + /// or "image/png". Browsers refuse to write types outside a short allow-list. + ///
+ /// ClipboardItem.types + ///
public string MimeType { get; set; } = default!; + /// + /// The item's payload. Text formats are the UTF-8 bytes of the string; image formats are the + /// encoded file. + ///
+ /// ClipboardItem.getType() + ///
public byte[] Data { get; set; } = default!; } diff --git a/src/Butil/Bit.Butil/Publics/Contacts/ContactInfo.cs b/src/Butil/Bit.Butil/Publics/Contacts/ContactInfo.cs index 646a0d88173..9d277f65a86 100644 --- a/src/Butil/Bit.Butil/Publics/Contacts/ContactInfo.cs +++ b/src/Butil/Bit.Butil/Publics/Contacts/ContactInfo.cs @@ -6,8 +6,13 @@ namespace Bit.Butil; ///
public class ContactInfo { + /// The contact's names. Empty unless "name" was among the requested properties. public string[] Name { get; set; } = []; + + /// The contact's email addresses. Empty unless "email" was among the requested properties. public string[] Email { get; set; } = []; + + /// The contact's phone numbers. Empty unless "tel" was among the requested properties. public string[] Tel { get; set; } = []; /// Postal addresses serialized as plain strings. diff --git a/src/Butil/Bit.Butil/Publics/Cookie/ButilCookie.cs b/src/Butil/Bit.Butil/Publics/Cookie/ButilCookie.cs index fcb84c49958..1895cd15f91 100644 --- a/src/Butil/Bit.Butil/Publics/Cookie/ButilCookie.cs +++ b/src/Butil/Bit.Butil/Publics/Cookie/ButilCookie.cs @@ -4,18 +4,72 @@ namespace Bit.Butil; +/// +/// One document.cookie entry, and the attributes it is written with. +/// +/// Only and ever come back from a read: document.cookie +/// hands out nothing but name/value pairs, so a cookie produced by has every +/// other member at its default. The rest exist for the write side, where +/// renders them into a Set-Cookie-shaped string. Read +/// instead when the attributes matter. +/// +///
+/// Document.cookie +///
public class ButilCookie { + /// The cookie's name. Percent-encoded on write, decoded on . public string? Name { get; set; } + + /// The cookie's value. Percent-encoded on write, decoded on . public string? Value { get; set; } + + /// + /// The domain the cookie is sent to, subdomains included. Omitting it - the default - scopes + /// the cookie to the exact host that set it, which is the narrower choice. + /// public string? Domain { get; set; } + + /// + /// When the cookie expires, written as an RFC 1123 date. null makes it a session cookie. + /// A time in the past is how a cookie is deleted. + /// public DateTimeOffset? Expires { get; set; } + + /// + /// Lifetime in seconds from now. Takes precedence over wherever both are + /// set, and unlike it, does not depend on the client's clock being right. + /// public long? MaxAge { get; set; } + + /// + /// Store the cookie under a partition key of the top-level site (CHIPS), so a third-party + /// cookie survives third-party cookie blocking. Requires . + ///
+ /// Cookies Having Independent Partitioned State + ///
public bool Partitioned { get; set; } + + /// The path prefix the cookie is sent for. Defaults to the current path, not to /. public string? Path { get; set; } + + /// + /// How far outside its own site the browser carries the cookie. null leaves the attribute + /// off, which browsers now treat as . + /// public SameSite? SameSite { get; set; } + + /// + /// Send the cookie over HTTPS only. Required by and by + /// . + /// public bool Secure { get; set; } + /// + /// Renders this cookie into the string document.cookie is assigned - name and value + /// percent-encoded, then each attribute that is set. Returns an empty string when + /// is empty. + /// public override string ToString() { if (string.IsNullOrEmpty(Name)) return string.Empty; @@ -85,6 +139,11 @@ private static string ValidateAttribute(string value, string attributeName) return value; } + /// + /// Parses one name=value pair out of document.cookie, percent-decoding both + /// halves. Returns null for anything without a name, and never fills in an attribute - + /// a read simply does not carry them. + /// public static ButilCookie? Parse(string rawCookie) { if (string.IsNullOrWhiteSpace(rawCookie)) return null; diff --git a/src/Butil/Bit.Butil/Publics/Cookie/CookieStoreItem.cs b/src/Butil/Bit.Butil/Publics/Cookie/CookieStoreItem.cs index 44999c472b7..68df93d9d99 100644 --- a/src/Butil/Bit.Butil/Publics/Cookie/CookieStoreItem.cs +++ b/src/Butil/Bit.Butil/Publics/Cookie/CookieStoreItem.cs @@ -8,18 +8,32 @@ namespace Bit.Butil; ///
public class CookieStoreItem { + /// The cookie's name. public string Name { get; set; } = string.Empty; + + /// The cookie's value. public string Value { get; set; } = string.Empty; + + /// The domain the cookie is scoped to, or null when it is host-only. public string? Domain { get; set; } + + /// The path prefix the cookie is sent for. public string? Path { get; set; } /// Expiration time. Null for session cookies. public DateTimeOffset? Expires { get; set; } + /// Whether the cookie is sent over HTTPS only. public bool Secure { get; set; } /// One of "strict", "lax", "none", or null. public string? SameSite { get; set; } + /// + /// Whether the cookie is partitioned by top-level site (CHIPS). Null in engines that do not + /// report the attribute. + ///
+ /// Cookies Having Independent Partitioned State + ///
public bool? Partitioned { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Cookie/SameSite.cs b/src/Butil/Bit.Butil/Publics/Cookie/SameSite.cs index 255d7f0a917..9c296056db9 100644 --- a/src/Butil/Bit.Butil/Publics/Cookie/SameSite.cs +++ b/src/Butil/Bit.Butil/Publics/Cookie/SameSite.cs @@ -1,10 +1,16 @@ namespace Bit.Butil; +/// +/// A cookie's SameSite attribute: how far outside its own site the browser will carry it. +///
+/// Set-Cookie SameSite +///
public enum SameSite { /// - /// Explicitly states no restrictions will be applied. + /// Explicitly states no restrictions will be applied. /// The cookie will be sent in all requests, both cross-site and same-site. + /// Browsers reject this unless the cookie is also marked Secure. /// None, diff --git a/src/Butil/Bit.Butil/Publics/Crypto/AesCbcCryptoAlgorithmParams.cs b/src/Butil/Bit.Butil/Publics/Crypto/AesCbcCryptoAlgorithmParams.cs index a25cc2a1630..74c264ae7c3 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/AesCbcCryptoAlgorithmParams.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/AesCbcCryptoAlgorithmParams.cs @@ -7,6 +7,7 @@ ///
public class AesCbcCryptoAlgorithmParams : ICryptoAlgorithmParams { + /// The Web Crypto algorithm identifier - always "AES-CBC". public string Name => "AES-CBC"; /// diff --git a/src/Butil/Bit.Butil/Publics/Crypto/AesCtrCryptoAlgorithmParams.cs b/src/Butil/Bit.Butil/Publics/Crypto/AesCtrCryptoAlgorithmParams.cs index 178edb864c7..6d8b0157bbf 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/AesCtrCryptoAlgorithmParams.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/AesCtrCryptoAlgorithmParams.cs @@ -7,6 +7,7 @@ /// public class AesCtrCryptoAlgorithmParams : ICryptoAlgorithmParams { + /// The Web Crypto algorithm identifier - always "AES-CTR". public string Name => "AES-CTR"; /// diff --git a/src/Butil/Bit.Butil/Publics/Crypto/AesGcmCryptoAlgorithmParams.cs b/src/Butil/Bit.Butil/Publics/Crypto/AesGcmCryptoAlgorithmParams.cs index ee576532c92..7e90044804f 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/AesGcmCryptoAlgorithmParams.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/AesGcmCryptoAlgorithmParams.cs @@ -7,6 +7,7 @@ /// public class AesGcmCryptoAlgorithmParams : ICryptoAlgorithmParams { + /// The Web Crypto algorithm identifier - always "AES-GCM". public string Name => "AES-GCM"; /// diff --git a/src/Butil/Bit.Butil/Publics/Crypto/AesGcmTagLength.cs b/src/Butil/Bit.Butil/Publics/Crypto/AesGcmTagLength.cs index a6b86a395d8..9c7e89a2eb0 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/AesGcmTagLength.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/AesGcmTagLength.cs @@ -5,11 +5,24 @@ /// public enum AesGcmTagLength { + /// 32 bits. Only for very short messages, and only when the space really matters. Four = 32, + + /// 64 bits. Eight = 64, + + /// 96 bits. Twelve = 96, + + /// 104 bits. Thirteen = 104, + + /// 112 bits. Fourteen = 112, + + /// 120 bits. Fifteen = 120, + + /// 128 bits - the full tag, and the default. Use this unless something forces otherwise. Sixteen = 128 } diff --git a/src/Butil/Bit.Butil/Publics/Crypto/CryptoAlgorithm.cs b/src/Butil/Bit.Butil/Publics/Crypto/CryptoAlgorithm.cs index 2868825eed2..e8f510d1487 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/CryptoAlgorithm.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/CryptoAlgorithm.cs @@ -1,9 +1,23 @@ namespace Bit.Butil; +/// +/// The algorithms this wrapper can encrypt and decrypt with. Which one a call uses is decided by +/// the instance passed to it; this enum names the same set for +/// code that has to choose one by value. +///
+/// SubtleCrypto.encrypt() +///
public enum CryptoAlgorithm { + /// RSA-OAEP - public-key encryption, limited to payloads smaller than the key. RsaOaem, + + /// AES-CTR - AES in counter mode, no built-in authentication. AesCtr, + + /// AES-CBC - AES in cipher-block-chaining mode, no built-in authentication. AesCbc, + + /// AES-GCM - AES with authentication built in; the sensible default of the four. AesGcm } diff --git a/src/Butil/Bit.Butil/Publics/Crypto/CryptoKeyHash.cs b/src/Butil/Bit.Butil/Publics/Crypto/CryptoKeyHash.cs index 930f61b63b3..a91db5ecdcf 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/CryptoKeyHash.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/CryptoKeyHash.cs @@ -1,8 +1,19 @@ namespace Bit.Butil; +/// +/// The digest an RSA key is imported with. It is a property of the key, not of the message, so the +/// same value has to be used to import a key that was exported under it. +///
+/// SubtleCrypto.importKey() +///
public enum CryptoKeyHash { + /// SHA-256. Sha256, + + /// SHA-384. Sha384, + + /// SHA-512. Sha512, } diff --git a/src/Butil/Bit.Butil/Publics/Crypto/ICryptoAlgorithmParams.cs b/src/Butil/Bit.Butil/Publics/Crypto/ICryptoAlgorithmParams.cs index 04e89b3b892..a6d91662b65 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/ICryptoAlgorithmParams.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/ICryptoAlgorithmParams.cs @@ -1,6 +1,15 @@ namespace Bit.Butil; +/// +/// The shape every algorithm parameter object shares: the algorithm's own name, plus whatever +/// extra fields that algorithm needs (an IV, a counter, additional data). Implemented by +/// , , +/// and . +///
+/// SubtleCrypto.encrypt() +///
public interface ICryptoAlgorithmParams { + /// The Web Crypto algorithm identifier - e.g. "AES-GCM". string Name { get; } } diff --git a/src/Butil/Bit.Butil/Publics/Crypto/RsaOaepCryptoAlgorithmParams.cs b/src/Butil/Bit.Butil/Publics/Crypto/RsaOaepCryptoAlgorithmParams.cs index 1d3dd11f586..2df0a82b538 100644 --- a/src/Butil/Bit.Butil/Publics/Crypto/RsaOaepCryptoAlgorithmParams.cs +++ b/src/Butil/Bit.Butil/Publics/Crypto/RsaOaepCryptoAlgorithmParams.cs @@ -7,6 +7,7 @@ ///
public class RsaOaepCryptoAlgorithmParams : ICryptoAlgorithmParams { + /// The Web Crypto algorithm identifier - always "RSA-OAEP". public string Name => "RSA-OAEP"; /// diff --git a/src/Butil/Bit.Butil/Publics/Document.cs b/src/Butil/Bit.Butil/Publics/Document.cs index be0fcd4a689..294bc6e63ef 100644 --- a/src/Butil/Bit.Butil/Publics/Document.cs +++ b/src/Butil/Bit.Butil/Publics/Document.cs @@ -23,6 +23,18 @@ public class Document(IJSRuntime js) : IAsyncDisposable // on disposal - no static state, no cross-circuit leak. private readonly DomEventsInterop _events = new(); + /// + /// Adds a listener for one document-level event. is the event-args + /// type the event maps to - , + /// and friends - and the payload is projected to that shape on the JavaScript side. + ///
+ /// EventTarget.addEventListener() + ///
+ /// The event name, e.g. a constant. + /// Called when the event fires. Removing it later matches on this delegate's identity. + /// Listen during the capture phase rather than the bubble phase. + /// Suppress the browser's own action for the event. + /// Stop the event travelling any further. public async Task AddEventListener( string domEvent, Action listener, @@ -432,12 +444,17 @@ public Task SubscribeDomContentLoaded(Action handler) return SubscribeEvent(ButilEvents.DomContentLoaded, bridge); } + /// Removes every listener this instance registered on the document and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/Document/CompatMode.cs b/src/Butil/Bit.Butil/Publics/Document/CompatMode.cs index 8bfd31a0ab4..46ca3146b75 100644 --- a/src/Butil/Bit.Butil/Publics/Document/CompatMode.cs +++ b/src/Butil/Bit.Butil/Publics/Document/CompatMode.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Which rendering mode the document was parsed in, decided by its doctype. +///
+/// Document.compatMode +///
public enum CompatMode { /// diff --git a/src/Butil/Bit.Butil/Publics/Document/DesignMode.cs b/src/Butil/Bit.Butil/Publics/Document/DesignMode.cs index 9e17ed79286..a61c2bd41de 100644 --- a/src/Butil/Bit.Butil/Publics/Document/DesignMode.cs +++ b/src/Butil/Bit.Butil/Publics/Document/DesignMode.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Whether the whole document is editable. +///
+/// Document.designMode +///
public enum DesignMode { /// diff --git a/src/Butil/Bit.Butil/Publics/Document/DocumentDir.cs b/src/Butil/Bit.Butil/Publics/Document/DocumentDir.cs index aa2cdfa13b7..25c79b39f33 100644 --- a/src/Butil/Bit.Butil/Publics/Document/DocumentDir.cs +++ b/src/Butil/Bit.Butil/Publics/Document/DocumentDir.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// The document's base text direction. +///
+/// Document.dir +///
public enum DocumentDir { /// diff --git a/src/Butil/Bit.Butil/Publics/Element/ContentEditable.cs b/src/Butil/Bit.Butil/Publics/Element/ContentEditable.cs index 700e07eecc8..8128d988f85 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ContentEditable.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ContentEditable.cs @@ -1,8 +1,12 @@ namespace Bit.Butil; +/// +/// Whether an element's content can be edited in place. +///
+/// contenteditable +///
public enum ContentEditable { - /// /// Indicates that the element inherits its parent's editable status. /// diff --git a/src/Butil/Bit.Butil/Publics/Element/ElementDir.cs b/src/Butil/Bit.Butil/Publics/Element/ElementDir.cs index 2971a1c26c4..1202c28c40b 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ElementDir.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ElementDir.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// An element's text direction. +///
+/// dir +///
public enum ElementDir { /// diff --git a/src/Butil/Bit.Butil/Publics/Element/EnterKeyHint.cs b/src/Butil/Bit.Butil/Publics/Element/EnterKeyHint.cs index 43b4845cfc4..aae9f07c78e 100644 --- a/src/Butil/Bit.Butil/Publics/Element/EnterKeyHint.cs +++ b/src/Butil/Bit.Butil/Publics/Element/EnterKeyHint.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// What the virtual keyboard's action key should say. A hint only: the browser still decides. +///
+/// enterkeyhint +///
public enum EnterKeyHint { /// diff --git a/src/Butil/Bit.Butil/Publics/Element/FullScreenNavigationUI.cs b/src/Butil/Bit.Butil/Publics/Element/FullScreenNavigationUI.cs new file mode 100644 index 00000000000..c27c7a08083 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Element/FullScreenNavigationUI.cs @@ -0,0 +1,20 @@ +namespace Bit.Butil; + +/// +/// Whether the browser should keep its own navigation controls visible while an element is +/// fullscreen. Only a request that is granted honours this - it is a hint, and engines are free to +/// ignore it. +///
+/// Element.requestFullscreen() navigationUI +///
+public enum FullScreenNavigationUI +{ + /// Let the browser decide. The default. + Auto, + + /// Ask for the whole screen, with no browser chrome over it. + Hide, + + /// Ask the browser to keep its navigation controls visible. + Show +} diff --git a/src/Butil/Bit.Butil/Publics/Element/FullscreenOptions.cs b/src/Butil/Bit.Butil/Publics/Element/FullScreenOptions.cs similarity index 54% rename from src/Butil/Bit.Butil/Publics/Element/FullscreenOptions.cs rename to src/Butil/Bit.Butil/Publics/Element/FullScreenOptions.cs index 794eac42215..3b05124eae6 100644 --- a/src/Butil/Bit.Butil/Publics/Element/FullscreenOptions.cs +++ b/src/Butil/Bit.Butil/Publics/Element/FullScreenOptions.cs @@ -1,7 +1,16 @@ namespace Bit.Butil; +/// +/// The options bag for a fullscreen request. +///
+/// Element.requestFullscreen() +///
public class FullScreenOptions { + /// + /// Whether browser navigation controls stay visible. null is the same as + /// . + /// public FullScreenNavigationUI? NavigationUI { get; set; } internal FullScreenJsOptions ToJsObject() diff --git a/src/Butil/Bit.Butil/Publics/Element/FullscreenNavigationUI.cs b/src/Butil/Bit.Butil/Publics/Element/FullscreenNavigationUI.cs deleted file mode 100644 index 9008a321281..00000000000 --- a/src/Butil/Bit.Butil/Publics/Element/FullscreenNavigationUI.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Bit.Butil; - -public enum FullScreenNavigationUI -{ - Auto, - Hide, - Show -} diff --git a/src/Butil/Bit.Butil/Publics/Element/Hidden.cs b/src/Butil/Bit.Butil/Publics/Element/Hidden.cs index 0cc7d14f92b..3a08b4723e4 100644 --- a/src/Butil/Bit.Butil/Publics/Element/Hidden.cs +++ b/src/Butil/Bit.Butil/Publics/Element/Hidden.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Whether an element is hidden, and how thoroughly. +///
+/// hidden +///
public enum Hidden { /// diff --git a/src/Butil/Bit.Butil/Publics/Element/InputMode.cs b/src/Butil/Bit.Butil/Publics/Element/InputMode.cs index 9143a68f532..f8dfb7dfeba 100644 --- a/src/Butil/Bit.Butil/Publics/Element/InputMode.cs +++ b/src/Butil/Bit.Butil/Publics/Element/InputMode.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Which virtual keyboard to bring up for an editable element. A hint only: the browser still decides. +///
+/// inputmode +///
public enum InputMode { /// diff --git a/src/Butil/Bit.Butil/Publics/Element/MediaElementState.cs b/src/Butil/Bit.Butil/Publics/Element/MediaElementState.cs index 528ef46dc81..e5c4010e5cf 100644 --- a/src/Butil/Bit.Butil/Publics/Element/MediaElementState.cs +++ b/src/Butil/Bit.Butil/Publics/Element/MediaElementState.cs @@ -6,6 +6,7 @@ namespace Bit.Butil; /// public class MediaElementState { + /// True while playback is paused - which it also is before it has ever started. public bool Paused { get; set; } /// True once playback has run past the end (and looping is off). @@ -14,10 +15,13 @@ public class MediaElementState /// True while a seek is in flight. public bool Seeking { get; set; } + /// True while the element is muted, independent of . public bool Muted { get; set; } + /// True when playback restarts from the beginning on reaching the end. public bool Loop { get; set; } + /// True when the element asked to start on its own. Browsers still refuse unmuted autoplay without a user gesture. public bool Autoplay { get; set; } /// 0 to 1. Always 1 on iOS, where volume is a hardware control. diff --git a/src/Butil/Bit.Butil/Publics/Element/Rect.cs b/src/Butil/Bit.Butil/Publics/Element/Rect.cs index d74d3822108..258e818b3c4 100644 --- a/src/Butil/Bit.Butil/Publics/Element/Rect.cs +++ b/src/Butil/Bit.Butil/Publics/Element/Rect.cs @@ -1,9 +1,21 @@ namespace Bit.Butil; +/// +/// A rectangle in CSS pixels, as returned by the DOM's own geometry APIs. +///
+/// DOMRect +///
public class Rect { + /// The rectangle's height. public double Height { get; set; } + + /// The rectangle's width. public double Width { get; set; } + + /// The x coordinate of the rectangle's origin. public double X { get; set; } + + /// The y coordinate of the rectangle's origin. public double Y { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Element/ScrollBehavior.cs b/src/Butil/Bit.Butil/Publics/Element/ScrollBehavior.cs index 81af34bb07a..bcfb919d621 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ScrollBehavior.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ScrollBehavior.cs @@ -1,8 +1,21 @@ namespace Bit.Butil; +/// +/// Whether a programmatic scroll animates or jumps. +///
+/// Element.scrollIntoView() +///
public enum ScrollBehavior { + /// + /// Follow the element's scroll-behavior CSS property, which is itself auto + /// (an instant jump) unless a stylesheet says otherwise. The default. + /// Auto, + + /// Jump straight to the destination, ignoring any CSS scroll-behavior. Instant, + + /// Animate, at the engine's own pace. Smooth } diff --git a/src/Butil/Bit.Butil/Publics/Element/ScrollIntoViewOptions.cs b/src/Butil/Bit.Butil/Publics/Element/ScrollIntoViewOptions.cs index f292001cf47..5784d74756f 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ScrollIntoViewOptions.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ScrollIntoViewOptions.cs @@ -1,11 +1,27 @@ namespace Bit.Butil; +/// +/// The options bag for scrolling an element into view. +///
+/// Element.scrollIntoView() +///
public class ScrollIntoViewOptions { + /// + /// Whether the scroll animates. null is the same as . + /// public ScrollBehavior? Behavior { get; set; } + /// + /// Where the element lands along the block axis. null is the same as + /// . + /// public ScrollLogicalPosition? Block { get; set; } + /// + /// Where the element lands along the inline axis. null is the same as + /// . + /// public ScrollLogicalPosition? Inline { get; set; } internal ScrollIntoViewJsOptions ToJsObject() diff --git a/src/Butil/Bit.Butil/Publics/Element/ScrollLogicalPosition.cs b/src/Butil/Bit.Butil/Publics/Element/ScrollLogicalPosition.cs index 32032a7094c..c217b2adf56 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ScrollLogicalPosition.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ScrollLogicalPosition.cs @@ -1,9 +1,22 @@ namespace Bit.Butil; +/// +/// Where the element should come to rest inside the scrolling box - along the block axis +/// (vertically, in a horizontal writing mode) or the inline axis. +///
+/// Element.scrollIntoView() +///
public enum ScrollLogicalPosition { + /// Align the element's start edge with the container's start edge. The default. Start, + + /// Centre the element in the container. Center, + + /// Align the element's end edge with the container's end edge. End, + + /// Scroll as little as possible - nothing at all if the element is already in view. Nearest } diff --git a/src/Butil/Bit.Butil/Publics/Element/ScrollOptions.cs b/src/Butil/Bit.Butil/Publics/Element/ScrollOptions.cs index 51cea90d99b..fdbc3dae32f 100644 --- a/src/Butil/Bit.Butil/Publics/Element/ScrollOptions.cs +++ b/src/Butil/Bit.Butil/Publics/Element/ScrollOptions.cs @@ -1,11 +1,21 @@ namespace Bit.Butil; +/// +/// The options bag for a scroll to an absolute or relative offset. +///
+/// Element.scroll() +///
public class ScrollOptions { + /// + /// Whether the scroll animates. null is the same as . + /// public ScrollBehavior? Behavior { get; set; } + /// The vertical offset in CSS pixels. null leaves the current offset alone. public double? Top { get; set; } + /// The horizontal offset in CSS pixels. null leaves the current offset alone. public double? Left { get; set; } internal ScrollJsOptions ToJsObject() diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilDragEventArgs.cs b/src/Butil/Bit.Butil/Publics/Events/ButilDragEventArgs.cs index a07e06a89e5..56809aeb194 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilDragEventArgs.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilDragEventArgs.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Bit.Butil; @@ -18,20 +18,51 @@ public class ButilDragEventArgs : EventArgs "offsetX", "offsetY", "pageX", "pageY", "screenX", "screenY", "shiftKey", "x", "y"]; + /// True when Alt was down as the event fired. public bool AltKey { get; set; } + + /// The button that changed state, when one did: 0 primary, 1 middle, 2 secondary. public int Button { get; set; } + + /// A bitmask of every button held at that moment: 1 primary, 2 secondary, 4 middle. public int Buttons { get; set; } + + /// The X coordinate in viewport coordinates. public double ClientX { get; set; } + + /// The Y coordinate in viewport coordinates. public double ClientY { get; set; } + + /// True when Ctrl was down as the event fired. public bool CtrlKey { get; set; } + + /// True when the Meta key (Command on macOS, the Windows key elsewhere) was down as the event fired. public bool MetaKey { get; set; } + + /// The X coordinate relative to the target's padding edge. public double OffsetX { get; set; } + + /// The Y coordinate relative to the target's padding edge. public double OffsetY { get; set; } + + /// The X coordinate relative to the whole document, so it includes the page scroll. public double PageX { get; set; } + + /// The Y coordinate relative to the whole document, so it includes the page scroll. public double PageY { get; set; } + + /// The X coordinate in screen coordinates. public double ScreenX { get; set; } + + /// The Y coordinate in screen coordinates. public double ScreenY { get; set; } + + /// True when Shift was down as the event fired. public bool ShiftKey { get; set; } + + /// Alias for . public double X { get; set; } + + /// Alias for . public double Y { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilEvents.cs b/src/Butil/Bit.Butil/Publics/Events/ButilEvents.cs index 3057fd58ffe..6aec9fc9314 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilEvents.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilEvents.cs @@ -1,92 +1,230 @@ namespace Bit.Butil; +/// +/// The DOM event names AddEventListener takes, as constants, so a typo is a compile error +/// rather than a listener that never fires. Nothing here is exhaustive - any event name the browser +/// knows works just as well as a plain string; these are the ones worth not misspelling. +///
+/// Event reference +///
public class ButilEvents { // ─── Mouse ──────────────────────────────────────────────────────────── + + /// click - a primary-button press and release on the same element. public const string Click = "click"; + + /// dblclick - two clicks close enough together to count as one gesture. public const string DblClick = "dblclick"; + + /// mousedown - a button was pressed over the element. public const string MouseDown = "mousedown"; + + /// mouseup - a button was released over the element. public const string MouseUp = "mouseup"; + + /// mousemove - the pointer moved while over the element. Fires very often; throttle the handler. public const string MouseMove = "mousemove"; + + /// mouseenter - the pointer entered the element. Does not bubble, and ignores movement between children. public const string MouseEnter = "mouseenter"; + + /// mouseleave - the pointer left the element. Does not bubble, and ignores movement between children. public const string MouseLeave = "mouseleave"; + + /// mouseover - the pointer entered the element or one of its descendants. Bubbles. public const string MouseOver = "mouseover"; + + /// mouseout - the pointer left the element or one of its descendants. Bubbles. public const string MouseOut = "mouseout"; + + /// contextmenu - the context menu was asked for. Cancel it to substitute your own. public const string ContextMenu = "contextmenu"; // ─── Keyboard ───────────────────────────────────────────────────────── + + /// keydown - a key went down. Repeats while the key is held. public const string KeyDown = "keydown"; + + /// keyup - a key came back up. public const string KeyUp = "keyup"; + + /// keypress - deprecated, and never fired for non-printing keys. Use . public const string KeyPress = "keypress"; // ─── Pointer ────────────────────────────────────────────────────────── + + /// pointerdown - a pointer (mouse, pen or touch) went down. public const string PointerDown = "pointerdown"; + + /// pointerup - a pointer came back up. public const string PointerUp = "pointerup"; + + /// pointermove - a pointer moved. Fires very often; throttle the handler. public const string PointerMove = "pointermove"; + + /// pointerenter - a pointer entered the element. Does not bubble. public const string PointerEnter = "pointerenter"; + + /// pointerleave - a pointer left the element. Does not bubble. public const string PointerLeave = "pointerleave"; + + /// pointerover - a pointer entered the element or a descendant. Bubbles. public const string PointerOver = "pointerover"; + + /// pointerout - a pointer left the element or a descendant. Bubbles. public const string PointerOut = "pointerout"; + + /// pointercancel - the browser took the pointer over, e.g. to start a scroll. No up event follows. public const string PointerCancel = "pointercancel"; + + /// gotpointercapture - this element now receives every event from that pointer. public const string GotPointerCapture = "gotpointercapture"; + + /// lostpointercapture - the capture ended, whether released or taken away. public const string LostPointerCapture = "lostpointercapture"; // ─── Touch ──────────────────────────────────────────────────────────── + + /// touchstart - a finger touched the surface. public const string TouchStart = "touchstart"; + + /// touchend - a finger left the surface. public const string TouchEnd = "touchend"; + + /// touchmove - a touching finger moved. Fires very often; throttle the handler. public const string TouchMove = "touchmove"; + + /// touchcancel - the browser took the touch over, e.g. to scroll. public const string TouchCancel = "touchcancel"; // ─── Wheel / scroll ─────────────────────────────────────────────────── + + /// wheel - a wheel or trackpad scroll gesture, before any scrolling happens. public const string Wheel = "wheel"; + + /// scroll - the scroll position changed. Does not bubble from an element, though it does from the document. public const string Scroll = "scroll"; // ─── Focus ──────────────────────────────────────────────────────────── + + /// focus - the element gained focus. Does not bubble; use for a delegated handler. public const string Focus = "focus"; + + /// focusin - the element or a descendant gained focus. Bubbles. public const string FocusIn = "focusin"; + + /// blur - the element lost focus. Does not bubble; use for a delegated handler. public const string Blur = "blur"; + + /// focusout - the element or a descendant lost focus. Bubbles. public const string FocusOut = "focusout"; // ─── Input ──────────────────────────────────────────────────────────── + + /// input - the value changed, on every keystroke. public const string Input = "input"; + + /// change - the value changed and was committed: on blur for a text field, immediately for a checkbox or a select. public const string Change = "change"; + + /// submit - a form is being submitted. Cancel it to take over. public const string Submit = "submit"; + + /// reset - a form is being reset. public const string Reset = "reset"; + + /// beforeinput - the value is about to change. Cancellable, unlike . public const string BeforeInput = "beforeinput"; - // ─── Drag & drop ────────────────────────────────────────────────────── + // ─── Drag and drop ──────────────────────────────────────────────────── + + /// dragstart - a drag began on this element. public const string DragStart = "dragstart"; + + /// drag - the drag is in progress, fired repeatedly on the source. public const string Drag = "drag"; + + /// dragend - the drag finished, whether it was dropped or abandoned. public const string DragEnd = "dragend"; + + /// dragenter - a drag entered this element, a possible drop target. public const string DragEnter = "dragenter"; + + /// dragleave - a drag left this element. public const string DragLeave = "dragleave"; + + /// dragover - a drag is over this element. Cancel it, or the drop never happens. public const string DragOver = "dragover"; + + /// drop - a drag was released over this element. public const string Drop = "drop"; // ─── Clipboard ──────────────────────────────────────────────────────── + + /// copy - the selection is being copied. public const string Copy = "copy"; + + /// cut - the selection is being cut. public const string Cut = "cut"; + + /// paste - clipboard content is being pasted in. public const string Paste = "paste"; // ─── Composition ────────────────────────────────────────────────────── + + /// compositionstart - an IME composition session began. public const string CompositionStart = "compositionstart"; + + /// compositionupdate - the text being composed changed. public const string CompositionUpdate = "compositionupdate"; + + /// compositionend - the composition was committed or abandoned. public const string CompositionEnd = "compositionend"; // ─── Window-only ────────────────────────────────────────────────────── + + /// resize - the window was resized. Fires on the window, not on an element. public const string Resize = "resize"; + + /// online - the browser believes it regained connectivity. It only knows about the local link, not about reachability. public const string Online = "online"; + + /// offline - the browser believes it lost connectivity. public const string Offline = "offline"; + + /// hashchange - the URL fragment changed. public const string HashChange = "hashchange"; + + /// languagechange - the preferred languages changed. public const string LanguageChange = "languagechange"; + + /// load - the page and all of its subresources finished loading. public const string Load = "load"; + + /// + /// unload - deprecated, and unreliable on mobile, where a backgrounded page is often + /// killed without it ever firing. Use to persist state. + /// public const string Unload = "unload"; // ─── Document-level visibility / fullscreen ─────────────────────────── + + /// visibilitychange - the document became hidden or visible. The reliable place to persist state. public const string VisibilityChange = "visibilitychange"; + + /// fullscreenchange - the document entered or left fullscreen. public const string FullscreenChange = "fullscreenchange"; + + /// fullscreenerror - a fullscreen request was refused. public const string FullscreenError = "fullscreenerror"; + + /// pointerlockchange - the pointer was locked to an element, or released. public const string PointerLockChange = "pointerlockchange"; + + /// pointerlockerror - a pointer-lock request was refused. public const string PointerLockError = "pointerlockerror"; + + /// DOMContentLoaded - the HTML is parsed. Note the capitalisation: it is the one event name here that is not lower-case. public const string DomContentLoaded = "DOMContentLoaded"; } diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilKeyCodes.cs b/src/Butil/Bit.Butil/Publics/Events/ButilKeyCodes.cs index 07b7891ec55..a6fe710bb60 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilKeyCodes.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilKeyCodes.cs @@ -1,120 +1,238 @@ namespace Bit.Butil; +/// +/// The KeyboardEvent.code values shortcuts are registered against. +/// +/// A code names a physical key position, not the character it produces: is the +/// key where A sits on a US layout, and it stays on an AZERTY keyboard +/// where that key prints Q. That is what makes it the right thing to match a shortcut on - +/// Ctrl+ lands under the same finger everywhere - and the wrong thing to build +/// text from. +/// +///
+/// KeyboardEvent.code +///
public static class ButilKeyCodes { // Letters + /// The KeyA code - the key that prints A on a US layout. public const string KeyA = "KeyA"; + /// The KeyB code - the key that prints B on a US layout. public const string KeyB = "KeyB"; + /// The KeyC code - the key that prints C on a US layout. public const string KeyC = "KeyC"; + /// The KeyD code - the key that prints D on a US layout. public const string KeyD = "KeyD"; + /// The KeyE code - the key that prints E on a US layout. public const string KeyE = "KeyE"; + /// The KeyF code - the key that prints F on a US layout. public const string KeyF = "KeyF"; + /// The KeyG code - the key that prints G on a US layout. public const string KeyG = "KeyG"; + /// The KeyH code - the key that prints H on a US layout. public const string KeyH = "KeyH"; + /// The KeyI code - the key that prints I on a US layout. public const string KeyI = "KeyI"; + /// The KeyJ code - the key that prints J on a US layout. public const string KeyJ = "KeyJ"; + /// The KeyK code - the key that prints K on a US layout. public const string KeyK = "KeyK"; + /// The KeyL code - the key that prints L on a US layout. public const string KeyL = "KeyL"; + /// The KeyM code - the key that prints M on a US layout. public const string KeyM = "KeyM"; + /// The KeyN code - the key that prints N on a US layout. public const string KeyN = "KeyN"; + /// The KeyO code - the key that prints O on a US layout. public const string KeyO = "KeyO"; + /// The KeyP code - the key that prints P on a US layout. public const string KeyP = "KeyP"; + /// The KeyQ code - the key that prints Q on a US layout. public const string KeyQ = "KeyQ"; + /// The KeyR code - the key that prints R on a US layout. public const string KeyR = "KeyR"; + /// The KeyS code - the key that prints S on a US layout. public const string KeyS = "KeyS"; + /// The KeyT code - the key that prints T on a US layout. public const string KeyT = "KeyT"; + /// The KeyU code - the key that prints U on a US layout. public const string KeyU = "KeyU"; + /// The KeyV code - the key that prints V on a US layout. public const string KeyV = "KeyV"; + /// The KeyW code - the key that prints W on a US layout. public const string KeyW = "KeyW"; + /// The KeyX code - the key that prints X on a US layout. public const string KeyX = "KeyX"; + /// The KeyY code - the key that prints Y on a US layout. public const string KeyY = "KeyY"; + /// The KeyZ code - the key that prints Z on a US layout. public const string KeyZ = "KeyZ"; // Digits + /// The Digit0 code - 0 on the number row. public const string Digit0 = "Digit0"; + /// The Digit1 code - 1 on the number row. public const string Digit1 = "Digit1"; + /// The Digit2 code - 2 on the number row. public const string Digit2 = "Digit2"; + /// The Digit3 code - 3 on the number row. public const string Digit3 = "Digit3"; + /// The Digit4 code - 4 on the number row. public const string Digit4 = "Digit4"; + /// The Digit5 code - 5 on the number row. public const string Digit5 = "Digit5"; + /// The Digit6 code - 6 on the number row. public const string Digit6 = "Digit6"; + /// The Digit7 code - 7 on the number row. public const string Digit7 = "Digit7"; + /// The Digit8 code - 8 on the number row. public const string Digit8 = "Digit8"; + /// The Digit9 code - 9 on the number row. public const string Digit9 = "Digit9"; + /// The Numpad0 code - 0 on the numeric keypad. public const string Numpad0 = "Numpad0"; + /// The Numpad1 code - 1 on the numeric keypad. public const string Numpad1 = "Numpad1"; + /// The Numpad2 code - 2 on the numeric keypad. public const string Numpad2 = "Numpad2"; + /// The Numpad3 code - 3 on the numeric keypad. public const string Numpad3 = "Numpad3"; + /// The Numpad4 code - 4 on the numeric keypad. public const string Numpad4 = "Numpad4"; + /// The Numpad5 code - 5 on the numeric keypad. public const string Numpad5 = "Numpad5"; + /// The Numpad6 code - 6 on the numeric keypad. public const string Numpad6 = "Numpad6"; + /// The Numpad7 code - 7 on the numeric keypad. public const string Numpad7 = "Numpad7"; + /// The Numpad8 code - 8 on the numeric keypad. public const string Numpad8 = "Numpad8"; + /// The Numpad9 code - 9 on the numeric keypad. public const string Numpad9 = "Numpad9"; + /// The NumLock code. public const string NumLock = "NumLock"; + /// The NumpadAdd code. public const string NumpadAdd = "NumpadAdd"; + /// The NumpadMultiply code. public const string NumpadMultiply = "NumpadMultiply"; + /// The NumpadSubtract code. public const string NumpadSubtract = "NumpadSubtract"; + /// The NumpadDecimal code. public const string NumpadDecimal = "NumpadDecimal"; + /// The NumpadDivide code. public const string NumpadDivide = "NumpadDivide"; + /// The NumpadEnter code. public const string NumpadEnter = "NumpadEnter"; + /// The NumpadEqual code. public const string NumpadEqual = "NumpadEqual"; + /// The NumpadComma code. public const string NumpadComma = "NumpadComma"; // Function keys + /// The F1 code. public const string F1 = "F1"; + /// The F2 code. public const string F2 = "F2"; + /// The F3 code. public const string F3 = "F3"; + /// The F4 code. public const string F4 = "F4"; + /// The F5 code. public const string F5 = "F5"; + /// The F6 code. public const string F6 = "F6"; + /// The F7 code. public const string F7 = "F7"; + /// The F8 code. public const string F8 = "F8"; + /// The F9 code. public const string F9 = "F9"; + /// The F10 code. public const string F10 = "F10"; + /// The F11 code. public const string F11 = "F11"; + /// The F12 code. public const string F12 = "F12"; + /// The Backspace code. public const string Backspace = "Backspace"; + /// The Tab code. public const string Tab = "Tab"; + /// The Enter code. public const string Enter = "Enter"; + /// The ShiftLeft code. public const string ShiftLeft = "ShiftLeft"; + /// The ShiftRight code. public const string ShiftRight = "ShiftRight"; + /// The ControlLeft code. public const string ControlLeft = "ControlLeft"; + /// The ControlRight code. public const string ControlRight = "ControlRight"; + /// The AltLeft code. public const string AltLeft = "AltLeft"; + /// The AltRight code. public const string AltRight = "AltRight"; + /// The Pause code. public const string PauseBreak = "Pause"; + /// The CapsLock code. public const string CapsLock = "CapsLock"; + /// The Escape code. public const string Escape = "Escape"; + /// The Space code. public const string Space = "Space"; + /// The PageUp code. public const string PageUp = "PageUp"; + /// The PageDown code. public const string PageDown = "PageDown"; + /// The End code. public const string End = "End"; + /// The Home code. public const string Home = "Home"; + /// The ArrowLeft code. public const string ArrowLeft = "ArrowLeft"; + /// The ArrowUp code. public const string ArrowUp = "ArrowUp"; + /// The ArrowRight code. public const string ArrowRight = "ArrowRight"; + /// The ArrowDown code. public const string ArrowDown = "ArrowDown"; + /// The PrintScreen code. public const string PrintScreen = "PrintScreen"; + /// The Insert code. public const string Insert = "Insert"; + /// The Delete code. public const string Delete = "Delete"; + /// The MetaLeft code. public const string MetaLeft = "MetaLeft"; + /// The MetaRight code. public const string MetaRight = "MetaRight"; + /// The ContextMenu code. public const string ContextMenu = "ContextMenu"; + /// The ScrollLock code. public const string ScrollLock = "ScrollLock"; // Symbols + /// The Semicolon code. public const string Semicolon = "Semicolon"; + /// The Equal code. public const string Equal = "Equal"; + /// The Comma code. public const string Comma = "Comma"; + /// The Minus code. public const string Minus = "Minus"; + /// The Period code. public const string Period = "Period"; + /// The Slash code. public const string Slash = "Slash"; + /// The Backquote code. public const string Backquote = "Backquote"; + /// The BracketLeft code. public const string BracketLeft = "BracketLeft"; + /// The Backslash code. public const string Backslash = "Backslash"; + /// The BracketRight code. public const string BracketRight = "BracketRight"; + /// The Quote code. public const string Quote = "Quote"; } diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilModifiers.cs b/src/Butil/Bit.Butil/Publics/Events/ButilModifiers.cs index 8bfd9e226e7..915a1e2f2ee 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilModifiers.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilModifiers.cs @@ -2,12 +2,28 @@ namespace Bit.Butil; +/// +/// The modifier keys a shortcut requires. Combine them with |; the +/// match is exact, so a shortcut registered for alone does not fire when Shift +/// is also down. +///
+/// KeyboardEvent.getModifierState() +///
[Flags] public enum ButilModifiers { + /// No modifier - the key on its own. None = 0, + + /// Alt, which is Option on macOS. Alt = 1, + + /// Control. Ctrl = 2, + + /// The Meta key: Command on macOS, the Windows key elsewhere. Meta = 4, + + /// Shift. Shift = 8 } diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilPointerEventArgs.cs b/src/Butil/Bit.Butil/Publics/Events/ButilPointerEventArgs.cs index 46d2d38a664..82529d3bb64 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilPointerEventArgs.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilPointerEventArgs.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Bit.Butil; @@ -15,23 +15,58 @@ public class ButilPointerEventArgs : EventArgs "pointerId", "width", "height", "pressure", "tangentialPressure", "tiltX", "tiltY", "twist", "pointerType", "isPrimary"]; + /// True when Alt was down as the event fired. public bool AltKey { get; set; } + + /// The button that changed state, when one did: 0 primary, 1 middle, 2 secondary. public int Button { get; set; } + + /// A bitmask of every button held at that moment: 1 primary, 2 secondary, 4 middle. public int Buttons { get; set; } + + /// The X coordinate in viewport coordinates. public double ClientX { get; set; } + + /// The Y coordinate in viewport coordinates. public double ClientY { get; set; } + + /// True when Ctrl was down as the event fired. public bool CtrlKey { get; set; } + + /// True when the Meta key (Command on macOS, the Windows key elsewhere) was down as the event fired. public bool MetaKey { get; set; } + + /// The X distance moved since the previous event of the same kind. public double MovementX { get; set; } + + /// The Y distance moved since the previous event of the same kind. public double MovementY { get; set; } + + /// The X coordinate relative to the target's padding edge. public double OffsetX { get; set; } + + /// The Y coordinate relative to the target's padding edge. public double OffsetY { get; set; } + + /// The X coordinate relative to the whole document, so it includes the page scroll. public double PageX { get; set; } + + /// The Y coordinate relative to the whole document, so it includes the page scroll. public double PageY { get; set; } + + /// The X coordinate in screen coordinates. public double ScreenX { get; set; } + + /// The Y coordinate in screen coordinates. public double ScreenY { get; set; } + + /// True when Shift was down as the event fired. public bool ShiftKey { get; set; } + + /// Alias for . public double X { get; set; } + + /// Alias for . public double Y { get; set; } /// Identifier for the pointer that produced the event (see PointerEvent.pointerId). diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilTouchEventArgs.cs b/src/Butil/Bit.Butil/Publics/Events/ButilTouchEventArgs.cs index d1e04214e85..a2c20a8f17c 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilTouchEventArgs.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilTouchEventArgs.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Bit.Butil; @@ -14,30 +14,24 @@ public class ButilTouchEventArgs : EventArgs "altKey", "ctrlKey", "metaKey", "shiftKey", "touches", "targetTouches", "changedTouches"]; + /// True when Alt was down as the event fired. public bool AltKey { get; set; } + + /// True when Ctrl was down as the event fired. public bool CtrlKey { get; set; } + + /// True when the Meta key (Command on macOS, the Windows key elsewhere) was down as the event fired. public bool MetaKey { get; set; } + + /// True when Shift was down as the event fired. public bool ShiftKey { get; set; } + /// Every touch point currently on the surface, anywhere in the document. public ButilTouchPoint[] Touches { get; set; } = []; + + /// The subset of whose contact started on this element. public ButilTouchPoint[] TargetTouches { get; set; } = []; - public ButilTouchPoint[] ChangedTouches { get; set; } = []; -} -/// -/// Individual touch point inside a . -/// -public class ButilTouchPoint -{ - public int Identifier { get; set; } - public double ClientX { get; set; } - public double ClientY { get; set; } - public double PageX { get; set; } - public double PageY { get; set; } - public double ScreenX { get; set; } - public double ScreenY { get; set; } - public double RadiusX { get; set; } - public double RadiusY { get; set; } - public double RotationAngle { get; set; } - public double Force { get; set; } + /// The touch points this event is about - the ones that went down, moved or came up. + public ButilTouchPoint[] ChangedTouches { get; set; } = []; } diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilTouchPoint.cs b/src/Butil/Bit.Butil/Publics/Events/ButilTouchPoint.cs new file mode 100644 index 00000000000..9b03ec35c0d --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Events/ButilTouchPoint.cs @@ -0,0 +1,42 @@ +using System; + +namespace Bit.Butil; + +/// +/// Individual touch point inside a . +/// +public class ButilTouchPoint +{ + /// A number identifying this contact for as long as the finger stays down. + public int Identifier { get; set; } + + /// The X coordinate in viewport coordinates. + public double ClientX { get; set; } + + /// The Y coordinate in viewport coordinates. + public double ClientY { get; set; } + + /// The X coordinate relative to the whole document, so it includes the page scroll. + public double PageX { get; set; } + + /// The Y coordinate relative to the whole document, so it includes the page scroll. + public double PageY { get; set; } + + /// The X coordinate in screen coordinates. + public double ScreenX { get; set; } + + /// The Y coordinate in screen coordinates. + public double ScreenY { get; set; } + + /// Half the width of the contact area, in CSS pixels. 1 where the device does not report it. + public double RadiusX { get; set; } + + /// Half the height of the contact area, in CSS pixels. 1 where the device does not report it. + public double RadiusY { get; set; } + + /// Degrees of rotation of the contact ellipse. 0 where the device does not report it. + public double RotationAngle { get; set; } + + /// Contact pressure in [0, 1]. 0 where the device does not report it. + public double Force { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/Events/ButilWheelEventArgs.cs b/src/Butil/Bit.Butil/Publics/Events/ButilWheelEventArgs.cs index 7710f7021f7..958d028756b 100644 --- a/src/Butil/Bit.Butil/Publics/Events/ButilWheelEventArgs.cs +++ b/src/Butil/Bit.Butil/Publics/Events/ButilWheelEventArgs.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Bit.Butil; @@ -12,19 +12,46 @@ public class ButilWheelEventArgs : EventArgs "offsetX", "offsetY", "pageX", "pageY", "screenX", "screenY", "shiftKey", "deltaX", "deltaY", "deltaZ", "deltaMode"]; + /// True when Alt was down as the event fired. public bool AltKey { get; set; } + + /// The button that changed state, when one did: 0 primary, 1 middle, 2 secondary. public int Button { get; set; } + + /// A bitmask of every button held at that moment: 1 primary, 2 secondary, 4 middle. public int Buttons { get; set; } + + /// The X coordinate in viewport coordinates. public double ClientX { get; set; } + + /// The Y coordinate in viewport coordinates. public double ClientY { get; set; } + + /// True when Ctrl was down as the event fired. public bool CtrlKey { get; set; } + + /// True when the Meta key (Command on macOS, the Windows key elsewhere) was down as the event fired. public bool MetaKey { get; set; } + + /// The X coordinate relative to the target's padding edge. public double OffsetX { get; set; } + + /// The Y coordinate relative to the target's padding edge. public double OffsetY { get; set; } + + /// The X coordinate relative to the whole document, so it includes the page scroll. public double PageX { get; set; } + + /// The Y coordinate relative to the whole document, so it includes the page scroll. public double PageY { get; set; } + + /// The X coordinate in screen coordinates. public double ScreenX { get; set; } + + /// The Y coordinate in screen coordinates. public double ScreenY { get; set; } + + /// True when Shift was down as the event fired. public bool ShiftKey { get; set; } /// Horizontal scroll amount. diff --git a/src/Butil/Bit.Butil/Publics/Fetch.cs b/src/Butil/Bit.Butil/Publics/Fetch.cs index 505554cbb24..505986fee59 100644 --- a/src/Butil/Bit.Butil/Publics/Fetch.cs +++ b/src/Butil/Bit.Butil/Publics/Fetch.cs @@ -98,6 +98,7 @@ public async Task Start(FetchRequest request) return new AbortableFetch(js, id); } + /// Releases the interop reference used for progress callbacks. Requests still in flight are not aborted - use their own for that. public ValueTask DisposeAsync() { _dotNetRef?.Dispose(); diff --git a/src/Butil/Bit.Butil/Publics/Fetch/AbortableFetch.cs b/src/Butil/Bit.Butil/Publics/Fetch/AbortableFetch.cs index c863642c8df..90bd13dc45a 100644 --- a/src/Butil/Bit.Butil/Publics/Fetch/AbortableFetch.cs +++ b/src/Butil/Bit.Butil/Publics/Fetch/AbortableFetch.cs @@ -26,6 +26,7 @@ public ValueTask Abort() return _js.InvokeVoid("BitButil.fetch.abort", _id); } + /// Aborts the request if it is still in flight. Does nothing once it has completed or already been aborted. public async ValueTask DisposeAsync() { if (_completed) return; diff --git a/src/Butil/Bit.Butil/Publics/Fetch/FetchRequest.cs b/src/Butil/Bit.Butil/Publics/Fetch/FetchRequest.cs index 7cda43a7763..017710a6c5d 100644 --- a/src/Butil/Bit.Butil/Publics/Fetch/FetchRequest.cs +++ b/src/Butil/Bit.Butil/Publics/Fetch/FetchRequest.cs @@ -9,6 +9,7 @@ namespace Bit.Butil; ///
public class FetchRequest { + /// The URL to request. A relative URL resolves against the current document. public string Url { get; set; } = string.Empty; /// HTTP verb. Defaults to GET. diff --git a/src/Butil/Bit.Butil/Publics/Fetch/FetchResponse.cs b/src/Butil/Bit.Butil/Publics/Fetch/FetchResponse.cs index 336be4765cd..f21df148d0b 100644 --- a/src/Butil/Bit.Butil/Publics/Fetch/FetchResponse.cs +++ b/src/Butil/Bit.Butil/Publics/Fetch/FetchResponse.cs @@ -11,6 +11,7 @@ public class FetchResponse /// HTTP status (or 0 when the request was aborted/failed before headers). public int Status { get; set; } + /// The status text that went with . Often empty over HTTP/2, which does not carry one. public string StatusText { get; set; } = string.Empty; /// Final URL after redirects. diff --git a/src/Butil/Bit.Butil/Publics/FileSystem/FileSystemFileInfo.cs b/src/Butil/Bit.Butil/Publics/FileSystem/FileSystemFileInfo.cs index b7c33bc7d45..6b685cf5c30 100644 --- a/src/Butil/Bit.Butil/Publics/FileSystem/FileSystemFileInfo.cs +++ b/src/Butil/Bit.Butil/Publics/FileSystem/FileSystemFileInfo.cs @@ -7,6 +7,7 @@ namespace Bit.Butil; ///
public class FileSystemFileInfo { + /// The file name, without any path: the browser never reveals where on disk it came from. public string Name { get; set; } = string.Empty; /// Size in bytes. diff --git a/src/Butil/Bit.Butil/Publics/Gamepad/GamepadState.cs b/src/Butil/Bit.Butil/Publics/Gamepad/GamepadState.cs index 2481e684f4a..a20d8365ccb 100644 --- a/src/Butil/Bit.Butil/Publics/Gamepad/GamepadState.cs +++ b/src/Butil/Bit.Butil/Publics/Gamepad/GamepadState.cs @@ -16,6 +16,7 @@ public class GamepadState /// A device string chosen by the browser, e.g. the vendor and product name. public string Id { get; set; } = string.Empty; + /// True while the controller is attached. A snapshot taken after it was unplugged reads false. public bool Connected { get; set; } /// diff --git a/src/Butil/Bit.Butil/Publics/Geolocation.cs b/src/Butil/Bit.Butil/Publics/Geolocation.cs index dbf3e2d4add..e6fe2149d27 100644 --- a/src/Butil/Bit.Butil/Publics/Geolocation.cs +++ b/src/Butil/Bit.Butil/Publics/Geolocation.cs @@ -132,6 +132,7 @@ public async ValueTask ClearAllWatches() } } + /// Clears every position watch started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try { await ClearAllWatches(); } diff --git a/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationError.cs b/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationErrorCode.cs similarity index 64% rename from src/Butil/Bit.Butil/Publics/Geolocation/GeolocationError.cs rename to src/Butil/Bit.Butil/Publics/Geolocation/GeolocationErrorCode.cs index 033725d908f..00223310a65 100644 --- a/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationError.cs +++ b/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationErrorCode.cs @@ -19,16 +19,3 @@ public enum GeolocationErrorCode /// The error is not one of the above. Unknown = 0 } - -/// -/// Wraps a GeolocationPositionError raised by the browser. -/// -public class GeolocationException : Exception -{ - public GeolocationErrorCode Code { get; } - - public GeolocationException(GeolocationErrorCode code, string message) : base(message) - { - Code = code; - } -} diff --git a/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationException.cs b/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationException.cs new file mode 100644 index 00000000000..004d585348e --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Geolocation/GeolocationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Bit.Butil; + +/// +/// Wraps a GeolocationPositionError raised by the browser. +/// +public class GeolocationException : Exception +{ + /// Which of the four failures this was. + public GeolocationErrorCode Code { get; } + + /// Creates an exception for one GeolocationPositionError. + public GeolocationException(GeolocationErrorCode code, string message) : base(message) + { + Code = code; + } +} diff --git a/src/Butil/Bit.Butil/Publics/History.cs b/src/Butil/Bit.Butil/Publics/History.cs index 0f037aeeea1..88784cc6907 100644 --- a/src/Butil/Bit.Butil/Publics/History.cs +++ b/src/Butil/Bit.Butil/Publics/History.cs @@ -219,6 +219,7 @@ private async ValueTask RemovePopState(Guid[] ids) await RemoveFromJs(ids); } + /// Removes every popstate handler registered through this instance. public async ValueTask RemoveAllPopStates() { if (_handlers.Count == 0) return; @@ -235,6 +236,7 @@ private async ValueTask RemoveFromJs(Guid[] ids) await js.InvokeVoid("BitButil.history.removePopState", ids); } + /// Removes every popstate handler this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -242,6 +244,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/History/ScrollRestoration.cs b/src/Butil/Bit.Butil/Publics/History/ScrollRestoration.cs index 8baf0c0dc94..8c75d029ce9 100644 --- a/src/Butil/Bit.Butil/Publics/History/ScrollRestoration.cs +++ b/src/Butil/Bit.Butil/Publics/History/ScrollRestoration.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Whether the browser restores the scroll position when the user returns to a history entry. +///
+/// History.scrollRestoration +///
public enum ScrollRestoration { /// diff --git a/src/Butil/Bit.Butil/Publics/IdleDetector.cs b/src/Butil/Bit.Butil/Publics/IdleDetector.cs index 58169335711..38ff9159212 100644 --- a/src/Butil/Bit.Butil/Publics/IdleDetector.cs +++ b/src/Butil/Bit.Butil/Publics/IdleDetector.cs @@ -84,6 +84,7 @@ public async Task Start(int threshold, Action hand }); } + /// Stops every detector started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDatabaseInfo.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDatabaseInfo.cs new file mode 100644 index 00000000000..6fa65dccc78 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDatabaseInfo.cs @@ -0,0 +1,17 @@ +namespace Bit.Butil; + +/// Name and version of a database, as reported by the browser. +public sealed class IndexedDbDatabaseInfo +{ + /// The database name. + public string Name { get; set; } = string.Empty; + + /// The version currently on disk. + public int Version { get; set; } + + /// + /// Object stores in the database. Populated by ; empty for + /// entries from , which can't open the databases it lists. + /// + public string[] StoreNames { get; set; } = []; +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDurability.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDurability.cs new file mode 100644 index 00000000000..df10d74515e --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbDurability.cs @@ -0,0 +1,17 @@ +namespace Bit.Butil; + +/// +/// How hard the browser should try to flush a transaction to disk before reporting it complete. +/// See IDBTransaction.durability. +/// +public enum IndexedDbDurability +{ + /// Let the browser decide (its own default, usually equivalent to ). + Default, + + /// Complete as soon as the OS has the data; a crash may lose it. Faster. + Relaxed, + + /// Don't complete until the data is genuinely flushed to storage. Slower, survives a crash. + Strict +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbHandle.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbHandle.cs index 26f827f16ad..15ab0890d6f 100644 --- a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbHandle.cs +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbHandle.cs @@ -392,6 +392,7 @@ private static string ToName(IndexedDbTransactionMode mode) _ => null }; + /// Closes the database connection and releases the interop reference behind its callbacks. Calling it again does nothing. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexInfo.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexInfo.cs new file mode 100644 index 00000000000..208276ba459 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexInfo.cs @@ -0,0 +1,17 @@ +namespace Bit.Butil; + +/// Schema of a single index. +public sealed class IndexedDbIndexInfo +{ + /// The index's name. + public string Name { get; set; } = string.Empty; + + /// The index's keypath, flattened to an array (several entries for a compound index). + public string[] KeyPath { get; set; } = []; + + /// True when the index rejects duplicate keys. + public bool Unique { get; set; } + + /// True when an array-valued keypath produces one index entry per element. + public bool MultiEntry { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexSchema.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexSchema.cs new file mode 100644 index 00000000000..bd391ae9280 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbIndexSchema.cs @@ -0,0 +1,26 @@ +namespace Bit.Butil; + +/// Index schema inside an . +public class IndexedDbIndexSchema +{ + /// The index's name, unique within its store. + public string Name { get; set; } = string.Empty; + + /// The keypath the index is built over. Ignored when is set. + public string KeyPath { get; set; } = string.Empty; + + /// + /// Compound index - two or more keypaths making up one index key. Takes precedence over + /// when non-empty. + /// + public string[]? KeyPaths { get; set; } + + /// True to reject a second record carrying a key this index has already seen. + public bool Unique { get; set; } + + /// True to index each element of an array-valued keypath separately, rather than the array as one key. + public bool MultiEntry { get; set; } + + /// True to delete this index during the upgrade. Every other member is ignored when set. + public bool Drop { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbInfo.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbInfo.cs deleted file mode 100644 index 02c582bddf0..00000000000 --- a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbInfo.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Bit.Butil; - -/// Name and version of a database, as reported by the browser. -public sealed class IndexedDbDatabaseInfo -{ - public string Name { get; set; } = string.Empty; - - public int Version { get; set; } - - /// - /// Object stores in the database. Populated by ; empty for - /// entries from , which can't open the databases it lists. - /// - public string[] StoreNames { get; set; } = []; -} - -/// Schema of a single object store. -public sealed class IndexedDbStoreInfo -{ - public string Name { get; set; } = string.Empty; - - /// - /// The store's keypath, flattened to an array: empty for out-of-line keys, one entry for a - /// simple keypath, several for a compound one. - /// - public string[] KeyPath { get; set; } = []; - - /// True when the store generates its own keys. - public bool AutoIncrement { get; set; } - - /// Indexes defined on the store. - public string[] IndexNames { get; set; } = []; -} - -/// Schema of a single index. -public sealed class IndexedDbIndexInfo -{ - public string Name { get; set; } = string.Empty; - - /// The index's keypath, flattened to an array (several entries for a compound index). - public string[] KeyPath { get; set; } = []; - - /// True when the index rejects duplicate keys. - public bool Unique { get; set; } - - /// True when an array-valued keypath produces one index entry per element. - public bool MultiEntry { get; set; } -} - -/// What learned while opening the database. -internal sealed class IndexedDbOpenInfo -{ - public string Name { get; set; } = string.Empty; - public int Version { get; set; } - public string[] StoreNames { get; set; } = []; - public int OldVersion { get; set; } - public int NewVersion { get; set; } - public bool Upgraded { get; set; } -} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbKeyRecord.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbKeyRecord.cs new file mode 100644 index 00000000000..72a41f10614 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbKeyRecord.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace Bit.Butil; + +/// +/// One record from a key-only cursor walk - the keys without the cost of deserializing the value. +/// +public sealed class IndexedDbKeyRecord +{ + /// Store key, or index key when walking an index. + public JsonElement Key { get; set; } + + /// The record's primary key in the object store. + public JsonElement PrimaryKey { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbOpenInfo.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbOpenInfo.cs new file mode 100644 index 00000000000..8d8adb37976 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbOpenInfo.cs @@ -0,0 +1,12 @@ +namespace Bit.Butil; + +/// What learned while opening the database. +internal sealed class IndexedDbOpenInfo +{ + public string Name { get; set; } = string.Empty; + public int Version { get; set; } + public string[] StoreNames { get; set; } = []; + public int OldVersion { get; set; } + public int NewVersion { get; set; } + public bool Upgraded { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbRecord.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbRecord.cs index 97915a3d6bf..6579c2e2b6b 100644 --- a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbRecord.cs +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbRecord.cs @@ -20,15 +20,3 @@ public sealed class IndexedDbRecord /// The stored value. public T? Value { get; set; } } - -/// -/// One record from a key-only cursor walk - the keys without the cost of deserializing the value. -/// -public sealed class IndexedDbKeyRecord -{ - /// Store key, or index key when walking an index. - public JsonElement Key { get; set; } - - /// The record's primary key in the object store. - public JsonElement PrimaryKey { get; set; } -} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreInfo.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreInfo.cs new file mode 100644 index 00000000000..8accf058d71 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreInfo.cs @@ -0,0 +1,20 @@ +namespace Bit.Butil; + +/// Schema of a single object store. +public sealed class IndexedDbStoreInfo +{ + /// The object store's name. + public string Name { get; set; } = string.Empty; + + /// + /// The store's keypath, flattened to an array: empty for out-of-line keys, one entry for a + /// simple keypath, several for a compound one. + /// + public string[] KeyPath { get; set; } = []; + + /// True when the store generates its own keys. + public bool AutoIncrement { get; set; } + + /// Indexes defined on the store. + public string[] IndexNames { get; set; } = []; +} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreSchema.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreSchema.cs index f088ec28aec..f318a7f5d12 100644 --- a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreSchema.cs +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbStoreSchema.cs @@ -11,6 +11,7 @@ namespace Bit.Butil; /// public class IndexedDbStoreSchema { + /// The object store's name. It is the key the schema is matched on, so renaming one creates a second store rather than renaming the first. public string Name { get; set; } = string.Empty; /// The keypath to use as the store's primary key. Null means out-of-line keys. @@ -34,24 +35,3 @@ public class IndexedDbStoreSchema /// Indexes to create alongside the store. public IndexedDbIndexSchema[] Indexes { get; set; } = []; } - -/// Index schema inside an . -public class IndexedDbIndexSchema -{ - public string Name { get; set; } = string.Empty; - - public string KeyPath { get; set; } = string.Empty; - - /// - /// Compound index - two or more keypaths making up one index key. Takes precedence over - /// when non-empty. - /// - public string[]? KeyPaths { get; set; } - - public bool Unique { get; set; } - - public bool MultiEntry { get; set; } - - /// True to delete this index during the upgrade. Every other member is ignored when set. - public bool Drop { get; set; } -} diff --git a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbTransactionMode.cs b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbTransactionMode.cs index c2834476c32..7703c2c9f51 100644 --- a/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbTransactionMode.cs +++ b/src/Butil/Bit.Butil/Publics/IndexedDb/IndexedDbTransactionMode.cs @@ -12,19 +12,3 @@ public enum IndexedDbTransactionMode /// Reads only; these run concurrently with each other. ReadOnly } - -/// -/// How hard the browser should try to flush a transaction to disk before reporting it complete. -/// See IDBTransaction.durability. -/// -public enum IndexedDbDurability -{ - /// Let the browser decide (its own default, usually equivalent to ). - Default, - - /// Complete as soon as the OS has the data; a crash may lose it. Faster. - Relaxed, - - /// Don't complete until the data is genuinely flushed to storage. Slower, survives a crash. - Strict -} diff --git a/src/Butil/Bit.Butil/Publics/IntersectionObserver/IntersectionObserverEntry.cs b/src/Butil/Bit.Butil/Publics/IntersectionObserver/IntersectionObserverEntry.cs index c1955b09d0c..4a005f5857a 100644 --- a/src/Butil/Bit.Butil/Publics/IntersectionObserver/IntersectionObserverEntry.cs +++ b/src/Butil/Bit.Butil/Publics/IntersectionObserver/IntersectionObserverEntry.cs @@ -14,7 +14,12 @@ public class IntersectionObserverEntry /// Time at which the intersection was detected (DOMHighResTimeStamp, in ms). public double Time { get; set; } + /// The target's own bounding box at the moment of the callback. public Rect? BoundingClientRect { get; set; } + + /// The part of the target that is inside the root. All zeroes when nothing intersects. public Rect? IntersectionRect { get; set; } + + /// The root's bounds, after any root margin. Null when the root is the implicit viewport in a cross-origin frame. public Rect? RootBounds { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Keyboard.cs b/src/Butil/Bit.Butil/Publics/Keyboard.cs index 57ff6e4eb96..498e9e7811a 100644 --- a/src/Butil/Bit.Butil/Publics/Keyboard.cs +++ b/src/Butil/Bit.Butil/Publics/Keyboard.cs @@ -40,6 +40,19 @@ public void InvokeKeyboard(Guid id) if (_handlers.TryGetValue(id, out var handler)) handler.Invoke(); } + /// + /// Registers an app-wide shortcut and returns the id that removes it again. The match is on + /// KeyboardEvent.code - a physical key position, so the shortcut lands under the same + /// finger on every keyboard layout. + ///
+ /// KeyboardEvent.code + ///
+ /// The code to match, e.g. . + /// Called when the combination is pressed. + /// The modifiers that must be down. Matched exactly, so extra modifiers do not fire it. + /// Suppress the browser's own action for the combination. + /// Stop the keydown travelling any further. + /// Keep firing while the key is held, instead of once per press. public async Task Add(string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false) { var listenerId = Guid.NewGuid(); @@ -123,6 +136,7 @@ public async ValueTask Remove(Action handler) return ids; } + /// Removes one shortcut, by the id returned. public async ValueTask Remove(Guid id) { await Remove([id]); @@ -140,6 +154,7 @@ private async ValueTask Remove(Guid[] ids) await RemoveFromJs(ids); } + /// Removes every shortcut registered through this instance. public async ValueTask RemoveAll() { if (_handlers.Count == 0) return; @@ -156,6 +171,7 @@ private async ValueTask RemoveFromJs(Guid[] ids) await js.InvokeVoid("BitButil.keyboard.remove", ids); } + /// Removes every shortcut this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -163,6 +179,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/Locks/WebLockInfo.cs b/src/Butil/Bit.Butil/Publics/Locks/WebLockInfo.cs new file mode 100644 index 00000000000..0fe2ef528b9 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Locks/WebLockInfo.cs @@ -0,0 +1,14 @@ +namespace Bit.Butil; + +/// One entry inside a . +public class WebLockInfo +{ + /// The lock's name - the string it was requested under. + public string Name { get; set; } = string.Empty; + + /// "exclusive" or "shared". + public string Mode { get; set; } = "exclusive"; + + /// An opaque id for the tab or worker holding or awaiting the lock. + public string ClientId { get; set; } = string.Empty; +} diff --git a/src/Butil/Bit.Butil/Publics/Locks/WebLockSnapshot.cs b/src/Butil/Bit.Butil/Publics/Locks/WebLockSnapshot.cs index dd878091992..70f013add63 100644 --- a/src/Butil/Bit.Butil/Publics/Locks/WebLockSnapshot.cs +++ b/src/Butil/Bit.Butil/Publics/Locks/WebLockSnapshot.cs @@ -5,14 +5,9 @@ namespace Bit.Butil; ///
public class WebLockSnapshot { + /// The locks currently granted, across every tab of this origin. public WebLockInfo[] Held { get; set; } = []; - public WebLockInfo[] Pending { get; set; } = []; -} -/// One entry inside a . -public class WebLockInfo -{ - public string Name { get; set; } = string.Empty; - public string Mode { get; set; } = "exclusive"; - public string ClientId { get; set; } = string.Empty; + /// The requests still waiting for a lock to be released. + public WebLockInfo[] Pending { get; set; } = []; } diff --git a/src/Butil/Bit.Butil/Publics/MediaDevices/DisplayMediaSettings.cs b/src/Butil/Bit.Butil/Publics/MediaDevices/DisplayMediaSettings.cs index 4684cfb04ed..2fac3ad18a3 100644 --- a/src/Butil/Bit.Butil/Publics/MediaDevices/DisplayMediaSettings.cs +++ b/src/Butil/Bit.Butil/Publics/MediaDevices/DisplayMediaSettings.cs @@ -12,8 +12,10 @@ public class DisplayMediaSettings /// What the user picked: "monitor", "window" or "browser". Empty when the runtime doesn't report it. public string DisplaySurface { get; set; } = string.Empty; + /// The negotiated frame width in pixels. 0 when unreported. public int Width { get; set; } + /// The negotiated frame height in pixels. 0 when unreported. public int Height { get; set; } /// The negotiated frame rate. May differ from a requested one, and is 0 when unreported. diff --git a/src/Butil/Bit.Butil/Publics/MediaDevices/MediaDeviceInfo.cs b/src/Butil/Bit.Butil/Publics/MediaDevices/MediaDeviceInfo.cs index 2c0756a6686..5ca71696ee8 100644 --- a/src/Butil/Bit.Butil/Publics/MediaDevices/MediaDeviceInfo.cs +++ b/src/Butil/Bit.Butil/Publics/MediaDevices/MediaDeviceInfo.cs @@ -5,11 +5,15 @@ namespace Bit.Butil; ///
public class MediaDeviceInfo { + /// The device's id, stable per origin. Empty until the user has granted permission once. public string DeviceId { get; set; } = string.Empty; /// One of "audioinput", "audiooutput", "videoinput". public string Kind { get; set; } = string.Empty; + /// The human-readable device name. Empty until permission has been granted, since it identifies the user's hardware. public string Label { get; set; } = string.Empty; + + /// Shared by every device on the same physical unit, which is how a headset's microphone and speaker are paired up. public string GroupId { get; set; } = string.Empty; } diff --git a/src/Butil/Bit.Butil/Publics/MediaDevices/MediaStreamHandle.cs b/src/Butil/Bit.Butil/Publics/MediaDevices/MediaStreamHandle.cs index 887c9b722b4..d83067b760b 100644 --- a/src/Butil/Bit.Butil/Publics/MediaDevices/MediaStreamHandle.cs +++ b/src/Butil/Bit.Butil/Publics/MediaDevices/MediaStreamHandle.cs @@ -28,6 +28,7 @@ public ValueTask AttachTo(ElementReference videoOrAudioElement) public ValueTask SetEnabled(bool enabled) => _js.InvokeVoid("BitButil.mediaDevices.setEnabled", _id, enabled); + /// Stops every track in the stream - what actually turns the camera light off. Calling it again does nothing. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Butil/Bit.Butil/Publics/MediaSession/MediaMetadata.cs b/src/Butil/Bit.Butil/Publics/MediaSession/MediaMetadata.cs index 04defc56787..fe4c6e96207 100644 --- a/src/Butil/Bit.Butil/Publics/MediaSession/MediaMetadata.cs +++ b/src/Butil/Bit.Butil/Publics/MediaSession/MediaMetadata.cs @@ -6,10 +6,13 @@ namespace Bit.Butil; ///
public class MediaMetadata { + /// The track title. public string Title { get; set; } = string.Empty; + /// The performer. public string Artist { get; set; } = string.Empty; + /// The album or collection the track belongs to. public string Album { get; set; } = string.Empty; /// diff --git a/src/Butil/Bit.Butil/Publics/MediaSession/MediaSessionAction.cs b/src/Butil/Bit.Butil/Publics/MediaSession/MediaSessionAction.cs index 1cc7f80bf83..6b68e606bca 100644 --- a/src/Butil/Bit.Butil/Publics/MediaSession/MediaSessionAction.cs +++ b/src/Butil/Bit.Butil/Publics/MediaSession/MediaSessionAction.cs @@ -11,8 +11,13 @@ namespace Bit.Butil; /// public enum MediaSessionAction { + /// Resume playback. Play, + + /// Pause playback, keeping the position. Pause, + + /// Stop playback and give up the session. Stop, /// Jump back by the details' seek offset, or a sensible default when none is given. @@ -24,7 +29,10 @@ public enum MediaSessionAction /// Jump to an absolute position - the details carry the target time. SeekTo, + /// Go to the previous track. PreviousTrack, + + /// Go to the next track. NextTrack, /// Skip an advertisement. diff --git a/src/Butil/Bit.Butil/Publics/Navigation/NavigationEntry.cs b/src/Butil/Bit.Butil/Publics/Navigation/NavigationEntry.cs index 7f4ab55635b..1f5e6d5643d 100644 --- a/src/Butil/Bit.Butil/Publics/Navigation/NavigationEntry.cs +++ b/src/Butil/Bit.Butil/Publics/Navigation/NavigationEntry.cs @@ -28,31 +28,3 @@ public record NavigationEntry( string Url, int Index, bool SameDocument); - -/// How a call should affect the history list. -public enum NavigationHistoryBehavior -{ - /// Let the browser decide: push, unless the URL is unchanged, in which case replace. - Auto, - - /// Always add a new entry. - Push, - - /// Always overwrite the current entry, adding nothing to the back stack. - Replace, -} - -/// What happened, delivered to a subscription. -/// -/// For , the entry that was current before the -/// change; null for the other events and for a change with no previous entry. -/// -/// -/// push, replace, reload or traverse - null when the change was an -/// in-place state update rather than a navigation. -/// -/// The failure message, on only. -public record NavigationEventInfo( - NavigationEntry? From, - string? NavigationType, - string? Message); diff --git a/src/Butil/Bit.Butil/Publics/Navigation/NavigationEventInfo.cs b/src/Butil/Bit.Butil/Publics/Navigation/NavigationEventInfo.cs new file mode 100644 index 00000000000..40aa3810421 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Navigation/NavigationEventInfo.cs @@ -0,0 +1,16 @@ +namespace Bit.Butil; + +/// What happened, delivered to a subscription. +/// +/// For , the entry that was current before the +/// change; null for the other events and for a change with no previous entry. +/// +/// +/// push, replace, reload or traverse - null when the change was an +/// in-place state update rather than a navigation. +/// +/// The failure message, on only. +public record NavigationEventInfo( + NavigationEntry? From, + string? NavigationType, + string? Message); diff --git a/src/Butil/Bit.Butil/Publics/Navigation/NavigationHistoryBehavior.cs b/src/Butil/Bit.Butil/Publics/Navigation/NavigationHistoryBehavior.cs new file mode 100644 index 00000000000..4e53f3ad8ec --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Navigation/NavigationHistoryBehavior.cs @@ -0,0 +1,14 @@ +namespace Bit.Butil; + +/// How a call should affect the history list. +public enum NavigationHistoryBehavior +{ + /// Let the browser decide: push, unless the URL is unchanged, in which case replace. + Auto, + + /// Always add a new entry. + Push, + + /// Always overwrite the current entry, adding nothing to the back stack. + Replace, +} diff --git a/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs b/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs index 6acb79f9666..020918ed236 100644 --- a/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs +++ b/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs @@ -1,9 +1,27 @@ namespace Bit.Butil; +/// +/// What the native share sheet is handed. At least one of the three has to be set, and a URL that +/// is not a valid absolute URL makes the whole call fail rather than being dropped. +///
+/// Navigator.share() +///
public class ShareData { // files?: File[]; + + /// The body text to share. public string? Text { get; set; } + + /// + /// The title to share. Lower-cased because it is the JSON member name the browser reads; + /// renaming it would break callers. + /// public string? title { get; set; } + + /// + /// The URL to share, absolute. Lower-cased because it is the JSON member name the browser + /// reads; renaming it would break callers. + /// public string? url { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Nfc.cs b/src/Butil/Bit.Butil/Publics/Nfc.cs index 4b88d470de3..3cd1bcd44a0 100644 --- a/src/Butil/Bit.Butil/Publics/Nfc.cs +++ b/src/Butil/Bit.Butil/Publics/Nfc.cs @@ -91,6 +91,7 @@ public ValueTask WriteText(string text, string? lang = null, string? id = public ValueTask WriteUrl(string url, string? id = null) => js.Invoke("BitButil.nfc.writeUrl", url, id); + /// Stops every scan started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/Nfc/NdefMessage.cs b/src/Butil/Bit.Butil/Publics/Nfc/NdefMessage.cs new file mode 100644 index 00000000000..28077b8b203 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/Nfc/NdefMessage.cs @@ -0,0 +1,11 @@ +namespace Bit.Butil; + +/// One scanned NDEF message. +public class NdefMessage +{ + /// The tag's serial number, when the platform exposes one. + public string SerialNumber { get; set; } = string.Empty; + + /// The records the message carries. + public NdefRecord[] Records { get; set; } = []; +} diff --git a/src/Butil/Bit.Butil/Publics/Nfc/NdefRecord.cs b/src/Butil/Bit.Butil/Publics/Nfc/NdefRecord.cs index 58c09bd7ae4..cb333979c0c 100644 --- a/src/Butil/Bit.Butil/Publics/Nfc/NdefRecord.cs +++ b/src/Butil/Bit.Butil/Publics/Nfc/NdefRecord.cs @@ -26,10 +26,3 @@ public class NdefRecord /// Raw bytes when applicable (mime/etc.). public byte[]? Data { get; set; } } - -/// One scanned NDEF message. -public class NdefMessage -{ - public string SerialNumber { get; set; } = string.Empty; - public NdefRecord[] Records { get; set; } = []; -} diff --git a/src/Butil/Bit.Butil/Publics/Notification.cs b/src/Butil/Bit.Butil/Publics/Notification.cs index c75ff054c17..425fd8b029d 100644 --- a/src/Butil/Bit.Butil/Publics/Notification.cs +++ b/src/Butil/Bit.Butil/Publics/Notification.cs @@ -157,6 +157,7 @@ public async ValueTask ShowTracked(string title, return new NotificationHandle(this, js, id); } + /// Detaches every notification still tracked on the JavaScript side - so a later click or close cannot call into a disposed reference - and releases that reference. public async ValueTask DisposeAsync() { // Detach any still-tracked notifications on the JS side before releasing the ref. Without diff --git a/src/Butil/Bit.Butil/Publics/Notification/NotificationDirection.cs b/src/Butil/Bit.Butil/Publics/Notification/NotificationDirection.cs index caf5d426de4..b04d7a65c0a 100644 --- a/src/Butil/Bit.Butil/Publics/Notification/NotificationDirection.cs +++ b/src/Butil/Bit.Butil/Publics/Notification/NotificationDirection.cs @@ -1,9 +1,18 @@ namespace Bit.Butil; +/// +/// Which way a notification's title and body are laid out. +///
+/// Notification.dir +///
public enum NotificationDirection { + /// Follow the browser's own locale. The default. Auto, + + /// Left to right. Ltr, + + /// Right to left. Rtl } - diff --git a/src/Butil/Bit.Butil/Publics/Notification/NotificationHandle.cs b/src/Butil/Bit.Butil/Publics/Notification/NotificationHandle.cs index 4636642471d..c9c9c345b76 100644 --- a/src/Butil/Bit.Butil/Publics/Notification/NotificationHandle.cs +++ b/src/Butil/Bit.Butil/Publics/Notification/NotificationHandle.cs @@ -22,6 +22,7 @@ public sealed class NotificationHandle : IAsyncDisposable /// Closes the notification programmatically. public ValueTask Close() => _js.InvokeVoid("BitButil.notification.close", _id); + /// Closes the notification if it is still on screen, and stops its callbacks. Calling it again does nothing. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Butil/Bit.Butil/Publics/Notification/NotificationPermission.cs b/src/Butil/Bit.Butil/Publics/Notification/NotificationPermission.cs index 76a8d29fffa..9d85a238e90 100644 --- a/src/Butil/Bit.Butil/Publics/Notification/NotificationPermission.cs +++ b/src/Butil/Bit.Butil/Publics/Notification/NotificationPermission.cs @@ -1,5 +1,10 @@ namespace Bit.Butil; +/// +/// Whether this origin may show system notifications. +///
+/// Notification.permission +///
public enum NotificationPermission { /// diff --git a/src/Butil/Bit.Butil/Publics/ObjectUrls.cs b/src/Butil/Bit.Butil/Publics/ObjectUrls.cs index a1f064bbfc8..ed6b52f7552 100644 --- a/src/Butil/Bit.Butil/Publics/ObjectUrls.cs +++ b/src/Butil/Bit.Butil/Publics/ObjectUrls.cs @@ -29,6 +29,7 @@ public async ValueTask Create(byte[] data, string mimeType = "applicatio /// Revokes a previously created object URL. public ValueTask Revoke(string objectUrl) => js.InvokeVoid("BitButil.objectUrls.revoke", objectUrl); + /// Revokes every object URL created through this instance, freeing the blobs behind them. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/Performance.cs b/src/Butil/Bit.Butil/Publics/Performance.cs index 79ca1bc9bd5..c7764e05923 100644 --- a/src/Butil/Bit.Butil/Publics/Performance.cs +++ b/src/Butil/Bit.Butil/Publics/Performance.cs @@ -128,6 +128,7 @@ public async Task SubscribeObserver(string[] entryTypes, }); } + /// Disconnects every PerformanceObserver started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/Performance/PerformanceMemory.cs b/src/Butil/Bit.Butil/Publics/Performance/PerformanceMemory.cs index 1c2a5fee2ae..8c90f488478 100644 --- a/src/Butil/Bit.Butil/Publics/Performance/PerformanceMemory.cs +++ b/src/Butil/Bit.Butil/Publics/Performance/PerformanceMemory.cs @@ -6,7 +6,12 @@ namespace Bit.Butil; /// public class PerformanceMemory { + /// The heap size, in bytes, the engine will not grow past. public long? JsHeapSizeLimit { get; set; } + + /// The currently allocated heap, in bytes. public long? TotalJsHeapSize { get; set; } + + /// The part of the allocated heap actually in use, in bytes. public long? UsedJsHeapSize { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Reporting.cs b/src/Butil/Bit.Butil/Publics/Reporting.cs index 67b5d0abf52..4ce72dfa3f1 100644 --- a/src/Butil/Bit.Butil/Publics/Reporting.cs +++ b/src/Butil/Bit.Butil/Publics/Reporting.cs @@ -67,6 +67,7 @@ public async Task Subscribe(Action handler, }); } + /// Disconnects every ReportingObserver started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverBox.cs b/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverBox.cs index 9581efc765c..f15434b27a5 100644 --- a/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverBox.cs +++ b/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverBox.cs @@ -6,7 +6,12 @@ namespace Bit.Butil; ///
public enum ResizeObserverBox { + /// The content box - the element's size excluding padding and border. The default. ContentBox, + + /// The border box - the content box plus padding and border. BorderBox, + + /// The content box in device pixels, which is what a canvas backing store should be sized to. DevicePixelContentBox } diff --git a/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverEntry.cs b/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverEntry.cs index 9b0fbdb27c7..fc3548fc448 100644 --- a/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverEntry.cs +++ b/src/Butil/Bit.Butil/Publics/ResizeObserver/ResizeObserverEntry.cs @@ -6,9 +6,18 @@ namespace Bit.Butil; ///
public class ResizeObserverEntry { + /// The target's content box at the moment of the callback. public Rect? ContentRect { get; set; } + + /// The content box's inline size - its width in a horizontal writing mode. public double InlineSize { get; set; } + + /// The content box's block size - its height in a horizontal writing mode. public double BlockSize { get; set; } + + /// The inline size in device pixels, which is what a canvas backing store should be sized to. public double DevicePixelInlineSize { get; set; } + + /// The block size in device pixels, which is what a canvas backing store should be sized to. public double DevicePixelBlockSize { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Screen.cs b/src/Butil/Bit.Butil/Publics/Screen.cs index b4ddcfa37b9..658f4af2e29 100644 --- a/src/Butil/Bit.Butil/Publics/Screen.cs +++ b/src/Butil/Bit.Butil/Publics/Screen.cs @@ -233,6 +233,7 @@ private async ValueTask RemoveChange(Guid[] ids) await RemoveFromJs(ids); } + /// Removes every screen-change handler registered through this instance. public async ValueTask RemoveAllChanges() { if (_handlers.Count == 0) return; @@ -249,6 +250,7 @@ private async ValueTask RemoveFromJs(Guid[] ids) await js.InvokeVoid("BitButil.screen.removeChange", ids); } + /// Removes every screen-change handler this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -256,6 +258,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/ScreenOrientation.cs b/src/Butil/Bit.Butil/Publics/ScreenOrientation.cs index 797a43d5e40..498796b09aa 100644 --- a/src/Butil/Bit.Butil/Publics/ScreenOrientation.cs +++ b/src/Butil/Bit.Butil/Publics/ScreenOrientation.cs @@ -199,6 +199,7 @@ private async ValueTask RemoveChange(Guid[] ids) await RemoveFromJs(ids); } + /// Removes every orientation-change handler registered through this instance. public async ValueTask RemoveAllChanges() { if (_handlers.Count == 0) return; @@ -215,6 +216,7 @@ private async ValueTask RemoveFromJs(Guid[] ids) await js.InvokeVoid("BitButil.screenOrientation.removeChange", ids); } + /// Removes every orientation-change handler this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -222,6 +224,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationLockType.cs b/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationLockType.cs index f4576818705..ada82088f93 100644 --- a/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationLockType.cs +++ b/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationLockType.cs @@ -1,13 +1,34 @@ namespace Bit.Butil; +/// +/// What a screen-orientation lock asks for. Locking generally requires the document to be +/// fullscreen, and mobile browsers reject it outright on a page that is not. +///
+/// ScreenOrientation.lock() +///
public enum OrientationLockType { + /// Any orientation the device supports - effectively releases the lock. Any, + + /// The device's own natural orientation. Natural, + + /// Either landscape orientation, whichever way the device is held. Landscape, + + /// Either portrait orientation, whichever way the device is held. Portrait, + + /// Portrait, natural way round only. PortraitPrimary, + + /// Portrait, upside down only. PortraitSecondary, + + /// Landscape, natural way round only. LandscapePrimary, + + /// Landscape, upside down only. LandscapeSecondary } diff --git a/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationState.cs b/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationState.cs index 3948c8be6fa..4634f092d58 100644 --- a/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationState.cs +++ b/src/Butil/Bit.Butil/Publics/ScreenOrientation/OrientationState.cs @@ -1,7 +1,24 @@ namespace Bit.Butil; +/// +/// The screen orientation as one value, so a reader and a change handler both get the angle and the +/// type together rather than in two round trips. +///
+/// ScreenOrientation +///
public class OrientationState { + /// + /// Degrees clockwise from the device's natural orientation - 0, 90, 180 or 270. + ///
+ /// ScreenOrientation.angle + ///
public ushort Angle { get; set; } + + /// + /// The orientation as a named axis and direction. + ///
+ /// ScreenOrientation.type + ///
public ScreenOrientationType Type { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/ScreenOrientation/ScreenOrientationType.cs b/src/Butil/Bit.Butil/Publics/ScreenOrientation/ScreenOrientationType.cs index e85288dfcaf..4d3b8b30a20 100644 --- a/src/Butil/Bit.Butil/Publics/ScreenOrientation/ScreenOrientationType.cs +++ b/src/Butil/Bit.Butil/Publics/ScreenOrientation/ScreenOrientationType.cs @@ -1,9 +1,23 @@ namespace Bit.Butil; +/// +/// The orientation the screen is currently in. "Primary" and "secondary" are the two ways round a +/// given axis can be held, relative to the device's natural orientation - which is portrait on most +/// phones and landscape on most tablets and laptops. +///
+/// ScreenOrientation.type +///
public enum ScreenOrientationType { + /// Portrait, the way round the device calls natural. PortraitPrimary, + + /// Portrait, upside down from . PortraitSecondary, + + /// Landscape, the way round the device calls natural. LandscapePrimary, + + /// Landscape, upside down from . LandscapeSecondary } diff --git a/src/Butil/Bit.Butil/Publics/ServiceWorker.cs b/src/Butil/Bit.Butil/Publics/ServiceWorker.cs index 29e23e947d6..86f501848ab 100644 --- a/src/Butil/Bit.Butil/Publics/ServiceWorker.cs +++ b/src/Butil/Bit.Butil/Publics/ServiceWorker.cs @@ -168,6 +168,7 @@ public async Task SubscribeControllerChange(Action handler) }); } + /// Unsubscribes every message and controller-change handler this instance registered, and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/Speech/SpeechVoice.cs b/src/Butil/Bit.Butil/Publics/Speech/SpeechVoice.cs index a1896391ba8..983e043ce67 100644 --- a/src/Butil/Bit.Butil/Publics/Speech/SpeechVoice.cs +++ b/src/Butil/Bit.Butil/Publics/Speech/SpeechVoice.cs @@ -5,6 +5,7 @@ namespace Bit.Butil; ///
public class SpeechVoice { + /// The voice's name, as the platform reports it. public string Name { get; set; } = string.Empty; /// BCP-47 language tag - e.g. "en-US". diff --git a/src/Butil/Bit.Butil/Publics/SpeechRecognition.cs b/src/Butil/Bit.Butil/Publics/SpeechRecognition.cs index 5913acb26f8..a305ff35b90 100644 --- a/src/Butil/Bit.Butil/Publics/SpeechRecognition.cs +++ b/src/Butil/Bit.Butil/Publics/SpeechRecognition.cs @@ -91,6 +91,7 @@ public ValueTask Stop(Guid id) return js.InvokeVoid("BitButil.speechRecognition.stop", id); } + /// Stops every recognition session started through this instance and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/Storage/ButilStorage.cs b/src/Butil/Bit.Butil/Publics/Storage/ButilStorage.cs index f99b8e089b4..8f40e5e6b6e 100644 --- a/src/Butil/Bit.Butil/Publics/Storage/ButilStorage.cs +++ b/src/Butil/Bit.Butil/Publics/Storage/ButilStorage.cs @@ -174,6 +174,7 @@ public async Task SubscribeChanges(Action handl }); } + /// Unsubscribes every storage-event handler this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/StorageManager/StorageEstimate.cs b/src/Butil/Bit.Butil/Publics/StorageManager/StorageEstimate.cs index b193fd20337..1552a85af02 100644 --- a/src/Butil/Bit.Butil/Publics/StorageManager/StorageEstimate.cs +++ b/src/Butil/Bit.Butil/Publics/StorageManager/StorageEstimate.cs @@ -6,8 +6,10 @@ namespace Bit.Butil; ///
public class StorageEstimate { + /// How many bytes the origin may use. A conservative figure, deliberately fuzzed. public long? Quota { get; set; } + /// How many bytes the origin is using, likewise fuzzed and rounded. public long? Usage { get; set; } /// @@ -17,13 +19,3 @@ public class StorageEstimate /// public StorageUsageDetail[] UsageDetails { get; set; } = []; } - -/// One entry of . -public class StorageUsageDetail -{ - /// The storage API holding the bytes - e.g. indexedDB, caches, serviceWorkerRegistrations. - public string Api { get; set; } = string.Empty; - - /// Bytes attributed to that API. - public long Bytes { get; set; } -} diff --git a/src/Butil/Bit.Butil/Publics/StorageManager/StorageUsageDetail.cs b/src/Butil/Bit.Butil/Publics/StorageManager/StorageUsageDetail.cs new file mode 100644 index 00000000000..c32896163d0 --- /dev/null +++ b/src/Butil/Bit.Butil/Publics/StorageManager/StorageUsageDetail.cs @@ -0,0 +1,11 @@ +namespace Bit.Butil; + +/// One entry of . +public class StorageUsageDetail +{ + /// The storage API holding the bytes - e.g. indexedDB, caches, serviceWorkerRegistrations. + public string Api { get; set; } = string.Empty; + + /// Bytes attributed to that API. + public long Bytes { get; set; } +} diff --git a/src/Butil/Bit.Butil/Publics/UserAgent/HighEntropyUserAgent.cs b/src/Butil/Bit.Butil/Publics/UserAgent/HighEntropyUserAgent.cs index 0a175401632..9b5dd1af9a4 100644 --- a/src/Butil/Bit.Butil/Publics/UserAgent/HighEntropyUserAgent.cs +++ b/src/Butil/Bit.Butil/Publics/UserAgent/HighEntropyUserAgent.cs @@ -6,14 +6,33 @@ namespace Bit.Butil; ///
public class HighEntropyUserAgent { + /// The CPU architecture, e.g. "x86" or "arm". public string? Architecture { get; set; } + + /// The architecture's word size, e.g. "64". public string? Bitness { get; set; } + + /// The brand list, major versions only - the same values the low-entropy header carries. public UserAgentBrand[]? Brands { get; set; } + + /// The brand list with full version strings. public UserAgentBrand[]? FullVersionList { get; set; } + + /// True on a mobile device. public bool? Mobile { get; set; } + + /// The device model. Only Android reports one; elsewhere it is empty. public string? Model { get; set; } + + /// The platform name, e.g. "Windows", "macOS", "Android". public string? Platform { get; set; } + + /// The platform version. On Windows this is a Chromium-specific number, not the marketing one. public string? PlatformVersion { get; set; } + + /// The browser's full version string. public string? UaFullVersion { get; set; } + + /// True for a 32-bit browser running under WOW64 on 64-bit Windows. public bool? Wow64 { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentBrand.cs b/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentBrand.cs index 814abdfef96..1f4b2fd8ccd 100644 --- a/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentBrand.cs +++ b/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentBrand.cs @@ -3,6 +3,9 @@ namespace Bit.Butil; /// One entry of NavigatorUAData.brands. public class UserAgentBrand { + /// The brand name. The list deliberately carries a nonsense entry, to break naive matching. public string Brand { get; set; } = string.Empty; + + /// The brand's version. public string Version { get; set; } = string.Empty; } diff --git a/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentProperties.cs b/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentProperties.cs index 08ad0621d9d..1478aefa292 100644 --- a/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentProperties.cs +++ b/src/Butil/Bit.Butil/Publics/UserAgent/UserAgentProperties.cs @@ -1,16 +1,48 @@ namespace Bit.Butil; +/// +/// A user-agent string parsed into the pieces people actually want from it. This is best-effort +/// pattern matching over a string browsers deliberately freeze and lie in, so treat every member as +/// a hint: for anything a decision depends on, feature-detect, or ask +/// for the UA Client Hints values instead. +///
+/// Navigator.userAgent +///
public class UserAgentProperties { + /// The browser or web-view name - e.g. "Chrome", "Safari". public string? Name { get; set; } + + /// The browser version, as far as the string reveals it. public string? Version { get; set; } + + /// The release channel when the build advertises one - e.g. "beta", "nightly". public string? Prerelease { get; set; } + + /// The rendering engine - e.g. "Blink", "WebKit", "Gecko". public string? Layout { get; set; } + + /// The device manufacturer, when the string names one - e.g. "Apple". public string? Manufacturer { get; set; } + + /// The device model, when the string names one - e.g. "iPhone". public string? Product { get; set; } + + /// The operating system name - e.g. "Windows", "Android", "iOS". public string? OsName { get; set; } + + /// The operating system version, as far as the string reveals it. public string? OsVersion { get; set; } + + /// The OS architecture in bits - 32 or 64 - or null when the string does not say. public int? OsArchitecture { get; set; } + + /// The parsed pieces joined into one human-readable line. public string? Description { get; set; } + + /// + /// The raw string all of the above was parsed out of - either the one passed to + /// or the browser's own. + /// public string? UserAgentValue { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/VisualViewport.cs b/src/Butil/Bit.Butil/Publics/VisualViewport.cs index 5040a5c9f5d..3396f77b194 100644 --- a/src/Butil/Bit.Butil/Publics/VisualViewport.cs +++ b/src/Butil/Bit.Butil/Publics/VisualViewport.cs @@ -322,6 +322,7 @@ private async ValueTask RemoveScrollFromJs(Guid[] ids) } + /// Removes every resize and scroll handler registered through this instance. public async ValueTask RemoveAllEventHandlers() { if (_handlers.Count == 0) return; @@ -348,6 +349,7 @@ public async ValueTask RemoveAllEventHandlers() await Task.WhenAll(toAwait); } + /// Removes every viewport handler this instance registered and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -355,6 +357,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/WakeLock.cs b/src/Butil/Bit.Butil/Publics/WakeLock.cs index 725cf9abb58..8510db0e9f7 100644 --- a/src/Butil/Bit.Butil/Publics/WakeLock.cs +++ b/src/Butil/Bit.Butil/Publics/WakeLock.cs @@ -63,6 +63,7 @@ public async ValueTask RequestPersistent() return new PersistentLockHandle(js, token); } + /// Releases the wake lock if one is held, letting the screen sleep again. public async ValueTask DisposeAsync() { try diff --git a/src/Butil/Bit.Butil/Publics/WebAudio/AudioPlaybackHandle.cs b/src/Butil/Bit.Butil/Publics/WebAudio/AudioPlaybackHandle.cs index b7ca42ab0e0..7fa4a98bc26 100644 --- a/src/Butil/Bit.Butil/Publics/WebAudio/AudioPlaybackHandle.cs +++ b/src/Butil/Bit.Butil/Publics/WebAudio/AudioPlaybackHandle.cs @@ -25,6 +25,7 @@ public sealed class AudioPlaybackHandle : IAsyncDisposable /// Sets per-source gain in [0, 1]. public ValueTask SetGain(double value) => _js.InvokeVoid("BitButil.webAudio.setGain", _id, value); + /// Stops playback and releases the browser-side handle. Calling it again does nothing. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAllowCredential.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAllowCredential.cs index 9c7b682c987..69aee599785 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAllowCredential.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAllowCredential.cs @@ -1,7 +1,15 @@ namespace Bit.Butil; +/// +/// One credential an assertion is allowed to be satisfied by. +///
+/// PublicKeyCredentialRequestOptions.allowCredentials +///
public class WebAuthnVerifyAllowCredential { + /// The credential id, base64url-encoded, as it was returned when the passkey was created. public required string Id { get; set; } + + /// Always "public-key"; it is the only credential type WebAuthn defines. public required string Type { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAuthenticatorSelection.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAuthenticatorSelection.cs index 496d90560ed..f29542f6b27 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAuthenticatorSelection.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyAuthenticatorSelection.cs @@ -1,6 +1,15 @@ namespace Bit.Butil; +/// +/// Which authenticators the relying party will accept for a new credential. +///
+/// PublicKeyCredentialCreationOptions.authenticatorSelection +///
public class WebAuthnVerifyAuthenticatorSelection { + /// + /// "platform" for the device's own authenticator (Touch ID, Windows Hello), + /// "cross-platform" for a roaming one such as a security key or a phone. + /// public required string AuthenticatorAttachment { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyOptions.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyOptions.cs index fc7176af65f..d07bcd228ed 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyOptions.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyOptions.cs @@ -1,12 +1,47 @@ namespace Bit.Butil; +/// +/// The options one WebAuthn call is made with. The same object serves both directions: creating a +/// passkey reads , , and +/// , while asserting an existing one reads +/// . is required either way. +///
+/// PublicKeyCredentialCreationOptions +///
public class WebAuthnVerifyOptions { + /// + /// The server-generated random challenge, base64url-encoded. It is what makes an assertion + /// unrepeatable, so it has to come from the server and be verified there. + /// public required string Challenge { get; set; } + + /// + /// How much attestation the relying party wants about the authenticator itself - + /// "none" (the default in practice), "indirect", "direct" or + /// "enterprise". + /// public string? Attestation { get; set; } + + /// The relying party - the site the credential belongs to. Creation only. public WebAuthnVerifyRp? Rp { get; set; } + + /// The account the credential is being created for. Creation only. public WebAuthnVerifyUser? User { get; set; } + + /// Which kind of authenticator is acceptable. Creation only. public WebAuthnVerifyAuthenticatorSelection? AuthenticatorSelection { get; set; } + + /// + /// The signature algorithms the relying party accepts, most preferred first. Creation only. + /// public WebAuthnVerifyPubKeyCredParam[]? PubKeyCredParams { get; set; } + + /// + /// The credentials this assertion may be satisfied by. Assertion only; leaving it empty asks + /// the browser to offer whatever discoverable credential it holds for the origin. + ///
+ /// PublicKeyCredentialRequestOptions.allowCredentials + ///
public WebAuthnVerifyAllowCredential[]? AllowCredentials { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyPubKeyCredParam.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyPubKeyCredParam.cs index 459892a583e..a3f022ea6a0 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyPubKeyCredParam.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyPubKeyCredParam.cs @@ -1,7 +1,18 @@ namespace Bit.Butil; +/// +/// One signature algorithm the relying party is willing to verify. +///
+/// PublicKeyCredentialCreationOptions.pubKeyCredParams +///
public class WebAuthnVerifyPubKeyCredParam { + /// + /// The COSE algorithm identifier - -7 for ES256 and -257 for RS256, the two every + /// authenticator supports. + /// public required int Alg { get; set; } + + /// Always "public-key"; it is the only credential type WebAuthn defines. public required string Type { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyRp.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyRp.cs index 1822d98fc05..2581cc0f756 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyRp.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyRp.cs @@ -1,6 +1,13 @@ namespace Bit.Butil; +/// +/// The relying party a credential is created for - the site itself. Its id is always the current +/// origin's domain and is filled in by the browser, so only the display name is stated here. +///
+/// PublicKeyCredentialCreationOptions.rp +///
public class WebAuthnVerifyRp { + /// The site name the browser shows in its passkey prompt. public string? Name { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyUser.cs b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyUser.cs index 03d899d59ca..2498cd8e5e5 100644 --- a/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyUser.cs +++ b/src/Butil/Bit.Butil/Publics/WebAuthn/WebAuthnVerifyUser.cs @@ -1,8 +1,23 @@ namespace Bit.Butil; +/// +/// The account a passkey is created for. The browser stores these three values with the credential +/// and shows them when the user later picks between passkeys. +///
+/// PublicKeyCredentialCreationOptions.user +///
public class WebAuthnVerifyUser { + /// + /// An opaque, base64url-encoded account handle - at most 64 bytes decoded. It must not be an + /// email address or anything else that identifies the person, since it is stored on the + /// authenticator. + /// public string? Id { get; set; } + + /// The account name, usually the sign-in identifier the user recognises. public string? Name { get; set; } + + /// A friendlier name for the same account, for the browser's own picker. public string? DisplayName { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Window.cs b/src/Butil/Bit.Butil/Publics/Window.cs index 0632b65c060..34c2a2bb2d7 100644 --- a/src/Butil/Bit.Butil/Publics/Window.cs +++ b/src/Butil/Bit.Butil/Publics/Window.cs @@ -50,6 +50,16 @@ public void InvokeMediaQueryChange(Guid id, MediaQueryList state) if (_matchMediaHandlers.TryGetValue(id, out var handler)) handler.Invoke(state); } + /// + /// Adds a listener for one window-level event. is the event-args type + /// the event maps to - , + /// and friends - and the payload is projected to that shape on the JavaScript side. + ///
+ /// EventTarget.addEventListener() + ///
+ /// The event name, e.g. a constant. + /// Called when the event fires. Removing it later matches on this delegate's identity. + /// Listen during the capture phase rather than the bubble phase. public async Task AddEventListener(string domEvent, Action listener, bool useCapture = false) { var id = await _events.AddEventListener(js, ElementName, domEvent, listener, useCapture); @@ -685,6 +695,7 @@ public async Task ScrollBy(float? x, float? y) public async Task Stop() => await js.InvokeVoid("BitButil.window.stop"); + /// Removes every listener this instance registered on the window and releases its interop reference. public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -692,6 +703,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// The disposal body. is false only on a finalizer path, where + /// reaching back into JavaScript is not safe, so nothing is torn down then. + /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposing is false) return; diff --git a/src/Butil/Bit.Butil/Publics/Window/BarProp.cs b/src/Butil/Bit.Butil/Publics/Window/BarProp.cs index b8410d86220..28de9999b87 100644 --- a/src/Butil/Bit.Butil/Publics/Window/BarProp.cs +++ b/src/Butil/Bit.Butil/Publics/Window/BarProp.cs @@ -1,6 +1,15 @@ namespace Bit.Butil; +/// +/// The visibility of one of the browser's own bars (menu, toolbar, status bar and friends). Modern +/// engines report a fixed answer rather than the real state - typically true in a normal tab +/// and false in a popup - so this reads as "is this a chrome-less window", not as "is that +/// particular bar on screen". +///
+/// BarProp +///
public class BarProp { + /// Whether the bar is reported as visible. public bool Visible { get; set; } } diff --git a/src/Butil/Bit.Butil/Publics/Window/MediaQueryList.cs b/src/Butil/Bit.Butil/Publics/Window/MediaQueryList.cs index 0406be9f06b..d7f49ebfb27 100644 --- a/src/Butil/Bit.Butil/Publics/Window/MediaQueryList.cs +++ b/src/Butil/Bit.Butil/Publics/Window/MediaQueryList.cs @@ -1,7 +1,17 @@ namespace Bit.Butil; +/// +/// The result of evaluating one media query - a snapshot, not the live object the browser keeps. +/// To follow it over time, subscribe through rather than +/// re-reading this. +///
+/// MediaQueryList +///
public class MediaQueryList { + /// Whether the document matched the query at the moment it was evaluated. public bool Matches { get; set; } + + /// The query as the browser serialized it, which may be normalised from what was asked. public string Media { get; set; } = default!; } diff --git a/src/Butil/Bit.Butil/Publics/Window/WindowFeatures.cs b/src/Butil/Bit.Butil/Publics/Window/WindowFeatures.cs index 8823252eccb..4e08f1a88e4 100644 --- a/src/Butil/Bit.Butil/Publics/Window/WindowFeatures.cs +++ b/src/Butil/Bit.Butil/Publics/Window/WindowFeatures.cs @@ -2,16 +2,44 @@ namespace Bit.Butil; +/// +/// The window features a window.open call is made with, rendered by +/// into the comma-separated string the browser expects. Sizes and positions +/// are requests: engines clamp them to the screen, ignore them for a tab, and ignore them entirely +/// unless the call came from a user gesture. +///
+/// Window.open() windowFeatures +///
public class WindowFeatures { + /// + /// Ask for a popup window rather than a tab. The size and position members below only mean + /// anything when this is set, since a tab has neither. + /// public bool Popup { get; set; } + + /// The content width in CSS pixels. Values under 100 are dropped, as browsers clamp them anyway. public uint Width { get; set; } + + /// The content height in CSS pixels. Values under 100 are dropped, as browsers clamp them anyway. public uint Height { get; set; } + + /// The distance from the left edge of the screen. -1, the default, means "don't ask". public int Left { get; set; } = -1; + + /// The distance from the top edge of the screen. -1, the default, means "don't ask". public int Top { get; set; } = -1; + + /// + /// Open without a usable window.opener, so the new document cannot reach back into this + /// one. Worth setting for anything untrusted. + /// public bool NoOpener { get; set; } + + /// Open without sending a Referer header. Implies in every engine. public bool NoReferrer { get; set; } + /// Renders the features set here into the comma-separated string window.open parses. public override string ToString() { var list = new[] { diff --git a/src/Butil/CLAUDE.md b/src/Butil/CLAUDE.md new file mode 100644 index 00000000000..83e8314c69e --- /dev/null +++ b/src/Butil/CLAUDE.md @@ -0,0 +1,105 @@ +# Bit.Butil + +Strongly-typed C# wrappers over browser Web APIs for Blazor (WebAssembly, Server, Hybrid, prerendering; net8.0/9.0/10.0). +`README.md` in this folder is the reference guide and is also served by the MCP server - keep it accurate. + +## Layout + +| Path | What it is | +| --- | --- | +| `Bit.Butil/Publics/` | The public API: one injectable service class per browser API, plus its DTOs/enums in a same-named subfolder | +| `Bit.Butil/Internals/` | Interop helpers, JS-callable relay classes, JS-shaped option types | +| `Bit.Butil/Scripts/*.ts` | One TypeScript file = one JS module = one `BitButil.` namespace | +| `Bit.Butil/build.mjs` | Assembles `wwwroot/bit-butil.js`, `wwwroot/modules/*.js` and the packed chunks + manifest (run by MSBuild; outputs are generated and git-ignored) | +| `Bit.Butil.Build/` | MSBuild task run in a consumer's publish: script scanning, trimming, bundling | +| `Bit.Butil.Demo/` | The documentation site (Client) and its host (Server), which also hosts the MCP server at `/mcp` | +| `Samples/` | Minimal hosting samples: `Samples.Core` (shared pages), `Samples.Web` (standalone WebAssembly), `Samples.Maui` (Hybrid) | +| `tests/` | `Tests.E2E` (Playwright), `Tests.Mcp` (MSTest against the live MCP server), `Tests.Manual` (trimming/bundling console harness), `Tests.PublishFixture` (the consumer app it publishes) | + +## Coding style + +Follow the `.editorconfig` at the root of `src`. Match the surrounding code: file-scoped namespaces, primary +constructors, expression-bodied one-liners, `is null` / `is false` over `== null` / `!`. Comments explain *why* +a thing is shaped the way it is (trimming, prerendering, browser quirks), not what the next line does. + +Every public type and member carries XML documentation ending in a link to its MDN page. That documentation is +what IntelliSense and the MCP server hand out, so it is part of the feature, not decoration. + +Each top-level type lives in its own file, and the file is named after the type. No file declares more than +one top-level type (nested types belong to their container, so they stay with it). The single exception is a +generic type accompanying an existing non-generic one of the same name - those two may share a file. + +## Adding or changing an API + +1. **Service class** in `Publics/`, marked `[ButilService(typeof(TheClass))]` and taking `IJSRuntime` through a + primary constructor. The attribute is the only registration step - there is no list to update - and its type + argument is what preserves the constructor under trimming. Payload DTOs and enums go in `Publics//`. +2. **Interop identifiers must be literal strings** of the form `"BitButil.."`. Never build one + by interpolation or concatenation: the string literals surviving trimming *are* the list of JS modules a + published app can still reach, and both the publish-time bundler and the `Bit.Butil.Build` scanner read them. +3. **Call through the `Invoke` / `InvokeVoid` extensions** (`Extensions/JSRuntimeExtensions.cs`), not + `IJSRuntime` directly - they return safe defaults instead of throwing during prerender/SSR, and route through + the lazy-script loader. +4. **JavaScript** in a `Scripts/.ts` that attaches to `window.BitButil`, following the existing shape. + Expose an `isSupported()` where the API is not universally implemented. Modules must be safe to evaluate more + than once; cross-module references (`butil.utils.*`) are discovered by `build.mjs` as dependencies. +5. **Types crossing the interop boundary** need `[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(T))]` + at the call site so trimming keeps what `System.Text.Json` reflects over. `[JSInvokable]` callbacks use the + explicit-identifier form (`[JSInvokable(InvokeMethodName)]`). +6. **Anything attaching a listener returns a `ButilSubscription`**; anything holding a browser resource open + (streams, recorders, handles) is `IAsyncDisposable`. Document the gesture/HTTPS/permission preconditions. +7. Add the service to the **`README.md` "What's in the box"** table. + +## Showcases - the Demo and Samples projects + +Every feature has to be demonstrable, not merely documented. A new or extended API is not done until: + +- **`Bit.Butil.Demo/Client/Pages/Page.razor`** covers it: a `PageHeader` (category, lead, MDN link, + `InjectAs`), one `DemoSection` per public member or coherent group of members - each with a runnable control, + a `Code` snippet the reader can copy, and a `DemoConsole` for output - and an `ApiTable` row for every public + member with its signature. Wrap calls in try/catch and report failures through the console, since most of these + APIs fail by refusing rather than by throwing something meaningful. +- **`Bit.Butil.Demo/Client/Docs/DocsNav.cs`** lists the page: title, slug (matching its `@page`), summary, page + type, `ApiSupport`, `ApiNeeds`, and `Services` when the type names are not the title. That list is the single + source of truth for the sidebar, the home grid, the browser-support matrix, the pager, the search index and the + MCP capability catalog - a page missing from it is invisible everywhere except its own URL. +- **`Samples/Bit.Butil.Samples.Core/Pages/`** carries the smaller, hosting-model-neutral version (a `DemoCard` + per scenario) with an entry in `Shared/NavMenu.razor`, so the same code is exercised under WebAssembly, Server + and MAUI. +- The extent of the showcase matches the extent of the API: a member with no way to exercise it in the Demo is a + member nobody can verify works. + +## MCP server + +The demo server hosts an MCP server (`Bit.Butil.Demo/Server/Controllers/McpController.cs`, seven tools, mirrored +as plain GETs under `/api/mcp/...`). Every feature must be reachable through it, which in practice means: + +- Its answers are **derived, not hand-written**: the API reference is reflected out of the shipped assembly with + its XML docs, capabilities come from `DocsNav`, and the guide and sources are embedded files. So a new API + reaches the tools by having XML documentation, a `DocsNav` entry, and a page that renders **outside the router + and layout** (the tools render pages standalone - a page reading something from its surroundings breaks there). +- New files under `Client/`, `Server/Components/` or `Samples/` are picked up by the `EmbeddedResource` globs in + `Bit.Butil.Demo/Server/Bit.Butil.Demo.Server.csproj`; a file outside them is a source the tools cannot serve. +- Hand-maintained lists that reflection cannot reach - notably `_fastInvokeServices` in + `Server/Services/ButilCapabilityCatalog.cs` - must be updated along with the feature. +- **Do not add tools.** The surface is deliberately seven: a tool description is paid for in every request of + every session. A listing is what a retrieval tool answers when called with no argument, not a tool of its own. + Adding one is a deliberate decision that also changes `tests/Bit.Butil.Tests.Mcp/Infrastructure/WireContracts.cs`. +- Tool, resource and prompt names are identifiers clients store: renaming one is a breaking change. + +## Tests + +Cover a feature in whichever of these it belongs to - in more than one, where it belongs to more than one: + +| Project | Covers | Run | +| --- | --- | --- | +| `tests/Bit.Butil.Tests.E2E` | Real browser behaviour, through the deterministic harness pages `Samples.Core/Pages/E2EPage.razor` and `E2EObserversPage.razor`. Give every control a stable `id`, write results to the single status element, and avoid APIs that prompt, so the suite stays headless and flake-free. | `dotnet test tests/Bit.Butil.Tests.E2E` (see its README for the browser env vars) | +| `tests/Bit.Butil.Tests.Mcp` | The MCP server against a real child-process deployment driven by a real MCP client: tool surface, behaviour, failures, search, resources, prompts, completions, the HTTP mirror, and cross-catalog consistency. | `dotnet test tests/Bit.Butil.Tests.Mcp` | +| `tests/Bit.Butil.Tests.Manual` | Trimming, the interop contract, and script scanning/bundling/trimming/publishing. A console app because the subject is a *publish* output; it exits non-zero on failure. | See its README - run untrimmed then trimmed from that folder, sharing `interop-manifest.txt` | + +`interop-manifest.txt` (this folder, and the Manual harness's copy) is generated from an untrimmed run and is the +contract for `[JSInvokable]` identifiers, JSON payload members and the `[ButilService]` roster. Regenerate it +deliberately when the surface changes; a diff in it is a real interop change, not noise. + +New JS modules, or new cross-module dependencies, change what `build.mjs` emits - the bundling checks in the +Manual harness compare the shipped artifacts against the sources, so run that harness after touching `Scripts/`. diff --git a/src/Butil/tests/Bit.Butil.Tests.Manual/InteropContract.cs b/src/Butil/tests/Bit.Butil.Tests.Manual/InteropContract.cs index db069aaf9ad..0d4f96850e5 100644 --- a/src/Butil/tests/Bit.Butil.Tests.Manual/InteropContract.cs +++ b/src/Butil/tests/Bit.Butil.Tests.Manual/InteropContract.cs @@ -6,64 +6,6 @@ namespace ButilTests.Manual; -/// -/// Captures the members Bit.Butil reaches by reflection at runtime, so a trimmed publish can be checked -/// against an untrimmed capture of the same thing. -/// -/// -/// The reflection-based registration only settles who gets registered. Two other things inside the -/// library are resolved by name at runtime and would fail silently, in the browser, if the trimmer removed -/// them - and neither shows up as a missing service: -/// -/// [JSInvokable] callbacks. JS dispatches these by method name through a -/// DotNetObjectReference, including ones on internal types the consumer never names - -/// DomEventsInterop, the observer interops, IndexedDbHandle. Nothing in a consumer's code -/// references them. -/// JSON payload types. The DTOs and option objects crossing the interop boundary are -/// (de)serialized by System.Text.Json reflecting over their constructors and properties, so a -/// trimmed-away property turns into a silently null field rather than an error. -/// -/// Both are meant to be covered by annotations already in the library - DotNetObjectReference.Create -/// preserves public methods, and the Invoke<T> overloads annotate T with -/// LinkerFlags.JsonSerialized. This class is what verifies that claim on real output rather than -/// taking it on trust. -/// -internal sealed record TypeContract(string TypeName, bool IsCallbackTarget, bool IsPayload, int PublicConstructors, string[] JSInvokableIdentifiers, string[] PublicProperties) -{ - public string Serialize() - => string.Join('|', - TypeName, - IsCallbackTarget ? "J" : "-", - IsPayload ? "P" : "-", - PublicConstructors.ToString(), - string.Join(',', JSInvokableIdentifiers), - string.Join(',', PublicProperties)); - - public static TypeContract? Deserialize(string line) - { - var parts = line.Split('|'); - if (parts.Length != 6) return null; - - return new TypeContract( - parts[0], - parts[1] == "J", - parts[2] == "P", - int.TryParse(parts[3], out var constructors) ? constructors : 0, - SplitList(parts[4]), - SplitList(parts[5])); - } - - private static string[] SplitList(string value) - => value.Length == 0 ? [] : value.Split(','); -} - -/// -/// What an untrimmed run records for a trimmed run to check itself against: the interop contract, plus the -/// roster of [ButilService] names so the trimmed run can tell a genuinely trimmed-away service from a -/// name that no longer refers to anything. -/// -internal sealed record InteropManifest(string[] ServiceNames, TypeContract[] Types); - internal static class InteropContract { /// diff --git a/src/Butil/tests/Bit.Butil.Tests.Manual/InteropManifest.cs b/src/Butil/tests/Bit.Butil.Tests.Manual/InteropManifest.cs new file mode 100644 index 00000000000..c620a0fe0b8 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Manual/InteropManifest.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text; +using Bit.Butil; +using Microsoft.JSInterop; + +namespace ButilTests.Manual; + +/// +/// What an untrimmed run records for a trimmed run to check itself against: the interop contract, plus the +/// roster of [ButilService] names so the trimmed run can tell a genuinely trimmed-away service from a +/// name that no longer refers to anything. +/// +internal sealed record InteropManifest(string[] ServiceNames, TypeContract[] Types); diff --git a/src/Butil/tests/Bit.Butil.Tests.Manual/TypeContract.cs b/src/Butil/tests/Bit.Butil.Tests.Manual/TypeContract.cs new file mode 100644 index 00000000000..26bec7a2a05 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Manual/TypeContract.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text; +using Bit.Butil; +using Microsoft.JSInterop; + +namespace ButilTests.Manual; + +/// +/// Captures the members Bit.Butil reaches by reflection at runtime, so a trimmed publish can be checked +/// against an untrimmed capture of the same thing. +/// +/// +/// The reflection-based registration only settles who gets registered. Two other things inside the +/// library are resolved by name at runtime and would fail silently, in the browser, if the trimmer removed +/// them - and neither shows up as a missing service: +/// +/// [JSInvokable] callbacks. JS dispatches these by method name through a +/// DotNetObjectReference, including ones on internal types the consumer never names - +/// DomEventsInterop, the observer interops, IndexedDbHandle. Nothing in a consumer's code +/// references them. +/// JSON payload types. The DTOs and option objects crossing the interop boundary are +/// (de)serialized by System.Text.Json reflecting over their constructors and properties, so a +/// trimmed-away property turns into a silently null field rather than an error. +/// +/// Both are meant to be covered by annotations already in the library - DotNetObjectReference.Create +/// preserves public methods, and the Invoke<T> overloads annotate T with +/// LinkerFlags.JsonSerialized. This class is what verifies that claim on real output rather than +/// taking it on trust. +/// +internal sealed record TypeContract(string TypeName, bool IsCallbackTarget, bool IsPayload, int PublicConstructors, string[] JSInvokableIdentifiers, string[] PublicProperties) +{ + public string Serialize() + => string.Join('|', + TypeName, + IsCallbackTarget ? "J" : "-", + IsPayload ? "P" : "-", + PublicConstructors.ToString(), + string.Join(',', JSInvokableIdentifiers), + string.Join(',', PublicProperties)); + + public static TypeContract? Deserialize(string line) + { + var parts = line.Split('|'); + if (parts.Length != 6) return null; + + return new TypeContract( + parts[0], + parts[1] == "J", + parts[2] == "P", + int.TryParse(parts[3], out var constructors) ? constructors : 0, + SplitList(parts[4]), + SplitList(parts[5])); + } + + private static string[] SplitList(string value) + => value.Length == 0 ? [] : value.Split(','); +} diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiDetailsResult.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiDetailsResult.cs new file mode 100644 index 00000000000..3a5871962ac --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiDetailsResult.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record ApiDetailsResult(ApiTypeDetails? Details, ApiType[]? Types, string? Message); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiInspection.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiInspection.cs new file mode 100644 index 00000000000..0d134453d46 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiInspection.cs @@ -0,0 +1,18 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record ApiInspection( + string Query, + bool IsKnown, + string? Message, + string? Api, + string[]? Services, + string[]? Inject, + string? BrowserSupport, + string[]? Requires, + string[]? Disposables, + string[]? NextCalls); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiMember.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiMember.cs new file mode 100644 index 00000000000..88771b61dc1 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiMember.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record ApiMember(string Name, string Kind, string? Type, string? Signature, string? Default, string? Summary, string? Remarks); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiType.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiType.cs new file mode 100644 index 00000000000..82605b364c6 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiType.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record ApiType(string Name, string Kind, bool IsInjectable, string? Summary); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiTypeDetails.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiTypeDetails.cs new file mode 100644 index 00000000000..3e3b1a48104 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ApiTypeDetails.cs @@ -0,0 +1,17 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record ApiTypeDetails( + string Name, + string FullName, + string Kind, + string? Inject, + string[]? Implements, + string? Summary, + string? Remarks, + string? DocsUrl, + ApiMember[] Members); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireContracts.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ButilMcp.cs similarity index 50% rename from src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireContracts.cs rename to src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ButilMcp.cs index f7442c5fa23..2817c7088be 100644 --- a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireContracts.cs +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ButilMcp.cs @@ -103,119 +103,3 @@ public static class ButilMcp /// The suffix a truncated answer ends with. public const string TruncationMarker = "[truncated - the full text is longer than"; } - -/// -/// One row of the index GetButilDocsPage answers with when it is called with no slug - which -/// is also the whole of butil://support, the page listing and the browser-support matrix -/// having been folded into one table. -/// -/// Parsed out of Markdown rather than deserialized, because that is the form the answer takes: a -/// listing is read and then one value from it is passed back, so it ships as a table an agent reads -/// instead of as a DTO with a tool description to advertise it. The suite reads it the same way, -/// which also holds the table's columns to a shape. -/// -/// -public sealed partial record DocsIndexRow(string Group, string Slug, string Title, string Summary, string[] Services, string Engines, string[] Requires) -{ - /// Every row of the index, with the group heading each one sat under. - public static DocsIndexRow[] ParseAll(string markdown) - { - var rows = new List(); - var group = string.Empty; - - foreach (var line in markdown.Split('\n').Select(line => line.TrimEnd('\r'))) - { - if (line.StartsWith("## ", StringComparison.Ordinal)) - { - group = line[3..].Trim(); - continue; - } - - var match = RowRegex().Match(line); - if (match.Success is false) continue; - - // Split on the pipes rather than on the pattern: the row is six cells, and a cell that - // went missing should read as a short row here rather than as a row that did not match. - // Such a row is thrown on rather than dropped - a table that quietly lost a column would - // otherwise shrink every listing the suite compares against it, on both sides at once. - var cells = SplitCells(line); - if (cells.Length != 6) throw new FormatException($"The index has a row of {cells.Length} cells rather than six: {line.Trim()}"); - - rows.Add(new DocsIndexRow(group, match.Groups["slug"].Value, cells[1], cells[2], Cell(cells[3]), cells[4], Cell(cells[5]))); - } - - return [.. rows]; - } - - /// - /// A row's cells, split on the pipes that are column breaks rather than on every pipe. The - /// renderer writes a pipe inside a cell as \|, which is one character of that cell's - /// text; splitting on the raw character would read such a row as a column too long and throw on - /// it - reporting the corruption this exists to catch against the one row that is not corrupt. - /// - private static string[] SplitCells(string line) - { - var body = line.Trim(); - if (body.StartsWith('|')) body = body[1..]; - if (body.EndsWith('|') && body.EndsWith(@"\|", StringComparison.Ordinal) is false) body = body[..^1]; - - var cells = new List(); - var cell = new StringBuilder(); - - for (var i = 0; i < body.Length; i++) - { - if (body[i] == '\\' && i + 1 < body.Length && body[i + 1] == '|') - { - cell.Append('|'); - i++; - } - else if (body[i] == '|') - { - cells.Add(cell.ToString().Trim()); - cell.Clear(); - } - else cell.Append(body[i]); - } - - cells.Add(cell.ToString().Trim()); - - return [.. cells]; - } - - /// A list cell: comma-separated, or "-" when the row has none of that thing. - private static string[] Cell(string text) - => text is "-" or "" ? [] : [.. text.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; - - [GeneratedRegex(@"^\|\s*`(?[^`]+)`\s*\|")] - private static partial Regex RowRegex(); -} - -/// -/// A follow-up call a search hit names, e.g. GetButilDocsPage(slug: "clipboard"). -/// -/// Every hit the search returns carries one, and the whole design rests on it being callable -/// verbatim: an agent is told to make that call next, and a hit that names a call which does not -/// resolve sends it somewhere there is nothing. Parsing them back into real calls is how the suite -/// proves the promise instead of assuming it. -/// -/// -public sealed partial record ToolCallReference(string Tool, string Argument, string Value) -{ - public static ToolCallReference? Parse(string? text) - { - if (string.IsNullOrWhiteSpace(text)) return null; - - var match = CallRegex().Match(text.Trim()); - - return match.Success - ? new ToolCallReference(match.Groups["tool"].Value, match.Groups["argument"].Value, match.Groups["value"].Value) - : null; - } - - public Dictionary Arguments => new(StringComparer.Ordinal) { [Argument] = Value }; - - // Greedy on the value so a heading containing a quote still parses: the call always ends with - // the same two characters. - [GeneratedRegex("""^(?\w+)\((?\w+):\s*"(?.*)"\)$""")] - private static partial Regex CallRegex(); -} diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/DocsIndexRow.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/DocsIndexRow.cs new file mode 100644 index 00000000000..34f8376f4c1 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/DocsIndexRow.cs @@ -0,0 +1,90 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +/// +/// One row of the index GetButilDocsPage answers with when it is called with no slug - which +/// is also the whole of butil://support, the page listing and the browser-support matrix +/// having been folded into one table. +/// +/// Parsed out of Markdown rather than deserialized, because that is the form the answer takes: a +/// listing is read and then one value from it is passed back, so it ships as a table an agent reads +/// instead of as a DTO with a tool description to advertise it. The suite reads it the same way, +/// which also holds the table's columns to a shape. +/// +/// +public sealed partial record DocsIndexRow(string Group, string Slug, string Title, string Summary, string[] Services, string Engines, string[] Requires) +{ + /// Every row of the index, with the group heading each one sat under. + public static DocsIndexRow[] ParseAll(string markdown) + { + var rows = new List(); + var group = string.Empty; + + foreach (var line in markdown.Split('\n').Select(line => line.TrimEnd('\r'))) + { + if (line.StartsWith("## ", StringComparison.Ordinal)) + { + group = line[3..].Trim(); + continue; + } + + var match = RowRegex().Match(line); + if (match.Success is false) continue; + + // Split on the pipes rather than on the pattern: the row is six cells, and a cell that + // went missing should read as a short row here rather than as a row that did not match. + // Such a row is thrown on rather than dropped - a table that quietly lost a column would + // otherwise shrink every listing the suite compares against it, on both sides at once. + var cells = SplitCells(line); + if (cells.Length != 6) throw new FormatException($"The index has a row of {cells.Length} cells rather than six: {line.Trim()}"); + + rows.Add(new DocsIndexRow(group, match.Groups["slug"].Value, cells[1], cells[2], Cell(cells[3]), cells[4], Cell(cells[5]))); + } + + return [.. rows]; + } + + /// + /// A row's cells, split on the pipes that are column breaks rather than on every pipe. The + /// renderer writes a pipe inside a cell as \|, which is one character of that cell's + /// text; splitting on the raw character would read such a row as a column too long and throw on + /// it - reporting the corruption this exists to catch against the one row that is not corrupt. + /// + private static string[] SplitCells(string line) + { + var body = line.Trim(); + if (body.StartsWith('|')) body = body[1..]; + if (body.EndsWith('|') && body.EndsWith(@"\|", StringComparison.Ordinal) is false) body = body[..^1]; + + var cells = new List(); + var cell = new StringBuilder(); + + for (var i = 0; i < body.Length; i++) + { + if (body[i] == '\\' && i + 1 < body.Length && body[i + 1] == '|') + { + cell.Append('|'); + i++; + } + else if (body[i] == '|') + { + cells.Add(cell.ToString().Trim()); + cell.Clear(); + } + else cell.Append(body[i]); + } + + cells.Add(cell.ToString().Trim()); + + return [.. cells]; + } + + /// A list cell: comma-separated, or "-" when the row has none of that thing. + private static string[] Cell(string text) + => text is "-" or "" ? [] : [.. text.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; + + [GeneratedRegex(@"^\|\s*`(?[^`]+)`\s*\|")] + private static partial Regex RowRegex(); +} diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs new file mode 100644 index 00000000000..21adeb145f6 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs @@ -0,0 +1,20 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record FeaturePlan( + ApiInspection[] Apis, + string[] Unknown, + bool RequiresSecureContext, + bool RequiresPermission, + bool RequiresUserGesture, + string[] EngineLimited, + string[] Checklist, + string[]? Ignored); + +// No record for the listings: they are answered as Markdown, not as structured content, which is +// what let the four tools that used to serve them go away. DocsIndexRow in WireContracts.cs parses +// the docs index, and McpTestBase.ListAsync reads the identifiers out of the other two. diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchHit.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchHit.cs new file mode 100644 index 00000000000..e306f06e293 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchHit.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record SearchHit(string Kind, string Title, string? Context, string Tool, string Snippet); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchResult.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchResult.cs new file mode 100644 index 00000000000..1ccc74144e8 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/SearchResult.cs @@ -0,0 +1,8 @@ +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +// The shapes the structured tools answer with, re-declared here rather than shared with the server. +// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// or dropped on the server has to fail a test instead of quietly flowing through a shared type. +// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. + +public sealed record SearchResult(SearchHit[] Hits, string? Message); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ToolCallReference.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ToolCallReference.cs new file mode 100644 index 00000000000..cf2b2a66630 --- /dev/null +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/ToolCallReference.cs @@ -0,0 +1,34 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Bit.Butil.Tests.Mcp.Infrastructure; + +/// +/// A follow-up call a search hit names, e.g. GetButilDocsPage(slug: "clipboard"). +/// +/// Every hit the search returns carries one, and the whole design rests on it being callable +/// verbatim: an agent is told to make that call next, and a hit that names a call which does not +/// resolve sends it somewhere there is nothing. Parsing them back into real calls is how the suite +/// proves the promise instead of assuming it. +/// +/// +public sealed partial record ToolCallReference(string Tool, string Argument, string Value) +{ + public static ToolCallReference? Parse(string? text) + { + if (string.IsNullOrWhiteSpace(text)) return null; + + var match = CallRegex().Match(text.Trim()); + + return match.Success + ? new ToolCallReference(match.Groups["tool"].Value, match.Groups["argument"].Value, match.Groups["value"].Value) + : null; + } + + public Dictionary Arguments => new(StringComparer.Ordinal) { [Argument] = Value }; + + // Greedy on the value so a heading containing a quote still parses: the call always ends with + // the same two characters. + [GeneratedRegex("""^(?\w+)\((?\w+):\s*"(?.*)"\)$""")] + private static partial Regex CallRegex(); +} diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireDtos.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireDtos.cs deleted file mode 100644 index fe5a9beef76..00000000000 --- a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/WireDtos.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Bit.Butil.Tests.Mcp.Infrastructure; - -// The shapes the structured tools answer with, re-declared here rather than shared with the server. -// That is deliberate: these records ARE the contract a client codes against, so a property renamed -// or dropped on the server has to fail a test instead of quietly flowing through a shared type. -// Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. - -public sealed record SearchResult(SearchHit[] Hits, string? Message); - -public sealed record SearchHit(string Kind, string Title, string? Context, string Tool, string Snippet); - -public sealed record ApiType(string Name, string Kind, bool IsInjectable, string? Summary); - -public sealed record ApiMember(string Name, string Kind, string? Type, string? Signature, string? Default, string? Summary, string? Remarks); - -public sealed record ApiTypeDetails( - string Name, - string FullName, - string Kind, - string? Inject, - string[]? Implements, - string? Summary, - string? Remarks, - string? DocsUrl, - ApiMember[] Members); - -public sealed record ApiDetailsResult(ApiTypeDetails? Details, ApiType[]? Types, string? Message); - -public sealed record ApiInspection( - string Query, - bool IsKnown, - string? Message, - string? Api, - string[]? Services, - string[]? Inject, - string? BrowserSupport, - string[]? Requires, - string[]? Disposables, - string[]? NextCalls); - -public sealed record FeaturePlan( - ApiInspection[] Apis, - string[] Unknown, - bool RequiresSecureContext, - bool RequiresPermission, - bool RequiresUserGesture, - string[] EngineLimited, - string[] Checklist, - string[]? Ignored); - -// No record for the listings: they are answered as Markdown, not as structured content, which is -// what let the four tools that used to serve them go away. DocsIndexRow in WireContracts.cs parses -// the docs index, and McpTestBase.ListAsync reads the identifiers out of the other two. From 4e67f804f71dad079ae32f6006d32c24e0adeb52 Mon Sep 17 00:00:00 2001 From: msynk Date: Wed, 2 Sep 2026 09:30:28 +0330 Subject: [PATCH 2/2] fix missing stuff --- .../Client/Pages/NavigatorPage.razor | 8 ++++---- .../Bit.Butil/Publics/Navigator/ShareData.cs | 16 +++++----------- src/Butil/CLAUDE.md | 2 +- .../Pages/NavigatorPage.razor | 6 +++--- .../Infrastructure/FeaturePlan.cs | 8 ++++---- src/Butil/tests/Bit.Butil.Tests.Mcp/README.md | 8 +++++--- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/Butil/Bit.Butil.Demo/Client/Pages/NavigatorPage.razor b/src/Butil/Bit.Butil.Demo/Client/Pages/NavigatorPage.razor index 423c664a0f5..f702bfe6f2f 100644 --- a/src/Butil/Bit.Butil.Demo/Client/Pages/NavigatorPage.razor +++ b/src/Butil/Bit.Butil.Demo/Client/Pages/NavigatorPage.razor @@ -63,9 +63,9 @@ if (await navigator.CanShare()) { await navigator.Share(new ShareData { - title = "Bit.Butil", + Title = "Bit.Butil", Text = "Browser APIs for C# developers", - url = "https://bitplatform.dev", + Url = "https://bitplatform.dev", }); } @@ -244,9 +244,9 @@ bool accepted = await navigator.Vibrate([200, 100, 200]); { await navigator.Share(new ShareData { - title = "Bit.Butil", + Title = "Bit.Butil", Text = "Browser APIs for C# developers", - url = "https://bitplatform.dev", + Url = "https://bitplatform.dev", }); await shareOutput.Success("Share sheet completed."); } diff --git a/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs b/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs index 020918ed236..f1c4ac567db 100644 --- a/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs +++ b/src/Butil/Bit.Butil/Publics/Navigator/ShareData.cs @@ -1,4 +1,4 @@ -namespace Bit.Butil; +namespace Bit.Butil; /// /// What the native share sheet is handed. At least one of the three has to be set, and a URL that @@ -13,15 +13,9 @@ public class ShareData /// The body text to share. public string? Text { get; set; } - /// - /// The title to share. Lower-cased because it is the JSON member name the browser reads; - /// renaming it would break callers. - /// - public string? title { get; set; } + /// The title to share. + public string? Title { get; set; } - /// - /// The URL to share, absolute. Lower-cased because it is the JSON member name the browser - /// reads; renaming it would break callers. - /// - public string? url { get; set; } + /// The URL to share, absolute. + public string? Url { get; set; } } diff --git a/src/Butil/CLAUDE.md b/src/Butil/CLAUDE.md index 83e8314c69e..ab73d2d293b 100644 --- a/src/Butil/CLAUDE.md +++ b/src/Butil/CLAUDE.md @@ -84,7 +84,7 @@ as plain GETs under `/api/mcp/...`). Every feature must be reachable through it, `Server/Services/ButilCapabilityCatalog.cs` - must be updated along with the feature. - **Do not add tools.** The surface is deliberately seven: a tool description is paid for in every request of every session. A listing is what a retrieval tool answers when called with no argument, not a tool of its own. - Adding one is a deliberate decision that also changes `tests/Bit.Butil.Tests.Mcp/Infrastructure/WireContracts.cs`. + Adding one is a deliberate decision that also changes `tests/Bit.Butil.Tests.Mcp/Infrastructure/ButilMcp.cs`. - Tool, resource and prompt names are identifiers clients store: renaming one is a breaking change. ## Tests diff --git a/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/NavigatorPage.razor b/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/NavigatorPage.razor index 0bdb79829d7..c7e71af8a1c 100644 --- a/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/NavigatorPage.razor +++ b/src/Butil/Samples/Bit.Butil.Samples.Core/Pages/NavigatorPage.razor @@ -12,7 +12,7 @@ @@inject Bit.Butil.Navigator navigator var userAgent = await navigator.GetUserAgent(); -await navigator.Share(new ShareData { title = "Bit.Butil", url = "https://bitplatform.dev" }); +await navigator.Share(new ShareData { Title = "Bit.Butil", Url = "https://bitplatform.dev" });
@@ -137,8 +137,8 @@ await navigator.Share(new ShareData { title = "Bit.Butil", url = "https://bitpla var shareData = new ShareData() { Text = textValue, - title = titleValue, - url = urlValue + Title = titleValue, + Url = urlValue }; await navigator.Share(shareData); diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs index 21adeb145f6..c64b4efb15b 100644 --- a/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/Infrastructure/FeaturePlan.cs @@ -1,7 +1,7 @@ namespace Bit.Butil.Tests.Mcp.Infrastructure; -// The shapes the structured tools answer with, re-declared here rather than shared with the server. -// That is deliberate: these records ARE the contract a client codes against, so a property renamed +// The shape PlanButilFeature answers with, re-declared here rather than shared with the server. +// That is deliberate: this record IS the contract a client codes against, so a property renamed // or dropped on the server has to fail a test instead of quietly flowing through a shared type. // Only the fields the suite asserts on are declared; unknown ones are ignored by the deserializer. @@ -16,5 +16,5 @@ public sealed record FeaturePlan( string[]? Ignored); // No record for the listings: they are answered as Markdown, not as structured content, which is -// what let the four tools that used to serve them go away. DocsIndexRow in WireContracts.cs parses -// the docs index, and McpTestBase.ListAsync reads the identifiers out of the other two. +// what let the four tools that used to serve them go away. DocsIndexRow parses the docs index, and +// McpTestBase.ListAsync reads the identifiers out of the other two. diff --git a/src/Butil/tests/Bit.Butil.Tests.Mcp/README.md b/src/Butil/tests/Bit.Butil.Tests.Mcp/README.md index 25128d33d9f..411bd2e29f6 100644 --- a/src/Butil/tests/Bit.Butil.Tests.Mcp/README.md +++ b/src/Butil/tests/Bit.Butil.Tests.Mcp/README.md @@ -44,8 +44,10 @@ suite runs the app the way the app runs. | --- | --- | | `Infrastructure/McpServerFixture.cs` | Assembly-level `[SetUpFixture]`: boots the demo server on a free port, into its own artifacts path so a developer's running instance cannot lock the build. | | `Infrastructure/McpTestBase.cs` | A live `McpClient` per fixture, and the helpers the assertions are written in. | -| `Infrastructure/WireContracts.cs` | The server's public inventory, written down tool, resource and prompt names are identifiers clients store, so renaming one has to fail a test. Also parses the `Tool` strings hits hand back into real calls. | -| `Infrastructure/WireDtos.cs` | The payloads the data tools answer with, re-declared rather than shared with the server: these records **are** the contract a client codes against. | +| `Infrastructure/ButilMcp.cs` | The server's public inventory, written down: tool, resource and prompt names are identifiers clients store, so renaming one has to fail a test. | +| `Infrastructure/ToolCallReference.cs` | The follow-up call a search hit names, parsed back into a real call so the suite invokes what the hit promised instead of assuming it resolves. | +| `Infrastructure/DocsIndexRow.cs` | One row of the index `GetButilDocsPage` answers with when it is called with no slug, parsed out of Markdown because that is the form the answer takes. | +| `Infrastructure/ApiDetailsResult.cs`, `ApiInspection.cs`, `ApiMember.cs`, `ApiType.cs`, `ApiTypeDetails.cs`, `FeaturePlan.cs`, `SearchHit.cs`, `SearchResult.cs` | The payloads the data tools answer with, re-declared rather than shared with the server: these records **are** the contract a client codes against. | | `ServerContractTests.cs` | The handshake serverInfo, advertised capabilities, and the instructions the model carries all session. | | `ToolSurfaceTests.cs` | tools/list: the names, titles, descriptions, annotations, input schemas, the absence of output schemas which would double every answer and the standing context cost of the whole surface. | | `ToolBehaviourTests.cs` | What each tool answers when called properly including rendering **every** documentation page. | @@ -61,7 +63,7 @@ suite runs the app the way the app runs. ## What a failure here usually means * **A tool, resource or prompt name changed** that is a breaking change for every client that - already holds the old name, and `WireContracts.cs` is where you accept it deliberately. The count + already holds the old name, and `ButilMcp.cs` is where you accept it deliberately. The count is part of that contract: the surface is deliberately **seven** tools, because a description is paid for in every request of every session. A listing is not a tool here it is what a retrieval tool answers when called with no argument and `PlanButilFeature` answers for one API as well as