diff --git a/.gitignore b/.gitignore index b085b1d..c593e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .idea/ .DS_Store *~ +*.cached.json +*.exe *.out +*.prof vendor/ dist/ .env diff --git a/README.md b/README.md index 22ed16d..4e52252 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,74 @@ [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Lightweight+fast+and+deps+free+commented+json+parser+for+Golang&url=https://github.com/adhocore/jsonc&hashtags=go,golang,parser,json,json-comment) -- Lightweight JSON comment stripper library for Go. +- Lightweight [JSON5](https://json5.org) pre-processor library for Go. +- Parses JSON5 input to JSON that Go understands. (Think of it as a superset to JSON.) - Makes possible to have comment in any form of JSON data. - Supported comments: single line `// comment` or multi line `/* comment */`. -- Also strips trailing comma at the end of array or object, eg: +- Supports trailing comma at the end of array or object, eg: - `[1,2,,]` => `[1,2]` - `{"x":1,,}` => `{"x":1}` -- Handles literal LF (newline/linefeed) within string notation so that we can have multiline string -- Supports JSON string inside JSON string +- Supports single quoted string. +- Supports object keys without quotes. +- Handles literal LF (linefeed) in string for splitting long lines. +- Supports explicit positive and hex number. `{"change": +10, "hex": 0xffff}` +- Supports decimal numbers with leading or trailing period. `{"leading": .5, "trailing": 2.}` +- Supports JSON string inside JSON string. - Zero dependency (no vendor bloat). +--- +### Example + +This is [example](./examples/test.json5) of the JSON that you can parse with `adhocore/jsonc`: + +```json5 +/*start*/ +//.. +{ + // this is line comment + a: [ // unquoted key + 'bb', // single quoted string + "cc", // double quoted string + /* multi line + * comment + */ + 123, // number + +10, // +ve number, equivalent to 10 + -20, // -ve number + .25, // floating number, equivalent to 0.25 + 5., // floating number, equivalent to 5.0 + 0xabcDEF, // hex base16 number, equivalent to base10 counterpart: 11259375 + { + 123: 0xf, // even number as key? + xx: [1, .1, 'xyz',], y: '2', // array inside object, inside array + }, + "// not a comment", + "/* also not a comment */", + ['', "", true, false, null, 1, .5, 2., 0xf, // all sort of data types + {key:'val'/*comment*/,}], // object inside array, inside array + 'single quoted', + ], + /*aa*/aa: ['AA', {in: ['a', "b", ],},], + 'd': { // single quoted key + t: /*creepy comment*/true, 'f': false, + a_b: 1, _1_z: 2, Ḁẟḕỻ: 'ɷɻɐỨẞṏḉ', // weird keys? + "n": null /*multiple trailing commas?*/,,, + /* 1 */ + /* 2 */ + }, + "e": 'it\'s "good", eh?', // double quoted key, single quoted value with escaped quote + // json with comment inside json with comment, read that again: + "g": "/*comment*/{\"i\" : 1, \"url\" : \"http://foo.bar\" }//comment", + "h": "a new line after word 'literal' +this text is in a new line as there is literal EOL just above. \ +but this one is continued in same line due to backslash escape", + // 1. + // 2. +} +//.. +/*end*/ +``` + Find jsonc in [pkg.go.dev](https://pkg.go.dev/github.com/adhocore/jsonc). ## Installation @@ -26,13 +84,19 @@ Find jsonc in [pkg.go.dev](https://pkg.go.dev/github.com/adhocore/jsonc). go get -u github.com/adhocore/jsonc ``` +## Usecase + +You would ideally use this for organizing JSON configurations for humans to read and manage. +The JSON5 input is processed down into JSON which can be Unmarshal'ed by `encoding/json`. + +For performance reasons you may also use [cached decoder](#cached-decoder) to have a cached copy of processed JSON output. + ## Usage Import and init library: ```go import ( "fmt" - "github.com/adhocore/jsonc" ) @@ -84,12 +148,42 @@ j.UnmarshalFile("./examples/test.json5", &out) fmt.Printf("%+v\n", out) ``` +### Cached Decoder + +If you are weary of parsing same JSON5 source file over and over again, you can use cached decoder. +The source file is preprocessed and cached into output file with extension `.cached.json`. +It syncs the file `mtime` (aka modified time) from JSON5 source file to the cached JSON file to detect change. + +The output file can then be consumed readily by `encoding/json`. +Leave that cached output untouched for machine and deal with source file only. +> (You can add `*.cached.json` to `.gitignore` if you so wish.) + +As an example [examples/test.json5](./examples/test.json5) will be processed and cached into `examples/test.cached.json`. + +Every change in source file `examples/test.json5` is reflected to the cached output on next call to `Decode()` +thus always maintaining the sync. + +```go +import ( + "fmt" + "github.com/adhocore/jsonc" +) + +var dest map[string]interface{} +err := jsonc.NewCachedDecoder().Decode("./examples/test.json5", &dest); +if err != nil { + fmt.Printf("%+v", err) +} else { + fmt.Printf("%+v", dest) +} +``` + > Run working [examples](./examples/main.go) with `go run examples/main.go`. --- ## License -> © [MIT](./LICENSE) | 2021-2099, Jitendra Adhikari +> © [MIT](./LICENSE) | 2022-2099, Jitendra Adhikari --- ### Other projects diff --git a/decode.go b/decode.go new file mode 100644 index 0000000..75e21c5 --- /dev/null +++ b/decode.go @@ -0,0 +1,54 @@ +package jsonc + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// CachedDecoder is a managed decoder that caches a copy of json5 transitioned to json +type CachedDecoder struct { + jsonc *Jsonc + ext string +} + +// NewCachedDecoder gives a cached decoder +func NewCachedDecoder(ext ...string) *CachedDecoder { + ext = append(ext, ".cached.json") + return &CachedDecoder{New(), ext[0]} +} + +// Decode decodes from cache if exists and relevant else decodes from source +func (fd *CachedDecoder) Decode(file string, v interface{}) error { + stat, err := os.Stat(file) + if err != nil { + return err + } + + cache := strings.TrimSuffix(file, filepath.Ext(file)) + fd.ext + cstat, err := os.Stat(cache) + exist := !os.IsNotExist(err) + if err != nil && exist { + return err + } + + // Update if not exist, or source file modified + update := !exist || stat.ModTime() != cstat.ModTime() + if !update { + jsonb, _ := ioutil.ReadFile(cache) + return json.Unmarshal(jsonb, v) + } + + jsonb, _ := ioutil.ReadFile(file) + cfile, err := os.Create(cache) + if err != nil { + return err + } + + jsonb = fd.jsonc.Strip(jsonb) + cfile.Write(jsonb) + os.Chtimes(cache, stat.ModTime(), stat.ModTime()) + return json.Unmarshal(jsonb, v) +} diff --git a/examples/chromium.json5 b/examples/chromium.json5 new file mode 100644 index 0000000..1b0e297 --- /dev/null +++ b/examples/chromium.json5 @@ -0,0 +1,3128 @@ +// From: https://github.com/chromium/chromium/blob/feb3c9f670515edf9a88f185301cbd7794ee3e52/third_party/blink/renderer/platform/runtime_enabled_features.json5 +// +{ + // See third_party/blink/renderer/platform/RuntimeEnabledFeatures.md + // + // This list is used to generate runtime_enabled_features.h/cc which contains + // a class that stores static enablers for all experimental features. + + parameters: { + // Each feature can be assigned a "status". The "status" can be either + // one of the values in the |valid_values| list or a dictionary of + // the platforms listed in |valid_keys| to |valid_values|. + // Use "default" as the key if you want to specify the status of + // the platforms other than the ones declared in the dictionary. + // ** Omitting "default" means the feature is not enabled on + // the platforms not listed in the status dictionary + // + // Definition of each status: + // * status=stable: Enable this in all Blink configurations. We are + // committed to these APIs indefinitely. + // * status=experimental: In-progress features, Web Developers might play + // with, but are not on by default in stable. + // * status=test: Enabled in ContentShell for testing, otherwise off. + // Features without a status are not enabled anywhere by default. + // + // Example of the dictionary value use: + // { + // name: "ExampleFeature", + // status: {"Android": "stable", "Win": "experimental"}, + // } + // "ExampleFeature" will be stable on Android/WebView/WebLayer, experimental + // on Windows and not enabled on any other platform. + // + // Note that the Android status key implies Chrome for Android, WebView and + // WebLayer. + // + // "stable" features listed here should be rare, as anything which we've + // shipped stable can have its runtime flag removed soon after. + status: { + valid_values: ["stable", "experimental", "test"], + valid_keys: ["Android", "Win", "ChromeOS_Ash", "ChromeOS_Lacros", "Mac", "Linux"] + }, + + // "implied_by" or "depends_on" specifies relationship to other features: + // * implied_by: ["feature1","feature2",...] + // The feature is automatically enabled if any implied_by features is + // enabled. To effectively disable the feature, you must disable the + // feature and all the implied_by features. + // * depends_on: ["feature1","feature2",...] + // The feature can be enabled only if all depends_on features are enabled. + // Only one of "implied_by" and "depends_on" can be specified. + implied_by: { + default: [], + valid_type: "list", + }, + + // *DO NOT* specify features that depend on origin trial features. + // It is NOT supported. As a workaround, you can either specify the same + // |origin_trial_feature_name| for the feature or add the OT feature to + // the |implied_by| list. + // TODO(https://crbug.com/954679): Add support for origin trial features in 'depends_on' list + depends_on: { + default: [], + valid_type: "list", + }, + + // origin_trial_feature_name: "FEATURE_NAME" is used to integrate the + // feature with the Origin Trials framework. The framework allows the + // feature to be enabled at runtime on a per-page basis through a signed + // token for the corresponding feature name. Declaring the + // origin_trial_feature_name will modify the generation of the static + // methods in runtime_enabled_features.h/cpp -- the no-parameter version + // will not be generated, so all callers have to use the version that takes + // a const FeatureContext* argument. + origin_trial_feature_name: { + }, + // origin_trial_os specifies the platforms where the trial is available. + // The default is empty, meaning all platforms. + origin_trial_os: { + default: [], + valid_type: "list", + }, + // origin_trial_type specifies the unique type of the trial, when not the + // usual trial for a new experimental feature. + origin_trial_type: { + default: "", + valid_type: "str", + valid_values: ["deprecation", "intervention", ""], + }, + // origin_trial_allows_insecure specifies whether the trial can be enabled + // in an insecure context, with default being false. This can only be set + // to true for a "deprecation" type trial. + origin_trial_allows_insecure: { + valid_type: "bool", + }, + + // origin_trial_allows_third_party specifies whether the trial can be enabled + // from third party origins, with default being false. + origin_trial_allows_third_party: { + valid_type: "bool", + }, + + // settable_from_internals specifies whether a feature can be set from + // internals.runtimeFlags, with the default being false. + settable_from_internals: { + valid_type: "bool", + }, + + // public specifies whether a feature can be accessed via + // third_party/blink/public/platform/web_runtime_features.h, with the + // default being false. + public: { + valid_type: "bool", + }, + + // Feature policy IDL extended attribute (see crrev.com/2247923004). + feature_policy: { + }, + + // The string name of a base::Feature. The C++ variable name in + // blink::features is built with this string by prepending 'k'. + // If this field is specified, the base::Feature is automatically generated + // in features_generated.{h,cc}. + // + // The default value of the base::Feature instance is: + // base::FEATURE_ENABLED_BY_DEFAULT if 'status' field is 'stable", and + // base::FEATURE_DISABLED_BY_DEFAULT otheriwse. + // It can be overridden by 'base_feature_status' field. + // + // If the flag should be associated with a feature not in blink::features, + // we need to map them in content/child/runtime_features.cc. + base_feature: { + valid_type: "str", + default: "", + }, + + // Specifiy the default value of the base::Feature instance. This field + // works only if base_feature is not empty. + // If the field is missing or "", the default value depends on the 'status' + // field. See the comment above. + // "disabled" sets base::FEATURE_DISABLED_BY_DEFAULT, and "enabled" sets + // base::FEATURE_ENABLED_BY_DEFAULT. + base_feature_status: { + valid_type: "str", + valid_values: ["", "disabled", "enabled"], + default: "", + }, + + // Specify how the flag value is updated from the base::Feature value. This + // field is used only if base_feature is not empty. + // + // * "enabled_or_overridden" + // - If the base::Feature default is overridden by field trial or command + // line, set Blink feature to the state of the base::Feature; + // - Otherwise if the base::Feature is enabled, enable the Blink feature. + // - Otherwise no change. + // + // * "overridden" + // Enables the Blink feature when the base::Feature is overridden by field + // trial or command line. Otherwise no change. Its difference from + // "enabled_or_overridden" is that the Blink feature isn't affected by the + // default state of the base::Feature. + // + // This is useful for Blink origin trial features especially those + // implemented in both Chromium and Blink. As origin trial only controls + // the Blink features, for now we require the base::Feature to be enabled + // by default, but we don't want the default enabled status affect the + // Blink feature. See also https://crbug.com/1048656#c10. + // This can also be used for features that are enabled by default in + // Chromium but not in Blink on all platforms and we want to use the Blink + // status. However, we would prefer consistent Chromium and Blink status + // to this. + copied_from_base_feature_if: { + valid_type: "str", + valid_values: ["enabled_or_overridden", "overridden"], + default: "enabled_or_overridden", + }, + }, + + data: [ + { + name: "AbortSignalThrowIfAborted", + status: "stable", + }, + { + name: "AbortSignalTimeout", + status: "stable", + }, + { + name: "Accelerated2dCanvas", + settable_from_internals: true, + status: "stable", + }, + { + name: "AcceleratedSmallCanvases", + status: "stable", + }, + { + name: "AccessibilityAriaTouchPassthrough", + status: "experimental", + origin_trial_feature_name: "AccessibilityAriaTouchPassthrough", + }, + { + name: "AccessibilityAriaVirtualContent", + public: true, + status: "experimental", + }, + { + name: "AccessibilityExposeDisplayNone", + status: "test", + }, + { + name: "AccessibilityExposeHTMLElement", + public: true, + }, + { + name: "AccessibilityExposeIgnoredNodes", + public: true, + }, + { + name: "AccessibilityObjectModel", + status: "experimental", + }, + { + name: "AccessibilityPageZoom", + public: true, + }, + { + name: "AccessibilityUseAXPositionForDocumentMarkers", + public: true, + }, + { + name: "AddressSpace", + status: "experimental", + implied_by: ["CorsRFC1918"], + }, + { + // Interest Group JS API/runtimeflag. + name: "AdInterestGroupAPI", + origin_trial_feature_name: "AdInterestGroupAPI", + implied_by: ["Fledge", "Parakeet"], + base_feature: "AdInterestGroupAPI", + }, + { + name: "AdTagging", + public: true, + status: "test", + }, + { + name: "AllowContentInitiatedDataUrlNavigations", + }, + { + name: "AndroidDownloadableFontsMatching", + public: true, + }, + { + name: "AnimationWorklet", + status: "experimental", + }, + { + name: "AnonymousIframe", + origin_trial_allows_third_party: true, + origin_trial_feature_name: "AnonymousIframeOriginTrial", + public: true, + status: "test", + }, + { + name: "AOMAriaRelationshipProperties", + public: true, + status: "experimental", + }, + { + name: "AriaTouchPassthrough", + status: "experimental", + }, + { + name: "AttributionReporting", + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + status: "experimental", + }, + // The runtime flag for AudioContext.setSinkID(). + // See: https://github.com/WebAudio/web-audio-api/pull/2498 + { + name: "AudioContextSetSinkId", + status: "test", + }, + { + name: "AudioOutputDevices", + // Android does not yet support switching of audio output devices + status: {"Android": "", "default": "stable"}, + }, + { + name: "AudioVideoTracks", + status: "experimental", + }, + { + name: "AutoDarkMode", + origin_trial_feature_name: "AutoDarkMode", + }, + { + name: "AutoDisableAccessibilityV2", + public: true, + }, + { + // Makes autofill look across shadow boundaries when collecting form + // controls to fill. + name: "AutofillShadowDOM", + status: "experimental", + base_feature: "AutofillShadowDOM", + }, + { + name: "AutomationControlled", + public: true, + settable_from_internals: true, + }, + { + name: "AutoPictureInPicture", + depends_on: ["PictureInPictureAPI"], + status: "experimental", + }, + { + // Flag set by the media::kAutoplayIgnoreWebAudio feature flag. + name: "AutoplayIgnoresWebAudio", + public: true, + settable_from_internals: true, + }, + // When enabled, enforces new interoperable semantics for 3D transforms. + // See crbug.com/1008483. + { + name: "BackfaceVisibilityInterop", + base_feature: "BackfaceVisibilityInterop", + }, + { + name: "BackForwardCache", + public: true, + }, + { + name: "BackForwardCacheExperimentHTTPHeader", + origin_trial_feature_name: "BackForwardCacheExperimentHTTPHeader", + status: "experimental", + }, + { + name: "BackForwardCacheNotRestoredReasons", + status: "experimental", + origin_trial_feature_name: "BackForwardCacheNotRestoredReasons", + public: true, + }, + { + name: "BackgroundFetch", + public: true, + status: "stable", + }, + { + name: "BarcodeDetector", + status: { + // Built-in barcode detection APIs are only available from some + // platforms. See //services/shape_detection. + "Android": "stable", + "ChromeOS_Ash": "stable", + "ChromeOS_Lacros": "stable", + "Mac": "stable", + "default": "test", + }, + }, + { + name: "BatchFetchRequests", + status: "test", + base_feature: "BatchFetchRequests", + }, + { + // https://github.com/WICG/display-locking/blob/master/explainer-beforematch.md + name: "BeforeMatchEvent", + origin_trial_feature_name: "BeforeMatchEvent", + status: "stable", + }, + { + name: "BidiCaretAffinity", + }, + { + name: "BlinkExtensionChromeOS", + }, + { + name: "BlinkExtensionChromeOSHID", + depends_on: ["BlinkExtensionChromeOS"], + }, + { + name: "BlinkExtensionChromeOSWindowManagement", + depends_on: ["BlinkExtensionChromeOS"], + }, + { + name: "BlinkRuntimeCallStats", + }, + { + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#blocking-attributes + name: "BlockingAttribute", + status: "stable", + }, + { + name: "BlockingFocusWithoutUserActivation", + status: "experimental", + base_feature: "BlockingFocusWithoutUserActivation", + }, + { + name: "BrowserVerifiedUserActivationKeyboard", + public: true, + }, + { + name: "BrowserVerifiedUserActivationMouse", + public: true, + }, + { + name: "CacheStorageCodeCacheHint", + origin_trial_feature_name: "CacheStorageCodeCacheHint", + status: "experimental", + }, + { + name: "Canvas2dImageChromium", + public: true, + }, + { + name: "Canvas2dLayers", + }, + { + name: "Canvas2dScrollPathIntoView", + status: "experimental", + }, + { + name: "CanvasColorManagementV2", + status: "experimental", + }, + { + name: "CanvasHDR", + status: "experimental", + }, + { + name: "CanvasImageSmoothing", + status: "experimental", + }, + { + name: "CapabilityDelegationFullscreenRequest", + status: "stable", + }, + { + name: "CaptureHandle", + depends_on: ["GetDisplayMedia"], + status: {"Android": "", "default": "stable"}, + }, + { + name: "checkVisibility", + status: "stable", + }, + { + name: "ClickToCapturedPointer", + status: "experimental", + }, + { + name: "ClientHintsMetaEquivDelegateCH", + status: "stable", + base_feature: "ClientHintsMetaEquivDelegateCH", + }, + { + name: "ClientHintsMetaHTTPEquivAcceptCH", + status: "stable", + base_feature: "ClientHintsMetaHTTPEquivAcceptCH", + }, + { + name: "ClientHintThirdPartyDelegation", + status: "stable", + base_feature: "ClientHintThirdPartyDelegation", + }, + { + // Allows read/write of custom formats with unsanitized clipboard content. + // See crbug.com/106449. + name: "ClipboardCustomFormats", + status: "stable", + base_feature: "ClipboardCustomFormats", + }, + { + name: "ClipboardSvg", + status: "experimental", + }, + { + name: "ClipboardUnsanitizedContent", + status: "experimental", + }, + { + name: "CloseWatcher", + }, + { + name: "CLSScrollAnchoring", + status: "stable", + base_feature: "CLSScrollAnchoring", + }, + { + name: "CoepReflection", + status: "test", + }, + { + name: "CompositeBGColorAnimation", + public: true, + status: "experimental", + }, + { + name: "CompositeBoxShadowAnimation", + }, + { + name: "CompositeClipPathAnimation", + }, + { + name: "CompositedSelectionUpdate", + public: true, + status: {"Android": "stable"}, + }, + { + name: "ComputedAccessibilityInfo", + status: "experimental", + }, + { + // blink::features::kComputePressure is a kill switch for the API. If the + // feature is disabled, origin trial tokens are ignored. + name: "ComputePressure", + origin_trial_feature_name: "ComputePressure", + status: "experimental", + base_feature: "ComputePressure", + base_feature_status: "enabled", + copied_from_base_feature_if: "overridden", + }, + { + name: "ConditionalFocus", + origin_trial_feature_name: "ConditionalFocus", + status: {"Android": "", "default": "experimental"}, + }, + { + name: "ConsolidatedMovementXY", + public: true, + }, + { + name: "ContactsManager", + status: {"Android": "stable", "default": "test"}, + }, + { + name: "ContactsManagerExtraProperties", + status: {"Android": "stable", "default": "test"}, + }, + { + name: "ContentIndex", + status: {"Android": "stable", "default": "experimental"}, + }, + { + name: "ContentVisibilityAutoStateChangedEvent", + status: "stable", + }, + { + name: "ContextMenu", + status: "experimental", + }, + { + name: "CooperativeScheduling", + public: true, + }, + { + name: "CorsRFC1918", + }, + { + name: "CrossOriginOpenerPolicyReporting", + origin_trial_feature_name: "CrossOriginOpenerPolicyReporting", + origin_trial_allows_third_party: true, + status: "experimental", + }, + { + // Allows positioning a positioned element relative to another one. + // https://tabatkins.github.io/specs/css-anchor-position/ + name: "CSSAnchorPositioning", + status: "experimental", + }, + { + // Whether values are allowed as counter style + name: "CSSAtRuleCounterStyleImageSymbols", + }, + { + // https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as + name: "CSSAtRuleCounterStyleSpeakAsDescriptor", + status: "test", + }, + { + // https://drafts.csswg.org/css-conditional-4/#support-definition-ext + // https://github.com/w3c/csswg-drafts/issues/7280#issuecomment-1143852187 + name: "CSSAtSupportsAlwaysNonForgivingParsing", + status: "experimental", + }, + { + // Support CSS Values Level 4 calc simplification and serialization + // as specified in the specs below. + // https://drafts.csswg.org/css-values-4/#calc-simplification + // https://drafts.csswg.org/css-values-4/#calc-serialize + name: "CSSCalcSimplificationAndSerialization" + }, + { + // Support case-sensitive attribute selector modifier + // https://drafts.csswg.org/selectors-4/#attribute-case + name: "CSSCaseSensitiveSelector", + status: "test", + }, + { + // Support for CSS Color Module Level 4 + // https://www.w3.org/TR/css-color-4/ + name: "CSSColor4", + status: "experimental", + }, + { + name: "CSSColorContrast", + status: "experimental", + }, + { + name: "CSSColorTypedOM", + status: "experimental", + }, + { + // Flag for supporting size and inline-size container-types along with the + // @container rules for size queries. Also implies support for container + // relative units and inline-size containment. + // https://drafts.csswg.org/css-contain-3/#container-queries + name: "CSSContainerQueries", + status: "stable" + }, + { + // https://drafts.csswg.org/css-contain-3/#container-lengths + name: "CSSContainerRelativeUnits", + implied_by: ["CSSContainerQueries"] + }, + { + // With this flag disabled, we run UpdateStyle() over the full DOM tree + // before running interleaved style/layout. With this flag enabled, we + // skip style recalc in UpdateStyle() for containers which are guaranteed + // to be reachable in the following layout. + name: "CSSContainerSkipStyleRecalc", + implied_by: ["CSSContainerQueries"] + }, + { + // Support for contain:inline-size + name: "CSSContainSize1D", + implied_by: ["CSSContainerQueries"] + }, + { + // Include custom properties in CSSComputedStyleDeclaration::item/length. + // https://crbug.com/949807 + name: "CSSEnumeratedCustomProperties", + status: "test", + }, + { + name: "CSSFocusVisible", + status: "stable", + }, + { + name: "CSSFoldables", + status: "experimental", + }, + { + // @font-face Font property descriptors auto range. + // https://www.w3.org/TR/css-fonts-4/#font-prop-desc + name: "CSSFontFaceAutoVariableRange", + status: "test", + }, + { + name: "CSSFontFaceSrcTechParsing", + status: "experimental" + }, + { + name: "CSSFontFamilyMath", + status: "experimental", + implied_by: ["MathMLCore"], + }, + { + name: "CSSFontSizeAdjust", + status: "test", + }, + { + name: "CSSGridTemplatePropertyInterpolation", + depends_on: ["LayoutNGGridFragmentation"], + status: "stable", + }, + { + // This needs to be kept as a runtime flag as long as we need to forcibly + // disable it for WebView on Android versions older than P. See + // https://crrev.com/f311a84728272e30979432e8474089b3db3c67df + name: "CSSHexAlphaColor", + status: "stable", + }, + { + name: "CSSIcUnit", + status: "stable", + }, + { + name: "CSSIndependentTransformProperties", + status: "stable", + }, + { + name: "CSSLastBaseline", + status: "experimental", + }, + { + name: "CSSLayoutAPI", + status: "experimental", + }, + { + name: "CSSLhUnit", + status: "experimental", + }, + { + name: "CSSLogical", + status: "experimental", + }, + { + name: "CSSLogicalOverflow", + status: "test", + }, + { + name: "CSSMarkerNestedPseudoElement", + status: "experimental", + }, + { + name: "CSSMathDepth", + status: "experimental", + implied_by: ["MathMLCore"], + }, + { + name: "CSSMathShift", + status: "experimental", + implied_by: ["MathMLCore"], + }, + { + name: "CSSMathStyle", + status: "experimental", + implied_by: ["MathMLCore"], + }, + { + name: "CSSMathVariant", + status: "experimental", + implied_by: ["MathMLCore"], + }, + { + // Support for Media Queries Level 4 syntax. + // + // https://drafts.csswg.org/mediaqueries-4/#mq-syntax + name: "CSSMediaQueries4", + status: "stable", + }, + { + name: "CSSMixBlendModePlusLighter", + status: "stable" + }, + { + name: "CSSObjectViewBox", + status: "stable", + }, + { + name: "CSSOffsetPathRay", + status: "experimental", + }, + { + name: "CSSOffsetPathRayContain", + status: "experimental", + }, + { + name: "CSSOffsetPositionAnchor", + status: "experimental", + }, + { + name: "CSSOverflowForReplacedElements", + status: "test", + base_feature: "CSSOverflowForReplacedElements", + copied_from_base_feature_if: "overridden", + }, + { + name: "CSSPaintAPIArguments", + status: "experimental", + }, + { + name: "CSSPictureInPicture", + status: "experimental", + depends_on: ["PictureInPictureAPI"], + }, + { + name: "CSSPositionStickyStaticScrollPosition", + status: "experimental", + }, + { + // Match input elements which have been autofilled by user agent. + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + name: "CSSPseudoAutofill", + status: "experimental", + }, + { + name: "CSSPseudoDir", + status: "experimental", + }, + { + name: "CSSPseudoHas", + status: "stable", + }, + { + // When an audio, video, or similar resource is "playing" + // or "paused". + // https://www.w3.org/TR/selectors-4/#video-state + name: "CSSPseudoPlayingPaused", + status: "test", + }, + { + // https://drafts.csswg.org/css-cascade-6/#scoped-styles + name: "CSSScope", + status: "experimental", + }, + // Scrollbar styling. + // https://drafts.csswg.org/css-scrollbars/ + { + name: "CSSScrollbars", + status: "test", + }, + // Support for scroll-timeline. + // + // https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand + { + name: "CSSScrollTimeline", + status: "experimental", + }, + { + name: "CSSSelectorFragmentAnchor", + status: "experimental", + base_feature: "CssSelectorFragmentAnchor", + }, + { + name: "CSSSnapSize", + status: "experimental", + }, + { + // Support for CSS ::spelling-error, ::grammar-error, and the + // spelling-error and grammar-error values in text-decoration-line. + // + // https://drafts.csswg.org/css-pseudo-4/#selectordef-spelling-error + // https://drafts.csswg.org/css-pseudo-4/#selectordef-grammar-error + // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-line-spelling-error + // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-line-grammar-error + name: "CSSSpellingGrammarErrors", + status: "experimental", + }, + { + // https://drafts.csswg.org/css-contain-3/#style-container + name: "CSSStyleQueries", + status: "experimental" + }, + // Support for CSS Toggles, https://tabatkins.github.io/css-toggle/ + { + name: "CSSToggles", + status: "test", + }, + { + // Explainer: https://github.com/DevSDK/css-trigonometric-functions + name: "CSSTrigonometricFunctions", + status: "test", + }, + // Support for registered custom properties with syntax. + { + name: "CSSVariables2ImageValues", + status: "test", + }, + // Support for registered custom properties with and + // syntax. + { + name: "CSSVariables2TransformValues", + status: "test", + }, + { + name: "CSSVideoDynamicRangeMediaQueries", + status: "experimental" + }, + { + // Support for viewport units added by css-values-4. + // + // https://drafts.csswg.org/css-values-4/#viewport-relative-units + name: "CSSViewportUnits4", + status: "experimental", + }, + // Support for view-timeline. + // + // https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand + { + name: "CSSViewTimeline", + status: "test", + }, + { + name: "CustomElementDefaultStyle", + status: "experimental", + }, + { + name: "Database", + public: true, + status: "stable", + }, + { + name: "DecodeJpeg420ImagesToYUV", + public: true, + status: "stable", + }, + { + name: "DecodeLossyWebPImagesToYUV", + public: true, + status: "stable", + }, + { + // crbug.com/1259085 + name: "DeferredShaping", + depends_on: ["LayoutNG"], + status: "test", + base_feature: "DeferredShaping", + }, + { + name: "DeflateRawCompressionFormat", + status: "stable", + }, + { + name: "DelegatedInkTrails", + status: "stable", + }, + // https://github.com/w3c/resource-timing/pull/343 + { + name: "DeliveryType", + status: "experimental", + }, + { + name: "DesktopCaptureDisableLocalEchoControl", + status: "experimental", + }, + { + name: "DesktopPWAsSubApps", + status: "test", + base_feature: "DesktopPWAsSubApps", + }, + { + name: "DeviceAttributes", + origin_trial_feature_name: "DeviceAttributes", + origin_trial_os: ["chromeos"], + origin_trial_allows_third_party: true, + status: "experimental", + }, + { + name: "DeviceOrientationRequestPermission", + status: "experimental", + }, + { + name: "DevicePosture", + public: true, + status: "experimental", + }, + { + name: "DigitalGoods", + origin_trial_feature_name: "DigitalGoodsV2", + origin_trial_os: ["android", "chromeos"], + public: true, + status: { + "Android": "stable", + "ChromeOS_Ash": "stable", + "ChromeOS_Lacros": "stable", + // crbug.com/1143079: Web tests cannot differentiate ChromeOS and Linux, + // so enable the API on all platforms for testing. + "default": "test" + }, + }, + { + name: "DigitalGoodsV2_1", + status: { + "Android": "stable", + "ChromeOS_Ash": "stable", + "ChromeOS_Lacros": "stable", + // crbug.com/1143079: Web tests cannot differentiate ChromeOS and Linux, + // so enable the API on all platforms for testing. + "default": "test" + }, + }, + { + name: "DirectSockets", + public: true, + status: "experimental", + }, + { + name: "DisableDifferentOriginSubframeDialogSuppression", + origin_trial_feature_name: "DisableDifferentOriginSubframeDialogSuppression", + origin_trial_type: "deprecation", + origin_trial_allows_insecure: true, + }, + { + name: "DisableHardwareNoiseSuppression", + origin_trial_feature_name: "DisableHardwareNoiseSuppression", + status: "experimental", + }, + { + // When on, the display-capture permissions-policy is enforced. + // When off, enforcement of this permissions-policy is skipped. + // This is driven from the browser process by the Enterprise policy + // called "DisplayCapturePermissionsPolicyEnabled". + // TODO(crbug.com/1233969): Remove this around m100. + name: "DisplayCapturePermissionsPolicy", + public: true, + }, + { + name: "DisplayCutoutAPI", + public: true, + settable_from_internals: true, + }, + { + // When a Web application calls getDisplayMedia() and asks for video, + // allow a hint to be provided as to whether it prefers that a + // certain surface type be more prominently offered to the user. + // + // https://github.com/w3c/mediacapture-screen-share/pull/186/files + name: "DisplaySurfaceConstraint", + status: "stable", + }, + { + name: "DocumentCookie", + }, + { + name: "DocumentDomain", + }, + { + name: "DocumentPictureInPictureAPI", + depends_on: ["PictureInPictureAPI"], + public: true, + settable_from_internals: true, + }, + { + name: "DocumentPolicy", + public: true, + status: "stable", + }, + // Enables the ability to use Document Policy header to control feature + // DocumentDomain. + { + name: "DocumentPolicyDocumentDomain", + status: "experimental", + depends_on: ["DocumentPolicy"], + }, + { + name: "DocumentPolicyNegotiation", + origin_trial_feature_name: "DocumentPolicyNegotiation", + public: true, + status: "experimental", + depends_on: ["DocumentPolicy"], + }, + // Enables the ability to use Document Policy header to control feature + // SyncXHR. + { + name: "DocumentPolicySyncXHR", + status: "experimental", + depends_on: ["DocumentPolicy"], + }, + { + // Document transitions, including shared element transitions. + // See https://github.com/WICG/shared-element-transitions + name: "DocumentTransition", + base_feature: "DocumentTransition", + }, + { + name: "DocumentWrite", + }, + { + name: "EarlyHintsPreloadForNavigationOptIn", + origin_trial_feature_name: "EarlyHintsPreloadForNavigation", + status: "stable", + }, + { + // If enabled, allows web pages to use the experimental EditContext API to + // better control text input. See crbug.com/999184. + name: "EditContext", + status: "experimental", + base_feature: "EditContext", + }, + { + name: "ElementSuperRareData", + status: "experimental", + base_feature: "ElementSuperRareData", + }, + { + // Non-standard API Event.path. Should be replaced by Event.composedPath. + // TODO(1277431): This flag should be eventually disabled. + name: "EventPath", + public: true, + status: "stable", + base_feature: "EventPath", + }, + { + name: "ExperimentalContentSecurityPolicyFeatures", + status: "experimental", + }, + { + name: "ExperimentalJSProfilerMarkers", + status: "experimental" + }, + { + name: "ExperimentalPolicies", + depends_on: ["DocumentPolicy"], + status: "experimental" + }, + // Prototype implementation of web snapshots - blink integration. Suitable + // for local testing, *not* suitable for origin trials. Replacing the + // current implementation with the real Blink integration is expected + // before M102. See crbug.com/1173534. + { + name: "ExperimentalWebSnapshots", + }, + { + name: "ExtendedTextMetrics", + status: "experimental", + }, + { + name: "ExtraWebGLVideoTextureMetadata", + }, + { + name: "EyeDropperAPI", + status: { + // EyeDropper UI is available only on Windows, Mac, and Linux. This list + // should match the supported operating systems for the kEyeDropper + // base::Feature. + "Mac": "stable", + "Win": "stable", + "Linux": "stable", + }, + }, + { + name: "FaceDetector", + status: "experimental", + }, + { + name: "FakeNoAllocDirectCallForTesting", + status: "test", + }, + { + name: "FeaturePolicyReporting", + status: "experimental" + }, + { + name: "FedCm", + origin_trial_feature_name: "FedCM", + origin_trial_allows_third_party: true, + public: true, + status: "test", + }, + { + name: "FedCmIdpSigninStatus", + public: true, + status: "test", + }, + { + name: "FedCmIdpSignout", + public: true, + status: "test", + }, + { + name: "FedCmIframeSupport", + public: true, + status: "test", + }, + { + name: "FedCmMultipleIdentityProviders", + depends_on: ["FedCm"], + public: true, + status: "test", + }, + { + name: "FencedFrames", + // This helps enable and expose the element, but note that + // blink::features::kFencedFrames must be enabled as well, similar to + // Portals, as we require the support of the browser process to fully + // enable the feature. Enabling this runtime enabled feature alone has no + // effect. + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + public: true, + }, + { + name: "FetchUploadStreaming", + status: "stable", + }, + { + // Also enabled when blink::features::kFileHandlingAPI is overridden + // on the command line (or via chrome://flags). + name: "FileHandling", + depends_on: ["FileSystemAccessLocal"], + status: {"Android": "test", "default": "stable"}, + origin_trial_feature_name: "FileHandling", + origin_trial_os: ["win", "mac", "linux", "fuchsia", "chromeos"], + base_feature: "FileHandlingAPI", + }, + { + name: "FileHandlingIcons", + depends_on: ["FileHandling"], + status: {"Android": "test", "default": "experimental"}, + }, + { + name: "FileSystem", + public: true, + status: "stable", + }, + { + // Shared objects by OPFS and non-OPFS File System Access API. + name: "FileSystemAccess", + implied_by: ["FileSystemAccessLocal", "FileSystemAccessOriginPrivate"], + }, + { + name: "FileSystemAccessAccessHandle", + status: "stable", + }, + { + // In-development features for the File System Access API. + name: "FileSystemAccessAPIExperimental", + status: "experimental", + }, + { + // Non-OPFS File System Access API. + name: "FileSystemAccessLocal", + status: {"Android": "test", "default": "stable"}, + }, + { + // OPFS File System Access API. + name: "FileSystemAccessOriginPrivate", + status: {"Android": "experimental", "default": "stable"}, + }, + { + // Whether to re-enable async interface for FileSystemSyncAccessHandle + // This is used by enterprise policy during migration. + name: "FileSystemSyncAccessHandleAsyncInterfaceOverride", + public: true, + }, + { + name: "FirstPartySets", + }, + { + name: "FixedElementsDontOverscroll", + status: "stable", + base_feature: "FixedElementsDontOverscroll", + }, + { + name: "Fledge", + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + }, + { + name: "Focusgroup", + status: "experimental", + origin_trial_feature_name: "Focusgroup", + }, + { + name: "FocuslessSpatialNavigation", + settable_from_internals: true, + }, + { + name: "FontAccess", + base_feature: "FontAccess", + status: {"Android": "", "default": "stable"}, + }, + { + name: "FontPalette", + status: "stable", + }, + { + name: "FontSrcLocalMatching", + // No status, as the web platform runtime enabled feature is controlled by + // a Chromium level feature. + }, + { + // TODO(crbug.com/1231644): This flag is being kept (even though the + // feature has shipped) until there are settings to allow users to + // customize the feature. + name: "ForcedColors", + public: true, + status: "stable", + }, + { + name: "ForcedColorsPreserveParentColor", + status: "stable", + }, + { + // This is used in tests to perform memory measurement without + // waiting for GC. + name:"ForceEagerMeasureMemory", + }, + { + // https://github.com/flackr/reduce-motion/blob/main/explainer.md + name: "ForceReduceMotion", + }, + { + name: "ForceTallerSelectPopup", + status: {"ChromeOS_Ash": "stable", "ChromeOS_Lacros": "stable"}, + }, + { + name: "FormattedText", + depends_on: ["LayoutNG"], + status: "experimental", + }, + { + name: "FormRelAttribute", + status: "stable", + base_feature: "FormRelAttribute", + }, + { + name: "FractionalScrollOffsets", + implied_by: ["PercentBasedScrolling"], + public: true, + }, + { + name: "FreezeFramesOnVisibility", + status: "experimental", + }, + { + name: "GamepadButtonAxisEvents", + status: "experimental", + }, + { + name: "GetDisplayMedia", + public: true, + status: { + "Android": "experimental", + "default": "stable", + }, + }, + { + // Enables the getDisplayMediaSet API for multi surface capture. + name: "GetDisplayMediaSet", + depends_on: ["GetDisplayMedia"], + public: true, + status: "test", + }, + { + name: "GetDisplayMediaSetAutoSelectAllScreens", + depends_on: ["GetDisplayMediaSet"], + public: true, + status: { + "ChromeOS_Ash": "test", + "ChromeOS_Lacros": "test", + "default": "", + } + }, + { + name: "GroupEffect", + status: "test", + }, + { + name: "HandwritingRecognition", + status: { + "ChromeOS_Ash": "stable", + "ChromeOS_Lacros": "stable", + "default": "experimental", + }, + }, + { + name: "HidDeviceForget", + depends_on: ["WebHID"], + status: {"Android": "", "default": "stable"}, + }, + // HighlightAPI's custom highlights use HighlightInheritance's behavior even + // if the HighlightInheritance feature is not enabled. + { + name: "HighlightAPI", + status: "stable", + }, + { + name: "HighlightInheritance", + }, + { + name: "HighlightOverlayPainting", + status: "stable", + }, + { + name: "HighlightPointerEvents", + depends_on: ["HighlightAPI"], + }, + { + name: "HrefTranslate", + depends_on: ["TranslateService"], + origin_trial_feature_name: "HrefTranslate", + status: "stable", + }, + { + // TODO(crbug.com/1315717): This flag is being used to deprecate support for urls within elements. This feature is controlled by blink::features::kHTMLParamElementUrlSupport. + name: "HTMLParamElementUrlSupport", + status: "stable", + base_feature: "HTMLParamElementUrlSupport", + }, + { + // TODO(crbug.com/1307772): Enables the Pop-up API. + name: "HTMLPopupAttribute", + status: "experimental", + origin_trial_feature_name: "HTMLPopupAttribute", + implied_by: ["HTMLSelectMenuElement"], + base_feature: "HTMLPopupAttribute", + }, + { + name: "HTMLSelectMenuElement", + status: "experimental", + }, + { + name: "IDBBatchGetAll", + status:"experimental", + }, + { + name: "IdentityInCanMakePaymentEventFeature", + origin_trial_allows_third_party: true, + origin_trial_feature_name: "IdentityInCanMakePaymentEventFeature", + origin_trial_type: "deprecation", + public: true, + status: "stable", + }, + { + name: "IdleDetection", + public: true, + status: "stable", + }, + { + name: "ImplicitRootScroller", + public: true, + settable_from_internals: true, + status: {"Android": "stable"}, + }, + { + name: "InertAttribute", + status: "stable", + }, + { + // If a painting bug no longer reproduces with this feature enabled, then + // the bug is caused by incorrect cull rects. + name: "InfiniteCullRect", + }, + { + name: "InputMultipleFieldsUI", + // No plan to support complex UI for date/time INPUT types on Android. + status: {"Android": "test", "default": "stable"}, + }, + { + name: "InstalledApp", + public: true, + status: "stable", + }, + { + name: "KeyboardAccessibleTooltip", + status: "experimental", + }, + { + name: "KeyboardFocusableScrollers", + status: "experimental", + }, + { + name: "LangAttributeAwareFormControlUI", + settable_from_internals: true, + }, + { + // LayoutNG has been enabled in M76, but we still keep this flag for + // testing. See web_tests/FlagExpectations/disable-layout-ng for more + // details about running web tests with LayoutNG disabled. This flag also + // provides a convenient way for testing legacy layout code path in blink + // unit tests. + name: "LayoutNG", + implied_by: ["BidiCaretAffinity"], + status: "stable", + base_feature: "LayoutNG", + }, + { + name: "LayoutNGBlockFragmentation", + status: "stable", + }, + { + name: "LayoutNGBlockInInline", + depends_on: ["LayoutNG"], + status: "stable", + }, + { + // A kill-switch for a fix of crbug.com/1366268. + name: "LayoutNGDelayScrollOffsetClamping", + base_feature: "LayoutNGDelayScrollOffsetClamping", + status: "stable", + }, + { + // Block fragmentation support in the flex layout algorithm. + // https://drafts.csswg.org/css-flexbox-1/#pagination + name: "LayoutNGFlexFragmentation", + depends_on: ["LayoutNG", "LayoutNGBlockFragmentation"], + status: "stable", + }, + { + // crbug.com/989916 + name: "LayoutNGForeignObject", + depends_on: ["LayoutNG"], + status: "stable", + }, + { + // crbug.com/1346221 + name: "LayoutNGFrameSet", + depends_on: ["LayoutNG"], + status: "stable", + }, + { + // Block fragmentation support in the grid layout algorithm. + // https://drafts.csswg.org/css-grid-1/#pagination + name: "LayoutNGGridFragmentation", + depends_on: ["LayoutNG","LayoutNGBlockFragmentation"], + status: "stable", + }, + { + name: "LayoutNGPrinting", + depends_on: ["LayoutNG","LayoutNGBlockFragmentation"], + status: "stable", + base_feature: "LayoutNGPrinting", + }, + { + name: "LayoutNGSubgrid", + depends_on: ["LayoutNG"], + status: "test" + }, + { + // Block fragmentation support in the table layout algorithm. + // https://drafts.csswg.org/css-tables-3/#fragmentation + name: "LayoutNGTableFragmentation", + depends_on: ["LayoutNG","LayoutNGBlockFragmentation"], + status: "stable", + }, + { + // crbug.com/1335309 + name: "LayoutNGVTTCue", + status: "stable", + }, + { + name: "LazyFrameLoading", + public: true, + status: "stable", + }, + { + name: "LazyFrameVisibleLoadTimeMetrics", + public: true, + }, + { + name: "LazyImageLoading", + public: true, + status: "stable", + }, + { + name: "LazyImageVisibleLoadTimeMetrics", + public: true, + }, + { + name: "LazyInitializeMediaControls", + public: true, + // This is enabled by features::kLazyInitializeMediaControls. + }, + { + name: "LCPAnimatedImagesWebExposed", + status: "test", + }, + { + name: "LegacyWindowsDWriteFontFallback", + // Enabled by features::kLegacyWindowsDWriteFontFallback; + }, + { + name: "MachineLearningCommon", + implied_by: ["MachineLearningModelLoader", "MachineLearningNeuralNetwork"], + status: "experimental", + }, + { + name: "MachineLearningModelLoader", + status: "experimental", + }, + { + name: "MachineLearningNeuralNetwork", + status: "test", + }, + { + name: "ManagedConfiguration", + status: "stable", + base_feature: "ManagedConfiguration", + }, + { + name: "MathMLCore", + status:"experimental", + depends_on: ["LayoutNG"], + }, + { + name:"MeasureMemory", + status:"stable", + }, + { + name: "MediaCapabilitiesDynamicRange", + status: "test", + }, + { + name: "MediaCapabilitiesEncodingInfo", + status: "experimental", + }, + { + name: "MediaCapabilitiesSpatialAudio", + status: "test", + }, + { + name: "MediaCapture", + status: {"Android": "stable"}, + }, + { + name: "MediaCaptureBackgroundBlur", + status: "experimental", + }, + // Set to reflect the MediaCastOverlayButton feature. + { + name: "MediaCastOverlayButton", + public: true, + }, + { + name: "MediaControlsExpandGesture", + public: true, + }, + { + name: "MediaControlsOverlayPlayButton", + public: true, + settable_from_internals: true, + status: {"Android": "stable"}, + }, + { + name: "MediaElementVolumeGreaterThanOne", + }, + // Set to reflect the kMediaEngagementBypassAutoplayPolicies feature. + { + name: "MediaEngagementBypassAutoplayPolicies", + public: true, + }, + { + name: "MediaLatencyHint", + status: "test", + }, + { + name: "MediaQueryNavigationControls", + }, + { + name: "MediaSession", + public: true, + status: "stable", + }, + { + name: "MediaSessionWebRTC", + public: true, + }, + { + name: "MediaSourceExperimental", + status: "experimental", + }, + { + name: "MediaSourceExtensionsForWebCodecs", + status: "experimental", + origin_trial_feature_name: "MediaSourceExtensionsForWebCodecs", + }, + { + name: "MediaSourceInWorkers", + status: "stable", + origin_trial_feature_name: "MediaSourceInWorkers", + base_feature: "MediaSourceInWorkers", + }, + { + // Availability of this feature depends on MediaSourceInWorkers also + // being enabled. MediaSourceInWorkers is an OT feature, so this cannot + // use the "depends_on" key. + name: "MediaSourceInWorkersUsingHandle", + status: "stable", + base_feature: "MediaSourceInWorkersUsingHandle", + }, + { + name: "MediaSourceNewAbortAndDuration", + status: "experimental", + }, + { + // This is used in cases of mixed specification of stable and + // experimental MediaSource features, such as in the IDL for an interface + // constructor where exposure of the constructor in Window vs other + // contexts can vary in stable vs experimental. + name: "MediaSourceStable", + status: "stable", + }, + { + // Feature added "guarding" the already launched exposure of + // MediaStreamTracks in the Window contexts, to allow exposure in other + // contexts to be feature controlled, crbug.com/c/1290274. + name: "MediaStreamTrackInWindow", + status: "stable", + }, + { + name: "MediaStreamTrackInWorker", + }, + { + name: "MediaStreamTrackTransfer", + status: "test", + }, + // This is enabled by default on Windows only. The only part that's + // "experimental" is the support on other platforms. + { + name: "MiddleClickAutoscroll", + status: "test", + }, + { + + name: "MobileLayoutTheme", + }, + { + name: "MojoJS", + status: "test", + }, + // MojoJSTest is used exclusively in testing environments, whereas MojoJS + // may also be used elsewhere. + { + name: "MojoJSTest", + status: "test", + }, + { + // When enabled, iframes are not capturing mouse events by default. + name: "MouseSubframeNoImplicitCapture", + public: true, + }, + { + name: "NavigationApi", + status: "stable", + }, + { + name: "NavigationId", + status: "experimental", + }, + { + name: "NavigatorContentUtils", + // Android does not yet support NavigatorContentUtils. + status: {"Android": "", "default": "stable"}, + }, + { + name: "NetInfoDownlinkMax", + public: true, + // Only Android, ChromeOS support NetInfo downlinkMax, type and ontypechange now + status: { + "Android": "stable", + "ChromeOS_Ash": "stable", + "ChromeOS_Lacros": "stable", + "default": "experimental", + }, + }, + { + name: "NeverSlowMode", + public: true, + }, + { + name: "NewFlexboxSizing", + }, + { + name: "NoIdleEncodingForWebTests", + status: "test", + }, + { + name: "NotificationConstructor", + // Android won't be able to reliably support non-persistent notifications, the + // intended behavior for which is in flux by itself. + status: {"Android": "", "default": "stable"}, + }, + // NotificationContentImage is not available in all platforms + // The Notification Center on Mac OS X does not support content images. + { + name: "NotificationContentImage", + public: true, + status: {"Mac": "test", "default": "stable"}, + }, + { + name: "Notifications", + public: true, + status: "stable", + }, + { + name: "NotificationTriggers", + origin_trial_feature_name: "NotificationTriggers", + status: "experimental", + }, + { + name: "OffMainThreadCSSPaint", + status: "stable", + }, + { + name: "OffscreenCanvasCommit", + status: "experimental", + }, + { + // TODO(crbug.com/920069): Remove OffsetParentNewSpecBehavior after the + // feature is in stable with no issues. + name: "OffsetParentNewSpecBehavior", + status: "experimental", + base_feature: "OffsetParentNewSpecBehavior", + }, + { + name: "OnDeviceChange", + // Android does not yet support SystemMonitor. + status: {"Android": "", "default": "stable"}, + }, + { + name: "OrientationEvent", + status: {"Android": "stable"}, + }, + { + name: "OriginIsolationHeader", + status: "stable", + }, + { + name: "OriginPolicy", + status: "experimental", + }, + + // Define a sample API for testing integration with the Origin Trials + // Framework. The sample API is used in both unit and web tests for the + // Origin Trials Framework. Do not change this flag to stable, as it exists + // solely to generate code used by the sample API implementation. + { + name: "OriginTrialsSampleAPI", + origin_trial_feature_name: "Frobulate", + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + // TODO(yashard): Add tests for this feature. + { + name: "OriginTrialsSampleAPIDependent", + depends_on: ["OriginTrialsSampleAPI"], + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIDeprecation", + origin_trial_feature_name: "FrobulateDeprecation", + origin_trial_type: "deprecation", + origin_trial_allows_insecure: true, + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIExpiryGracePeriod", + origin_trial_feature_name: "FrobulateExpiryGracePeriod", + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIExpiryGracePeriodThirdParty", + origin_trial_feature_name: "FrobulateExpiryGracePeriodThirdParty", + origin_trial_allows_third_party: true, + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIImplied", + origin_trial_feature_name: "FrobulateImplied", + implied_by: ["OriginTrialsSampleAPI", "OriginTrialsSampleAPIInvalidOS"], + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIInvalidOS", + origin_trial_feature_name: "FrobulateInvalidOS", + origin_trial_os: ["invalid"], + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPINavigation", + origin_trial_feature_name: "FrobulateNavigation", + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIPersistentExpiryGracePeriod", + origin_trial_feature_name: "FrobulatePersistentExpiryGracePeriod", + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIPersistentFeature", + origin_trial_feature_name: "FrobulatePersistent", + }, + // As above. Do not change this flag to stable, as it exists solely to + // generate code used by the origin trials sample API implementation. + { + name: "OriginTrialsSampleAPIThirdParty", + origin_trial_feature_name: "FrobulateThirdParty", + origin_trial_allows_third_party: true, + }, + { + name: "OverscrollCustomization", + settable_from_internals: true, + status: "experimental", + }, + // The following are developer opt-outs and opt-ins for page freezing. If + // neither is specified then heuristics will be applied to determine whether + // the page is eligible. + { + name: "PageFreezeOptIn", + origin_trial_feature_name: "PageFreezeOptIn", + }, + { + name: "PageFreezeOptOut", + origin_trial_feature_name: "PageFreezeOptOut", + }, + { + name: "PagePopup", + // Android does not have support for PagePopup + status: {"Android": "", "default": "stable"}, + }, + { + name: "PaintUnderInvalidationChecking", + settable_from_internals: true, + }, + { + // PARAKEET ad serving runtime flag/JS API. + name: "Parakeet", + origin_trial_feature_name: "Parakeet", + base_feature: "Parakeet", + }, + { + name: "ParallelPrimaryFont", + depends_on: ["ParallelTextShaping"], + // http://crbug.com/1315900 + // Global DeviceScaleFactor prevents us to use this feature. + status: {"ChromeOS_Ash": "", "ChromeOS_Lacros": "", "Linux": "", + "default": ""}, + }, + { + name: "ParallelTextShaping", + depends_on: ["LayoutNG"], + // http://crbug.com/1264280 + }, + { + name: "PartitionedCookies", + origin_trial_feature_name: "PartitionedCookies", + status: "experimental", + origin_trial_feature_name: "PartitionedCookies", + origin_trial_allows_third_party: true, + }, + // This is to add an option to enable the Reveal button on password inputs while waiting ::reveal gets standardized. + { + name: "PasswordReveal", + }, + { + name: "PaymentApp", + depends_on: ["PaymentRequest"], + public: true, + status: "experimental", + }, + { + name: "PaymentMethodChangeEvent", + depends_on: ["PaymentRequest"], + status: "stable", + }, + // PaymentRequest is enabled by default on Android + { + name: "PaymentRequest", + public: true, + status: "experimental", + }, + { + name: "PaymentRequestMerchantValidationEvent", + status: "experimental", + }, + { + name: "PendingBeaconAPI", + origin_trial_feature_name: "PendingBeaconAPI", + origin_trial_allows_third_party: true, + public: true, + status: "experimental", + }, + { + name: "PercentBasedScrolling", + public: true, + settable_from_internals: true, + }, + { + name: "PerformanceManagerInstrumentation", + public: true, + }, + { + name: "PeriodicBackgroundSync", + public: true, + status: "stable", + }, + { + name: "PerMethodCanMakePaymentQuota", + origin_trial_feature_name: "PerMethodCanMakePaymentQuota", + status: "experimental", + }, + { + name: "Permissions", + public: true, + status: "stable", + }, + // See https://crbug.com/1324111 + { + name: "PermissionsPolicyUnload", + origin_trial_feature_name: "PermissionsPolicyUnload", + status: "experimental", + base_feature: "PermissionsPolicyUnload", + }, + { + name: "PermissionsRequestRevoke", + status: "experimental", + }, + { + name: "PictureInPicture", + public: true, + settable_from_internals: true, + }, + { + name: "PictureInPictureAPI", + public: true, + status: "stable" + }, + // This is a reverse OT used for a phased deprecation. + // https://crbug.com/918374 + { + name: "PNaCl", + origin_trial_feature_name: "PNaCl", + }, + { + name: "PointerLockOptions", + origin_trial_feature_name: "PointerLockOptions", + public: true, + status: "stable", + }, + { + name: "Portals", + // Portals must be enabled by blink::features::kPortals as we require the + // support of the browser process to enable the feature. Enabling this + // feature specifically within blink has no effect. An experimental/test + // status can only be set here if the default of blink::features::kPortals + // allows for it. + + // Note that the origin trial paramaters are kept for future OT usage. + // There is no currently running OT and it is not possible for OT code to + // enable portals without browser process support. + origin_trial_feature_name: "Portals", + origin_trial_os: ["android"], + public: true, + base_feature: "Portals", + copied_from_base_feature_if: "overridden", + }, + { + name: "PreciseMemoryInfo", + public: true, + }, + // Prefer not using composited scrolling. Composited scrolling will still + // be used if there are other reasons forcing compositing. For consistency, + // any code calling Settings::GetPreferCompositingToLCDTextEnabled() should + // ensure that this flag overrides the setting. + { + name: "PreferNonCompositedScrolling", + settable_from_internals: true, + }, + { + name: "PrefersColorSchemeClientHintHeader", + status: "stable", + base_feature: "PrefersColorSchemeClientHintHeader", + }, + { + name: "PrefersReducedData", + status: "experimental", + }, + { + name: "PrefersReducedMotionClientHintHeader", + status: "stable", + base_feature: "PrefersReducedMotionClientHintHeader", + }, + // TODO(crbug/695586): This flag is being used to deprecate support for + // legacy quota API window.webkitStorageInfo. + { + name: "PrefixedStorageInfo", + public: true, + status: "stable", + base_feature: "PrefixedStorageInfo", + base_feature_status: "disabled", + }, + // This feature is deprecated and we are evangelizing affected sites. + // See https://crbug.com/346236 for current status. + { + name: "PrefixedVideoFullscreen", + status: "stable", + }, + { + // https://crbug.com/1126305 + name: "Prerender2", + status: {"Android": "stable"}, + base_feature: "Prerender2", + }, + // This RTE feature is used to indicate the status of RTE feature Prerender2 + // and an embedder feature flag that enables the Prerender2. With this flag + // on, the related functions document.prerendering, + // document.onprerenderingchange, and activationStart will be exported. + { + name: "Prerender2RelatedFeatures", + implied_by: ["Prerender2"], + // This is not going into origin trial, "origin_trial_feature_name" is + // required for using the "implied_by" behaviour. + origin_trial_feature_name: "Prerender2RelatedFeatures__DONOTUSE", + public: true, + }, + { + name: "Presentation", + public: true, + status: "stable", + }, + { + name: "PriorityHints", + status: "stable", + }, + // The RTE feature encompasses multiple APIs, including: Attribution + // Reporting, FLEDGE, Topics and Fenced Frames. + { + name: "PrivacySandboxAdsAPIs", + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + }, + { + name: "PrivateNetworkAccessNonSecureContextsAllowed", + origin_trial_feature_name: "PrivateNetworkAccessNonSecureContextsAllowed", + origin_trial_type: "deprecation", + origin_trial_allows_insecure: true, + status: "experimental", + }, + { + name: "PrivateNetworkAccessPermissionPrompt", + base_feature: "PrivateNetworkAccessPermissionPrompt", + }, + { + name: "PushMessaging", + public: true, + status: "stable", + }, + { + name: "PushMessagingSubscriptionChange", + public: true, + status: "experimental", + }, + { + name: "QuickIntensiveWakeUpThrottlingAfterLoading", + base_feature: "QuickIntensiveWakeUpThrottlingAfterLoading", + }, + { + name: "QuotaChange", + status: "experimental", + }, + // If enabled, the minor version of the User-Agent string will be reduced. + // This User-Agent Reduction feature has been enabled starting from M101, + // but we still keep this flag for future phase tests. + { + name: "ReduceUserAgentMinorVersion", + status: "stable", + base_feature: "ReduceUserAgentMinorVersion", + }, + { + // If enabled, the platform and oscpu of the User-Agent string will be + // reduced. + name: "ReduceUserAgentPlatformOsCpu", + depends_on: ["ReduceUserAgentMinorVersion"], + status: "experimental", + base_feature: "ReduceUserAgentPlatformOsCpu", + }, + { + name: "RegionCapture", + status: {"Android": "", "default": "stable"}, + }, + { + name: "RemotePlayback", + public: true, + status: "stable", + }, + { + name: "RemotePlaybackBackend", + settable_from_internals: true, + // Tracking bug for the implementation: https://crbug.com/728609 + status: {"Android": "stable", "default": ""}, + }, + { + name: "RemoveMobileViewportDoubleTap", + public: true, + status: "stable" + }, + { + name: "RenderBlockingStatus", + status: "stable" + }, + { + // The renderpriority attribute feature. + // https://github.com/WICG/display-locking/blob/main/explainers/update-rendering.md + name: "RenderPriorityAttribute" + }, + { + name: "ResourceTimingResponseStatus", + status: "experimental", + }, + { + name: "RestrictGamepadAccess", + public: true, + status: "experimental", + }, + { + name: "RtcAudioJitterBufferMaxPackets", + origin_trial_feature_name: "RtcAudioJitterBufferMaxPackets", + status: "experimental", + }, + // Enables the use of the RTCIceTransport with extensions. + { + name: "RTCIceTransportExtension", + origin_trial_feature_name: "RTCQuicTransport", + status: "experimental", + }, + // Enables the use of the Insertable Streams legacy API. + // TODO(https://crbug.com/1119801): Remove when the origin trial ends. + { + name: "RTCInsertableStreams", + origin_trial_feature_name: "RTCInsertableStreams", + status: "experimental", + }, + // Enables the use of the RTCQuicTransport object. + { + name: "RTCQuicTransport", + origin_trial_feature_name: "RTCQuicTransport", + status: "experimental", + }, + // Enables the use of |RTCRtpTransceiver::setOfferedRtpHeaderExtensions|, + // |RTCRtpTransceiver::headerExtensionsToOffer|, and + // |RTCRtpTransceiver::headerExtensionsNegotiated|. + { + name: "RTCRtpHeaderExtensionControl", + status: "experimental", + }, + { + name: "RTCStatsRelativePacketArrivalDelay", + origin_trial_feature_name: "RTCStatsRelativePacketArrivalDelay", + status: "experimental", + }, + // Enables the use of SVC scalability mode in WebRTC. + // Spec: https://w3c.github.io/webrtc-svc/ + { + name: "RTCSvcScalabilityMode", + status: "experimental", + }, + { + name: "SanitizerAPI", + status: "experimental", + base_feature: "SanitizerAPI", + }, + { + name: "SanitizerAPIv0", + status: "stable", + implied_by: ["SanitizerAPI"], + base_feature: "SanitizerAPIv0", + }, + { + // https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries + name: "ScopedCustomElementRegistry", + status: "test", + }, + + // WebSpeech API with both speech recognition and synthesis functionality + // is not fully enabled on all platforms. + { + name: "ScriptedSpeechRecognition", + public: true, + status: "stable", + }, + { + name: "ScriptedSpeechSynthesis", + public: true, + status: "stable", + }, + { + name: "ScriptElementSupports", + status: "stable", + }, + { + name: "ScrollbarWidth", + status: "test", + }, + { + name: "ScrollCustomization", + }, + { + name: "ScrollEndEvents", + settable_from_internals: true, + status: "experimental", + }, + { + name: "ScrollTimeline", + status: "experimental", + implied_by: ["AnimationWorklet", "CSSScrollTimeline", "CSSViewTimeline"] + }, + // Implements documentElement.scrollTop/Left and bodyElement.scrollTop/Left + // as per the spec, matching other Web engines. + { + name: "ScrollTopLeftInterop", + status: "stable", + }, + { + // Whether to enable scroll update optimizations. See crbug.com/1346789. + name: "ScrollUpdateOptimizations", + base_feature: "ScrollUpdateOptimizations", + status: "stable", + }, + { + name: "SecureContextFixForSharedWorkers", + status: "stable", + }, + // SecurePaymentConfirmation has shipped on some platforms, but its + // availability is controlled by the browser process (via the + // SecurePaymentConfirmationBrowser feature), as it requires browser + // support to function. See //content/public/common/content_features.cc + // + // The status is set to 'test' here to enable some WPT tests that only + // require blink-side support to function. + { + name: "SecurePaymentConfirmation", + public: true, + status: "test", + }, + { + name: "SecurePaymentConfirmationDebug", + public: true, + }, + { + name: "SecurePaymentConfirmationOptOut", + origin_trial_feature_name: "SecurePaymentConfirmationOptOut", + origin_trial_allows_third_party: true + }, + { + // When a Web application calls getDisplayMedia() and asks for video, + // allow a hint to be provided as to whether the current tab should be + // excluded from the list of tabs offered to the user. + // + // https://github.com/w3c/mediacapture-screen-share/pull/216/files + name: "SelfBrowserSurfaceConstraint", + status: "stable", + }, + { + name: "SendBeaconThrowForBlobWithNonSimpleType", + public: true, + status: "stable", + }, + { + name: "SendFullUserAgentAfterReduction", + origin_trial_feature_name: "SendFullUserAgentAfterReduction", + origin_trial_allows_third_party: true + }, + { + name: "SendMouseEventsDisabledFormControls", + status: "experimental", + }, + { + name: "SensorExtraClasses", + public: true, + status: "experimental", + }, + { + name: "Serial", + status: {"Android": "", "default": "stable"}, + }, + { + name: "SerialPortForget", + status: {"Android": "", "default": "stable"}, + }, + { + name: "ServiceWorkerClientLifecycleState", + status: "experimental", + }, + { + name: "SharedArrayBuffer", + public: true, + }, + { + name: "SharedArrayBufferOnDesktop", + public: true, + }, + { + name: "SharedArrayBufferUnrestrictedAccessAllowed", + public: true, + }, + { + name: "SharedAutofill", + public: true, + status: "test", + }, + { + name: "SharedStorageAPI", + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + public: true, + }, + { + name: "SharedWorker", + public: true, + // Android does not yet support SharedWorker. crbug.com/154571 + status: {"Android": "", "default": "stable"}, + }, + { + name: "SignatureBasedIntegrity", + origin_trial_feature_name: "SignatureBasedIntegrity", + status: "experimental", + }, + { + name: "SiteInitiatedMirroring", + status: "experimental", + }, + { + name: "SkipAd", + depends_on: ["MediaSession"], + origin_trial_feature_name: "SkipAd", + status: "experimental", + }, + { + // Skips the browser touch event filter, ensuring that events that reach + // the queue and would otherwise be filtered out will instead be passed + // onto the renderer compositor process as long as the page hasn't timed + // out. If skip_filtering_process is browser_and_renderer, also skip the + // renderer cc touch event filter, ensuring that events will be passed + // onto the renderer main thread. + name: "SkipTouchEventFilter", + settable_from_internals: true, + base_feature: "SkipTouchEventFilter", + status: "stable", + }, + { + name: "SoftNavigationHeuristics", + status: "experimental", + depends_on: ["NavigationId"], + }, + // An origin trial feature name is required for this, even though it's + // actually enabled by the more specific trial. Having these as separate + // features will allow Blink to distinguish for which thing the trial + // was enabled. + { + name: "SpeculationRules", + implied_by: ["SpeculationRulesPrefetchProxy", "SpeculationRulesPrefetchWithSubresources", "Prerender2"], + origin_trial_feature_name: "SpeculationRules__DONOTUSE", + }, + // Origin trial to enable Speculation Rules for access to the prefetch proxy. + // https://crbug.com/1190167 + { + name: "SpeculationRulesPrefetchProxy", + origin_trial_feature_name: "SpeculationRulesPrefetch", + status: {"Android": "stable"}, + base_feature: "SpeculationRulesPrefetchProxy", + copied_from_base_feature_if: "overridden", + }, + { + "name": "SpeculationRulesPrefetchWithSubresources", + }, + { + name: "SrcsetMaxDensity", + }, + // Used as argument in attribute of stable-release functions/interfaces + // where a runtime-enabled feature name is required for correct IDL syntax. + // This is a global flag; do not change its status. + { + name: "StableBlinkFeatures", + status: "stable", + }, + { + // Enabled when blink::features::kStorageAccessAPI is enabled. + name: "StorageAccessAPI", + status: "test", + }, + { + name: "StorageAccessAPIForOriginExtension", + public: true, + status: "test", + base_feature: "StorageAccessAPIForOriginExtension", + }, + { + name: "StorageBuckets", + status: "experimental", + }, + { + name: "StorageFoundationAPI", + origin_trial_feature_name: "StorageFoundationAPI", + status: "experimental", + }, + { + name: "StrictMimeTypesForWorkers", + status: "experimental" + }, + { + name: "StylusHandwriting", + }, + { + name: "SubresourceWebBundles", + public: true, + status: "experimental" + }, + { + name: "SupportsFontFormatTech", + status: "experimental" + }, + { + // When a Web application calls getDisplayMedia() and asks for video, + // allow a hint to be provided to offer the user an option to + // dynamically switch the source display surface during the capture. + // + // https://github.com/w3c/mediacapture-screen-share/pull/225/files + name: "SurfaceSwitchingConstraint", + status: "stable", + }, + { + name: "SVGTextNG", + depends_on: ["LayoutNG"], + status: "stable" + }, + { + name: "SynthesizedKeyboardEventsForAccessibilityActions", + status: "experimental", + }, + { + // When a Web application calls getDisplayMedia() and asks for audio, + // allow a hint to be provided as to whether the Web application is + // interested in system-audio being among the options offered to the user. + // + // https://github.com/w3c/mediacapture-screen-share/pull/222/files + name: "SystemAudioConstraint", + status: "stable", + }, + { + name: "SystemWakeLock", + status: "experimental", + }, + // For unit tests. + { + name: "TestFeature", + }, + // For unit tests. + { + name: "TestFeatureDependent", + depends_on: ["TestFeatureImplied"], + }, + // For unit tests. + { + name: "TestFeatureImplied", + implied_by: ["TestFeature"], + }, + { + // crbug.com/1008951 + name: "TextDecoratingBox", + status: "stable", + }, + { + name: "TextDetector", + status: "experimental", + }, + { + name: "TextFragmentAPI", + status: "experimental", + }, + { + name: "TextFragmentIdentifiers", + origin_trial_feature_name: "TextFragmentIdentifiers", + public: true, + status: "stable", + base_feature: "TextFragmentAnchor", + }, + { + name: "TextFragmentTapOpensContextMenu", + status: {"Android": "stable"}, + }, + { + // Forces same-process display:none cross-origin iframes to be throttled + // in the same manner that OOPIFs are. + name: "ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes", + status: "experimental", + base_feature: "ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes", + }, + { + name: "TimerThrottlingForBackgroundTabs", + public: true, + status: "stable", + }, + { + name: "TimeZoneChangeEvent", + status: "experimental", + }, + { + name: "TopicsAPI", + origin_trial_feature_name: "PrivacySandboxAdsAPIs", + origin_trial_allows_third_party: true, + public: true, + }, + // https://crbug.com/1314739 + { + name: "TouchActionEffectiveAtPointerDown", + status: "stable", + }, + // This feature allows touch dragging and a context menu to occur + // simultaneously, with the assumption that the menu is non-modal. Without + // this feature, a long-press touch gesture can start either a drag or a + // context-menu in Blink, not both (more precisely, a context menu is shown + // only if a drag cannot be started). + { + name: "TouchDragAndContextMenu", + implied_by: ["TouchDragOnShortPress"], + public: true, + }, + // This feature makes touch dragging to occur at the short-press gesture, + // which occurs right before the long-press gesture. This feature assumes + // that TouchDragAndContextMenu is enabled. + { + name: "TouchDragOnShortPress", + }, + // Many websites disable mouse support when touch APIs are available. We'd + // like to enable this always but can't until more websites fix this bug. + // Chromium sets this conditionally (eg. based on the presence of a + // touchscreen) in ApplyWebPreferences. "Touch events" themselves are always + // enabled since they're a feature always supported by Chrome. + { + name: "TouchEventFeatureDetection", + origin_trial_feature_name: "ForceTouchEventFeatureDetectionForInspector", + public: true, + status: "stable", + }, + // This is conditionally set if the platform supports translation. + { + name: "TranslateService" + }, + { + name: "TrustedTypeBeforePolicyCreationEvent", + status: "experimental", + }, + { + name: "TrustedTypesUseCodeLike", + status: "experimental", + }, + { + name: "TrustTokens", + origin_trial_feature_name: "TrustTokens", + origin_trial_allows_third_party: true, + public: true, + status: "test", + }, + { + // Always allow trust token issuance (so long as the base::Feature + // is enabled). Used for testing; circumvents a runtime check that, + // if this RuntimeEnabledFeature is not present, guarantees the origin + // trial is enabled. + name: "TrustTokensAlwaysAllowIssuance", + public: true, + status: "test", + }, + // This is a reverse OT used for a phased deprecation of Cryptotoken, the + // component extension that implements the U2F security key API. + // https://crbug.com/1224886 + { + name: "U2FSecurityKeyAPI", + origin_trial_feature_name: "U2FSecurityKeyAPI", + origin_trial_os: ["win", "mac", "linux", "chromeos"], + origin_trial_type: "deprecation", + }, + { + name: "UnclosedFormControlIsInvalid", + status: "experimental", + }, + { + name: "UnexposedTaskIds", + }, + // This is a reverse OT used for a phased deprecation, on desktop + // https://crbug.com/1071424 + { + name: "UnrestrictedSharedArrayBuffer", + origin_trial_feature_name: "UnrestrictedSharedArrayBuffer", + origin_trial_os: ["win", "mac", "linux", "fuchsia", "chromeos"], + }, + { + name: "URLPatternCompareComponent", + status: "experimental", + }, + { + name: "UserActivationSameOriginVisibility", + public: true, + }, + { + name: "UserAgentClientHint", + status: "stable", + base_feature: "UserAgentClientHint", + }, + { + name: "UserAgentReduction", + origin_trial_feature_name: "UserAgentReduction", + origin_trial_allows_third_party: true, + // iOS not included as it should not send a reduced User-Agent string. + origin_trial_os: ["android", "chromeos", "fuchsia", "linux", "mac", "win"], + base_feature: "ReduceUserAgent", + }, + { + name: "V8IdleTasks", + public: true, + }, + { + name: "VariableCOLRV1", + base_feature: "VariableCOLRV1", + }, + { + // Whether a video element should automatically play fullscreen unless + // 'playsinline' is set. + name: "VideoAutoFullscreen", + settable_from_internals: true, + }, + { + name: "VideoFullscreenOrientationLock", + }, + { + name: "VideoPlaybackQuality", + public: true, + status: "stable", + }, + { + name: "VideoRotateToFullscreen", + }, + { + name: "VideoTrackGenerator", + status: "test", + }, + { + name: "VideoTrackGeneratorInWindow", + status: "test", + }, + { + name: "VideoTrackGeneratorInWorker", + }, + { + name: "VideoWakeLockOptimisationHiddenMuted", + public: true, + status: "stable", + }, + { + name: "ViewportHeightClientHintHeader", + status: "stable", + base_feature: "ViewportHeightClientHintHeader", + }, + { + name: "ViewportSegments", + status: "experimental", + }, + { + name: "VisibilityCollapseColumn", + }, + { + name: "VisibilityStateEntry", + status: "experimental", + }, + { + // The "WakeLock" feature was originally implied_by "ScreenWakeLock" and + // "SystemWakeLock". The former was removed after being promoted to + // stable, but we need to keep this feature around for code and IDLs that + // should work with both screen and system wake locks. + name: "WakeLock", + status: "stable", + implied_by: ["SystemWakeLock"], + }, + { + name: "WebAnimationsAPI", + status: "stable", + implied_by: ["AnimationWorklet"], + }, + { + name: "WebAnimationsSVG", + status: "experimental", + }, + { + name: "WebAppDarkMode", + origin_trial_feature_name: "WebAppDarkMode", + status: "experimental", + base_feature: "WebAppEnableDarkMode", + }, + { + name: "WebAppLaunchHandler", + origin_trial_feature_name: "Launch Handler", + status: "experimental", + base_feature: "WebAppEnableLaunchHandler", + }, + { + name: "WebAppLaunchQueue", + // This is not going into origin trial, "origin_trial_feature_name" is + // required for using the "implied_by" behaviour. + origin_trial_feature_name: "WebAppLaunchQueue", + implied_by: ["WebAppLaunchHandler", "FileHandling"], + }, + { + name:"WebAppsLockScreen", + status:"experimental", + }, + { + name: "WebAppTabStrip", + status: "experimental", + }, + { + name: "WebAppTranslations", + status: "experimental", + base_feature: "WebAppEnableTranslations", + }, + { + // This flag enables the Manifest parser to handle URL Handlers. + // Also enabled when blink::features::kWebAppEnableUrlHandlers is + // overridden on the command line (or via chrome://flags). + name: "WebAppUrlHandling", + status: "experimental", + origin_trial_feature_name: "WebAppUrlHandling", + origin_trial_os: ["win", "mac", "linux"], + }, + { + name: "WebAppWindowControlsOverlay", + origin_trial_feature_name: "WebAppWindowControlsOverlay", + origin_trial_os: ["win", "mac", "linux", "chromeos"], + status: "stable", + }, + { + name: "WebAssemblyCSP", + status: "stable", + }, + { + name: "WebAssemblyExceptions", + origin_trial_feature_name: "WebAssemblyExceptions", + status: "test", + }, + // WebAuth is disabled on Android versions prior to N (7.0) due to lack of + // supporting APIs, see runtime_features.cc. + { + name: "WebAuth", + status: "stable", + }, + // When enabled adds the authenticator attachment used for registration and + // authentication to the public key credential response. + { + name: "WebAuthAuthenticatorAttachment", + status: "test" + }, + // A more subtle UI for WebAuthn that is web exposed. + // It's controlled by the corresponding Chromium feature which needs to + // be enabled to make the whole feature work, except for WPTs where the + // virtual authenticator environment is used. + { + name: "WebAuthenticationConditionalUI", + status: "stable", + }, + // https://github.com/w3c/webauthn/pull/1663 + { + name: "WebAuthenticationDevicePublicKey", + status: "experimental", + }, + { + name: "WebAuthenticationLargeBlobExtension", + status: "experimental", + }, + { + name: "WebAuthenticationRemoteDesktopSupport", + public: true, + }, + // WebBluetooth is enabled by default on Android, ChromeOS and Mac. + // It is also supported in Windows 10 which is handled in runtime_features.cc + { + name: "WebBluetooth", + public: true, + status: { + "Android": "stable", + "ChromeOS_Ash": "stable", "ChromeOS_Lacros": "stable", + "Mac": "stable", + "default": "experimental", + }, + }, + { + name: "WebBluetoothGetDevices", + public: true, + status: "experimental", + }, + { + name: "WebBluetoothScanning", + status: "experimental", + }, + { + name: "WebBluetoothWatchAdvertisements", + public: true, + status: "experimental", + }, + { + name: "WebCodecs", + status: "stable", + origin_trial_feature_name: "WebCodecs" + }, + { + name: "WebCodecsDequeueEvent", + status: "stable", + }, + { + name: "WebGLColorManagement", + status: "stable", + }, + { + name: "WebGLDeveloperExtensions", + public: true, + status: "experimental", + }, + { + // Draft WebGL extensions are deliberately not enabled by experimental web + // platform features. + name: "WebGLDraftExtensions", + public: true, + }, + { + name: "WebGLImageChromium", + public: true, + }, + // WebGPU adds a large attack surface area to the GPU process and allows + // running arbitrary programs on the GPU (compute shaders), which may + // perform arbitrary read/writes of GPU memory if not properly sandboxed. + // That's why it is not enabled as part of the + // --enable-experimental-web-platform-features flag. + { + name: "WebGPU", + origin_trial_feature_name: "WebGPU", + public: true, + }, + { + // WebGPU developer features are deliberately not enabled by experimental + // web platform features. + name: "WebGPUDeveloperFeatures", + public: true, + }, + { + // Requires both WebGPU and --enable-experimental-web-platform-features + name: "WebGPUImportTexture", + status: "experimental", + }, + { + name: "WebHID", + status: {"Android": "", "default": "stable"}, + }, + { + name: "WebHIDExclusionFiltersOption", + status: "stable", + }, + { + // It is only enabled in extension environment for now. + name: "WebHIDOnServiceWorkers", + depends_on: ["WebHID"], + public: true, + }, + // Legacy ::-webkit-scrollbar* pseudo element styling. Enabled for stable + // but configurable via WebPreferences. + { + name: "WebKitScrollbarStyling", + status: "stable", + }, + { + name: "WebNFC", + public: true, + status: {"Android": "stable", "default": "test"}, + }, + { + name: "WebNFCMakeReadOnly", + depends_on: ["WebNFC"], + status: {"Android": "stable", "default": "test"}, + }, + { + name: "WebOTP", + public: true, + status: "stable", + }, + { + name: "WebOTPAssertionFeaturePolicy", + depends_on: ["WebOTP"], + public: true, + status: "stable", + }, + { + name: "WebPaymentAPICSP", + origin_trial_feature_name: "WebPaymentAPICSP", + origin_trial_allows_third_party: true, + public: true, + }, + // WebShare is enabled by default on Android. + { + name: "WebShare", + public: true, + status: "test", + }, + { + name: "WebSocketStream", + status: "experimental", + }, + { + name: "WebTransportCustomCertificates", + origin_trial_feature_name: "WebTransportCustomCertificates", + status: "stable", + }, + { + name: "WebUSB", + public: true, + status: "stable", + }, + { + name: "WebUsbDeviceForget", + status: "stable", + }, + { + name: "WebUSBOnDedicatedWorkers", + status: "stable", + depends_on: ["WebUSB"], + }, + { + // It is only enabled in extension environment for now. + name: "WebUSBOnServiceWorkers", + depends_on: ["WebUSB"], + public: true, + }, + { + name: "WebVTTRegions", + status: "experimental", + }, + { + name: "WebXR", + public: true, + status: "stable", + }, + { + name: "WebXRAnchors", + depends_on: ["WebXRARModule", "WebXRHitTest"], + status: "stable" + }, + { + name: "WebXRARModule", + depends_on: ["WebXR"], + public: true, + status: "stable", + }, + { + name: "WebXRCameraAccess", + depends_on: ["WebXRARModule"], + public: true, + status: "stable", + }, + { + name: "WebXRDepth", + origin_trial_feature_name: "WebXRDepth", + depends_on: ["WebXRARModule"], + public: true, + status: "stable", + }, + { + name: "WebXRHandInput", + depends_on: ["WebXRARModule"], + public: true, + status: "experimental" + }, + { + name: "WebXRHitTest", + depends_on: ["WebXRARModule"], + public: true, + status: "stable", + }, + { + name: "WebXRHitTestEntityTypes", + depends_on: ["WebXRHitTest"], + status: "experimental" + }, + { + name: "WebXRImageTracking", + origin_trial_feature_name: "WebXRImageTracking", + depends_on: ["WebXRARModule"], + public: true, + status: "experimental", + }, + { + name: "WebXRLightEstimation", + depends_on: ["WebXRARModule"], + public: true, + status: "stable", + }, + { + name: "WebXRPlaneDetection", + origin_trial_feature_name: "WebXRPlaneDetection", + depends_on: ["WebXRARModule"], + public: true, + status: "experimental", + }, + { + name: "WebXRViewportScale", + depends_on: ["WebXRARModule"], + public: true, + status: "stable", + }, + { + name: "WGIGamepadTriggerRumble", + status: "test", + }, + // New behavior for window.open's interpretation of the windowFeatures parameter. + { + name: "WindowOpenNewPopupBehavior", + status: "stable", + base_feature: "WindowOpenNewPopupBehavior", + }, + // Extends window placement functionality for multi-screen devices. Also + // exposes requisite information about displays connected to the device. + { + name: "WindowPlacement", + status: "stable", + base_feature: "WindowPlacement", + }, + // Exposes user-friendly screen labels, determined by the platform, to sites + // with window-placement permission. + { + name: "WindowPlacementEnhancedScreenLabels", + status: "stable", + }, + // Allows sites to request fullscreen when the set of screens change. + { + name: "WindowPlacementFullscreenOnScreensChange", + depends_on: ["WindowPlacement"], + status: "experimental", + base_feature: "WindowPlacementFullscreenOnScreensChange", + }, + { + // If enabled, the `getDisplayMedia()` family of APIs will ask for NV12 + // frames, which should trigger a zero-copy path in the tab capture code. + name: "ZeroCopyTabCapture", + base_feature: "ZeroCopyTabCapture", + }, + ], +} diff --git a/examples/main.go b/examples/main.go index b997734..a371ac6 100644 --- a/examples/main.go +++ b/examples/main.go @@ -2,17 +2,32 @@ package main import ( "fmt" + "os" + "time" "github.com/adhocore/jsonc" ) func main() { + t := time.Now() v := make(map[string]interface{}) j := jsonc.New() + // strip and unmarshal from cached file + if err := jsonc.NewCachedDecoder().Decode("./examples/test.json5", &v); err != nil { + fmt.Printf("%#v\n", err) + os.Exit(1) + } + fmt.Printf("%#v\n---\n", v) + // strip and unmarshal from file: j.UnmarshalFile(file, &v) - j.UnmarshalFile("./examples/test.json5", &v) - fmt.Printf("%+v\n---\n", v) + if err := j.UnmarshalFile("./examples/test.json5", &v); err != nil { + fmt.Printf("%#v\n", err) + os.Exit(1) + } + fmt.Printf("\n%#v\n---\n", v) + n := time.Now() + fmt.Printf("took %d μs (%d - %d)\n", n.Sub(t).Microseconds(), n.Nanosecond()/1000, t.Nanosecond()/1000) // strip and unmarshal from byte array: j.Unmarshal(b, &v) b := []byte(`{ @@ -22,7 +37,7 @@ func main() { }`) v1 := make(map[string]interface{}) j.Unmarshal(b, &v1) - fmt.Printf("%+v\n---\n", v1) + fmt.Printf("%#v\n---\n", v1) // strip and unmarshal from string: j.Unmarshal(s, &v) s := `{ @@ -31,11 +46,11 @@ func main() { /* comment */` + "\n}" v2 := make(map[string]interface{}) j.Unmarshal([]byte(s), &v2) - fmt.Printf("%+v\n---\n", v2) + fmt.Printf("%#v\n---\n", v2) // strip only from byte array: j.Strip(b) - fmt.Printf("%+v\n---\n", j.Strip(b)) + fmt.Printf("%#v\n---\n", j.Strip(b)) // strip only from string: j.StripS(s) - fmt.Printf("%+v\n---\n", j.StripS(s)) + fmt.Printf("%#v\n---\n", j.StripS(s)) } diff --git a/examples/test.json5 b/examples/test.json5 index 717a8cc..0c18605 100644 --- a/examples/test.json5 +++ b/examples/test.json5 @@ -1,17 +1,45 @@ +/*start*/ +//.. { - // this is line comment - "a": [ - "b", - /* multi line - comment */ - 123, - "// not comment", - ], - "d": { - "e": /* creepy comment */true, - "f": false, - }, - "g": "/*comment*/{\"i\":1,\"url\":\"http://foo.bar\"}//comment", - "h": "literal - new line", + // this is line comment + a: [ // unquoted key + 'bb', // single quoted string + "cc", // double quoted string + /* multi line + * comment + */ + 123, // number + +10, // +ve number, equivalent to 10 + -20, // -ve number + .25, // floating number, equivalent to 0.25 + 5., // floating number, equivalent to 5.0 + 0xabcDEF, // hex base16 number, equivalent to base10 counterpart: 11259375 + { + 123: 0xf, // even number as key? + xx: [1, .1, 'xyz',], y: '2', // array inside object, inside array + }, + "// not a comment", + "/* also not a comment */", + ['', "", true, false, null, 1, .5, 2., 0xf, // all sort of data types + {key:'val'/*comment*/,}], // object inside array, inside array + 'single quoted', + ], + /*aa*/aa: ['AA', {in: ['a', "b", ],},], + 'd': { // single quoted key + t: /*creepy comment*/true, 'f': false, + a_b: 1, _1_z: 2, Ḁẟḕỻ: 'ɷɻɐỨẞṏḉ', // weird keys? + "n": null /*multiple trailing commas?*/,,, + /* 1 */ + /* 2 */ + }, + "e": 'it\'s "good", eh?', // double quoted key, single quoted value with escaped quote + // json with comment inside json with comment, read that again: + "g": "/*comment*/{\"i\" : 1, \"url\" : \"http://foo.bar\" }//comment", + "h": "a new line after word 'literal' +this text is in a new line as there is literal EOL just above. \ +but this one is continued in same line due to backslash escape", + // 1. + // 2. } +//.. +/*end*/ diff --git a/examples/test1.json5 b/examples/test1.json5 new file mode 100644 index 0000000..cb291c8 --- /dev/null +++ b/examples/test1.json5 @@ -0,0 +1,13 @@ +// kitchen sink example of json5: https://github.com/json5/json5#example +{ + // comments + unquoted: 'and you can quote me on that', + singleQuotes: 'I can use "double quotes" here', + lineBreaks: "Look, Mom! \ + No \\n's!", // comment + hexadecimal: 0xdecaf, + leadingDecimalPoint: .8675309, andTrailing: 8675309., + positiveSign: /* comment */ +1, + trailingComma: 'in objects', andIn: ['arrays',], + "backwardsCompatible": "with JSON",, +} diff --git a/jsonc.go b/jsonc.go index 84c1ee8..ec8985c 100644 --- a/jsonc.go +++ b/jsonc.go @@ -2,23 +2,30 @@ package jsonc import ( "encoding/json" + "fmt" "io/ioutil" + "regexp" + "strconv" "strings" - "unicode" ) // Jsonc is the structure for parsing json with comments type Jsonc struct { - comment int - commaPos int - inStr bool - index int - len int + index int // current index position in source data + comment uint // the type of comment: eithher 1 for // OR 2 for /* + len int // the length of source data + objDepth uint // the depth of nested objects + arrDepth uint // the depth of nested arrays + inStr bool // if inside string + inArr bool // if inside array notation: [ + inObj bool // if inside object notation: { + last string // last significant non whitespace char + strDelim string // string delimeter: either ' or " } -// New creates Jsonc struct with proper defaults +// New creates Jsonc struct func New() *Jsonc { - return &Jsonc{0, -1, false, 0, 0} + return &Jsonc{} } // Strip strips comments and trailing commas from input byte array @@ -27,38 +34,48 @@ func (j *Jsonc) Strip(jsonb []byte) []byte { return []byte(s) } -var crlf = map[string]string{"\n": `\n`, "\t": `\t`, "\r": `\r`} +var sq = `'` // single quote +var dq = `"` // double quote +var esc = `\` // escape +var comma = regexp.MustCompile(`(?:,+)(\s*)$`) // StripS strips comments and trailing commas from input string func (j *Jsonc) StripS(data string) string { var oldprev, prev, char, next, s string - j.doReset() + j.reset() j.len = len(data) + quote, quoted := "", false for j.index < j.len { oldprev, prev, char, next = j.getSegments(data, prev) - j.index++ - j.checkTrailComma(char) - if (char == "]" || char == "}") && j.commaPos > -1 { - s = j.trimTrailComma(s) + // If value starts with 0x, parse as hexadecimal + if j.isNonStringValue(char, "0") && (next == "x" || next == "X") { + s += j.hexadecimal(data) + continue + } + + quote, quoted = j.quoteKey(char, quoted) + s += quote + + // Trim trailing commas at the end of array or object + if j.comment == 0 && !j.inStr && ((j.inArr && char == "]") || (j.inObj && char == "}")) { + s = comma.ReplaceAllString(s, `$1`) } - if j.inStringOrCommentEnd(prev, char, char+next, oldprev) { - if c, ok := crlf[char]; ok && j.inStr { - char = c - } - s += char + + j.checkArrayObject(char) + + // Append char as is (or it's compliment pair) if inside string or outside comment + if j.inString(prev, char, next, oldprev) || j.outsideComment(char, next) { + s += j.compliment(prev, char, next) continue } - wasSingle := j.comment == 1 - if j.hasCommentEnded(char, next) && wasSingle { + // Wipe out trailing whitespaces around comment + if j.hasCommentEnded(char, next) && char == "\n" { s = strings.TrimRight(s, "\r\n\t ") + char } - if char+next == "*/" { - j.index++ - } } return s } @@ -77,12 +94,15 @@ func (j *Jsonc) UnmarshalFile(file string, v interface{}) error { return j.Unmarshal(jsonb, v) } -func (j *Jsonc) doReset() { - j.inStr = false - j.index = 0 - j.commaPos = -1 +// reset resets the Jsonc with proper defaults +func (j *Jsonc) reset() { + j.index, j.comment = 0, 0 + j.objDepth, j.arrDepth = 0, 0 + j.inStr, j.inArr, j.inObj = false, false, false + j.last, j.strDelim = "", "" } +// getSegments gets look-behind, current and look-ahead chars func (j *Jsonc) getSegments(json, old string) (oldprev, prev, char, next string) { oldprev = old if j.index > 0 { @@ -92,67 +112,165 @@ func (j *Jsonc) getSegments(json, old string) (oldprev, prev, char, next string) if j.index < j.len-1 { next = json[j.index+1 : j.index+2] } + j.index++ return } -func (j *Jsonc) checkTrailComma(char string) { - if char == "," || j.commaPos == -1 { - if char == "," { - j.commaPos++ +// isNonStringValue checks if char is value outside string (or comment) and matches chars +func (j *Jsonc) isNonStringValue(char, chars string) bool { + return !j.inStr && j.comment == 0 && strings.ContainsAny(char, chars) +} + +// hexadecimal consumes hex (0-9a-fA-F) chars and converts to decimal string +func (j *Jsonc) hexadecimal(data string) string { + j.index++ + hexa := "" + for j.index < j.len { + char := data[j.index : j.index+1] + if !isNumber(char, true) { + break } - return + hexa += char + j.index++ } + dec, _ := strconv.ParseInt(hexa, 16, 32) + return fmt.Sprintf("%d", dec) +} - rchar := []rune(char[0:1]) - if unicode.IsDigit(rchar[0]) || strings.ContainsAny(char, `"tfn{[`) { - j.commaPos = -1 - return +// quoteKey double quotes the unquoted object keys +func (j *Jsonc) quoteKey(char string, wasQuoted bool) (q string, quoted bool) { + quoted = wasQuoted + inKey := j.inObj && j.comment == 0 && (j.last == "{" || j.last == ",") + // Object key has just started without quote, so quote it + if !j.inStr && inKey && !strings.ContainsAny(char, "[]{}'\",/*:\r\n\t ") { + q = dq + j.inStr, quoted, j.strDelim = true, true, dq } - - if char != "]" && char != "}" { - j.commaPos++ + // Object key has just ended and was quoted before, so quote it again to compliment + if j.inStr && wasQuoted && inKey && (char == ":" || char == " " || char == sq) { + q = dq + j.inStr, quoted, j.strDelim = false, false, "" } + return } -func (j *Jsonc) trimTrailComma(s string) string { - pos := len(s) - j.commaPos - 1 - s = strings.TrimRight(s[0:pos], ",") + strings.TrimLeft(s[pos:], ",") - j.commaPos = -1 - return s +// checkArrayObject checks and sets the depth and state of array &/or object notation +func (j *Jsonc) checkArrayObject(char string) { + if j.isNonStringValue(char, char) { + // Last non whitespace char + if !strings.ContainsAny(char, "\r\n\t /") { + j.last = char + } + if char == "{" { + j.objDepth++ + j.inObj, j.inArr = true, false + } else if j.objDepth > 0 && char == "}" { + j.objDepth-- + j.inObj, j.inArr = j.objDepth > 0, j.arrDepth > 0 + } else if char == "[" { + j.arrDepth++ + j.inObj, j.inArr = false, true + } else if j.arrDepth > 0 && char == "]" { + j.arrDepth-- + j.inObj, j.inArr = j.objDepth > 0, j.arrDepth > 0 + } + } } -func (j *Jsonc) inStringOrCommentEnd(prev, char, charnext, oldprev string) bool { - return j.inString(prev, char, charnext, oldprev) || j.inCommentEnd(charnext) -} +func (j *Jsonc) inString(prev, char, next, oldprev string) bool { + charnext := char + next + maybeStr := (char == dq || char == sq) && (!j.inStr || j.strDelim == char) -func (j *Jsonc) inString(prev, char, charnext, oldprev string) bool { - if j.comment == 0 && char == `"` && prev != "\\" { + // Toggle j.inStr if j.strDelim is not escaped + if j.comment == 0 && maybeStr && prev != esc { + if !j.inStr { + j.strDelim = char + } j.inStr = !j.inStr return j.inStr } if j.inStr && (charnext == `":` || charnext == `",` || charnext == `"]` || charnext == `"}`) { - j.inStr = oldprev+prev != "\\\\" + j.inStr = oldprev+prev != esc+esc } return j.inStr } -func (j *Jsonc) inCommentEnd(charnext string) bool { +// outsideComment checks if char is outside comment +// it also sets the state of comment +func (j *Jsonc) outsideComment(char, next string) bool { + // Set comment state: `//` => 1 | `/*` => 2 if !j.inStr && j.comment == 0 { - if charnext == "//" { + if char+next == "//" { j.comment = 1 } - if charnext == "/*" { + if char+next == "/*" { j.comment = 2 } } return j.comment == 0 } +// hasCommentEnded checks if the comment has just ended and resets the state func (j *Jsonc) hasCommentEnded(char, next string) bool { + // Single line comment ends with `\n` and multiline ends with `*/` singleEnded := j.comment == 1 && char == "\n" - if singleEnded || (j.comment == 2 && char+next == "*/") { + multiEnded := j.comment == 2 && char+next == "*/" + if singleEnded || multiEnded { j.comment = 0 - return true } - return false + if multiEnded { + j.index++ + } + return j.comment == 0 +} + +var spacesPair = map[string]string{"\n": `\n`, "\t": `\t`, "\r": `\r`} + +// compliment appends char as is (or it's compliment pair) +// (eg: in string boundary the compliment of single quote is double quote) +// it also normalizes whitespaces inside string and signed &/or decimal numbers +func (j *Jsonc) compliment(prev, char, next string) string { + if j.inStr && char == esc && next == "\n" { + j.index++ + return "" + } else if c, ok := spacesPair[char]; ok && j.inStr { + return c + } + + // Signed +ve number + if j.isNonStringValue(char, "+") && isNumber(next, false) { + return "" + } + + // Decimal point number + if j.isNonStringValue(char, ".") { + prevNum, nextNum := isNumber(prev, false), isNumber(next, false) + if !prevNum && nextNum { + char = "0." + } else if prevNum && !nextNum { + char = ".0" + } + return char + } + + // Single quoted string + if j.strDelim == sq { + if char+next == esc+sq { + char = sq + j.index++ + } else if prev != esc && char == sq { + char = dq + } else if char == dq { + char = `\"` + } + } + return char +} + +// isNumber checks if a string char is numeric +func isNumber(char string, hex bool) bool { + if hex { + return strings.ContainsAny(char, "0123456789abcdefABCDEF") + } + return strings.ContainsAny(char, "0123456789") } diff --git a/jsonc_test.go b/jsonc_test.go index bf83785..2b6ab25 100644 --- a/jsonc_test.go +++ b/jsonc_test.go @@ -1,6 +1,7 @@ package jsonc import ( + "os" "strings" "testing" ) @@ -34,29 +35,53 @@ func TestUnmarshal(t *testing.T) { t.Run("Unmarshal "+name, func(t *testing.T) { var ref map[string]interface{} if err := j.Unmarshal([]byte(test.json), &ref); err != nil { - t.Errorf("[%s] unmarshal should not error, got %v", name, err) + t.Errorf("[%s] unmarshal should not error, got %#v", name, err) } if name == "nested subjson" { jo := ref["jo"].(string) if err := j.Unmarshal([]byte(jo), &ref); err != nil { - t.Errorf("[%v] unmarshal should not error, got %v", jo, err) + t.Errorf("[%v] unmarshal should not error, got %#v", jo, err) } } }) } t.Run("UnmarshalFile", func(t *testing.T) { - var ref map[string]interface{} - if err := j.UnmarshalFile("./examples/test.json5", &ref); err != nil { - t.Errorf("UnmarshalFile should not error, got %v", err) - } - s := ref["g"] - if err := j.Unmarshal([]byte(s.(string)), &ref); err != nil { - t.Errorf("[%v] unmarshal should not error, got %v", s, err) + files := []string{"chromium.json5", "test1.json5", "test.json5"} + for _, file := range files { + t.Run(file, func(t *testing.T) { + var ref map[string]interface{} + if err := j.UnmarshalFile("./examples/"+file, &ref); err != nil { + t.Errorf("UnmarshalFile should not error, got %#v", err) + } + if file != "test.json5" { + return + } + s := ref["g"] + if err := j.Unmarshal([]byte(s.(string)), &ref); err != nil { + t.Errorf("[%v] unmarshal should not error, got %#v", s, err) + } + if err := j.UnmarshalFile("./examples/invalid.json5", &ref); err == nil { + t.Error("invalid file should error, got none") + } + }) } - if err := j.UnmarshalFile("./examples/invalid.json5", &ref); err == nil { - t.Error("invalid file should error, got none") + }) +} + +func TestCachedDecoder(t *testing.T) { + file := "./examples/test1.json5" + cd, val := NewCachedDecoder(), make(map[string]interface{}) + t.Run("before cache", func(t *testing.T) { + os.Remove("./examples/test1.cached.json") + if err := cd.Decode(file, &val); err != nil { + t.Errorf("[%v] decode should not error, got %#v", file, err) } + t.Run("after cache", func(t *testing.T) { + if err := cd.Decode(file, &val); err != nil { + t.Errorf("[%v] decode should not error, got %#v", file, err) + } + }) }) }