├── .github └── workflows │ └── ci.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── constructor_type_parser.go ├── escape.go ├── go.mod ├── go.sum ├── options.go ├── parser.go ├── parts.go ├── testdata └── urlpatterntestdata.json ├── tokenizer.go ├── tokens.go ├── urlpattern.go └── urlpattern_test.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | - name: Lint Go Code 17 | uses: golangci/golangci-lint-action@v6 18 | 19 | test: 20 | name: Test 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | go-version: ['stable'] 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Set up Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | - name: Test 34 | run: go test -race ${{ matrix.go-version == 'stable' && '-covermode atomic -coverprofile=profile.cov' || ''}} 35 | - name: Upload coverage results 36 | if: matrix.go-version == 'stable' 37 | uses: shogo82148/actions-goveralls@v1 38 | with: 39 | path-to-profile: profile.cov 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, ethnicity, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact+coc@mercure.rocks. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Kévin Dunglas 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go URL Pattern 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/dunglas/go-urlpattern.svg)](https://pkg.go.dev/github.com/dunglas/go-urlpattern) 4 | [![Coverage Status](https://coveralls.io/repos/github/dunglas/go-urlpattern/badge.svg)](https://coveralls.io/github/dunglas/go-urlpattern) 5 | 6 | A spec-compliant implementation of [the WHATWG URL Pattern Living Standard](https://urlpattern.spec.whatwg.org/) 7 | written in [Go](https://go.dev). 8 | 9 | Tested with [web-platform-test](https://web-platform-tests.org) test suite. 10 | 11 | ## Docs 12 | 13 | [Read the docs on Go Packages](https://pkg.go.dev/github.com/dunglas/go-urlpattern). 14 | 15 | ## Limitations 16 | 17 | * Some [advanced unicode features (JavaScript's `v` mode)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) are not supported, because they are not supported by Go regular expressions. 18 | 19 | ## Credits 20 | 21 | Created by [Kévin Dunglas](https://dunglas.fr). 22 | 23 | Sponsored by: 24 | 25 | * [Mercure.rocks](https://mercure.rocks) 26 | * [Les-Tilleuls.coop](https://les-tilleuls.coop) 27 | -------------------------------------------------------------------------------- /constructor_type_parser.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | import ( 4 | "regexp" 5 | 6 | "golang.org/x/exp/utf8string" 7 | ) 8 | 9 | type state uint8 10 | 11 | // https://urlpattern.spec.whatwg.org/#constructor-string-parsing 12 | 13 | type constructorTypeParser struct { 14 | input utf8string.String 15 | tokenList []token 16 | result URLPatternInit 17 | componentStart int 18 | tokenIndex int 19 | tokenIncrement int 20 | groupDepth int 21 | hostnameIPv6BracketDepth int 22 | protocolMatchesASpecialScheme bool 23 | state state 24 | } 25 | 26 | // https://urlpattern.spec.whatwg.org/#constructor-string-parser-state 27 | 28 | const ( 29 | stateInit state = iota 30 | stateProtocol 31 | sateAuthority 32 | stateUsername 33 | statePassword 34 | stateHostname 35 | statePort 36 | statePathname 37 | stateSearch 38 | stateHash 39 | stateDone 40 | ) 41 | 42 | // https://urlpattern.spec.whatwg.org/#parse-a-constructor-string 43 | func newConstructorTypeParser(input string, tokenList []token) constructorTypeParser { 44 | return constructorTypeParser{ 45 | input: *utf8string.NewString(input), 46 | tokenList: tokenList, 47 | result: URLPatternInit{}, 48 | tokenIncrement: 1, 49 | state: stateInit, 50 | } 51 | } 52 | 53 | // https://urlpattern.spec.whatwg.org/#constructor-string-parsing 54 | func parseConstructorString(input string) (*URLPatternInit, error) { 55 | tl, err := tokenize(input, tokenizePolicyLenient) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | p := newConstructorTypeParser(input, tl) 61 | 62 | tlLen := len(p.tokenList) 63 | 64 | for p.tokenIndex < tlLen { 65 | p.tokenIncrement = 1 66 | 67 | if p.tokenList[p.tokenIndex].tType == tokenEnd { 68 | if p.state == stateInit { 69 | p.rewind() 70 | 71 | if p.isHashPrefix() { 72 | p.changeState(stateHash, 1) 73 | } else if p.isSearchPrefix() { 74 | p.changeState(stateSearch, 1) 75 | //p.result.Hash = "" 76 | } else { 77 | p.changeState(statePathname, 0) 78 | //p.result.Hash = "" 79 | //p.result.Search = "" 80 | } 81 | 82 | p.tokenIndex += p.tokenIncrement 83 | 84 | continue 85 | } 86 | 87 | if p.state == sateAuthority { 88 | p.rewindAndSetState(stateHostname) 89 | p.tokenIndex += p.tokenIncrement 90 | 91 | continue 92 | } 93 | 94 | p.changeState(stateDone, 0) 95 | 96 | break 97 | } 98 | 99 | if p.isGroupOpen() { 100 | p.groupDepth++ 101 | p.tokenIndex += p.tokenIncrement 102 | 103 | continue 104 | } 105 | 106 | if p.groupDepth > 0 { 107 | if p.isGroupClose() { 108 | p.groupDepth-- 109 | } else { 110 | p.tokenIndex += p.tokenIncrement 111 | 112 | continue 113 | } 114 | } 115 | 116 | // Switch on parser’s state and run the associated steps: 117 | switch p.state { 118 | case stateInit: 119 | if p.isProtocolSuffix() { 120 | p.rewindAndSetState(stateProtocol) 121 | } 122 | case stateProtocol: 123 | if p.isProtocolSuffix() { 124 | if err := p.computeProtocolMatchesSpecialSchemeFlag(); err != nil { 125 | return nil, err 126 | } 127 | 128 | nextState := statePathname 129 | skip := 1 130 | 131 | if p.nextIsAuthoritySlashes() { 132 | nextState = sateAuthority 133 | skip = 3 134 | } else if p.protocolMatchesASpecialScheme { 135 | nextState = sateAuthority 136 | } 137 | 138 | p.changeState(nextState, skip) 139 | } 140 | 141 | case sateAuthority: 142 | if p.isIdentityTerminator() { 143 | p.rewindAndSetState(stateUsername) 144 | } else if p.isPathnameStart() || 145 | p.isSearchPrefix() || 146 | p.isHashPrefix() { 147 | p.rewindAndSetState(stateHostname) 148 | } 149 | 150 | case stateUsername: 151 | if p.isPasswordPrefix() { 152 | p.changeState(statePassword, 1) 153 | } else if p.isIdentityTerminator() { 154 | p.changeState(stateHostname, 1) 155 | } 156 | 157 | case statePassword: 158 | if p.isIdentityTerminator() { 159 | p.changeState(stateHostname, 1) 160 | } 161 | 162 | case stateHostname: 163 | if p.isIPV6Open() { 164 | p.hostnameIPv6BracketDepth++ 165 | } else if p.isIPV6Close() { 166 | p.hostnameIPv6BracketDepth-- 167 | } else if p.isPortPrefix() && p.hostnameIPv6BracketDepth == 0 { 168 | p.changeState(statePort, 1) 169 | } else if p.isPathnameStart() { 170 | p.changeState(statePathname, 0) 171 | } else if p.isSearchPrefix() { 172 | p.changeState(stateSearch, 1) 173 | } else if p.isHashPrefix() { 174 | p.changeState(stateHash, 1) 175 | } 176 | 177 | case statePort: 178 | if p.isPathnameStart() { 179 | p.changeState(statePathname, 0) 180 | } else if p.isSearchPrefix() { 181 | p.changeState(stateSearch, 1) 182 | } else if p.isHashPrefix() { 183 | p.changeState(stateHash, 1) 184 | } 185 | 186 | case statePathname: 187 | if p.isSearchPrefix() { 188 | p.changeState(stateSearch, 1) 189 | } else if p.isHashPrefix() { 190 | p.changeState(stateHash, 1) 191 | } 192 | 193 | case stateSearch: 194 | if p.isHashPrefix() { 195 | p.changeState(stateHash, 1) 196 | } 197 | 198 | case stateHash: 199 | // noop 200 | 201 | case stateDone: 202 | // Assert: This step is never reached. 203 | panic("done state must never be reached") 204 | } 205 | 206 | p.tokenIndex += p.tokenIncrement 207 | } 208 | 209 | // If parser’s result contains "hostname" and not "port", then set parser’s result["port"] to the empty string. 210 | if p.result.Hostname != nil && p.result.Port == nil { 211 | es := "" 212 | p.result.Port = &es 213 | } 214 | 215 | return &p.result, nil 216 | } 217 | 218 | // https://urlpattern.spec.whatwg.org/#rewind 219 | func (p *constructorTypeParser) rewind() { 220 | p.tokenIndex = p.componentStart 221 | p.tokenIncrement = 0 222 | } 223 | 224 | // https://urlpattern.spec.whatwg.org/#rewind-and-set-state 225 | func (p *constructorTypeParser) rewindAndSetState(s state) { 226 | p.rewind() 227 | p.state = s 228 | } 229 | 230 | // https://urlpattern.spec.whatwg.org/#is-a-hash-prefix 231 | func (p *constructorTypeParser) isHashPrefix() bool { 232 | return p.isNonSpecialPatternChar(p.tokenIndex, "#") 233 | } 234 | 235 | // https://urlpattern.spec.whatwg.org/#is-a-search-prefix 236 | func (p *constructorTypeParser) isSearchPrefix() bool { 237 | if p.isNonSpecialPatternChar(p.tokenIndex, "?") { 238 | return true 239 | } 240 | 241 | if p.tokenList[p.tokenIndex].value != "?" { 242 | return false 243 | } 244 | 245 | previousIndex := p.tokenIndex - 1 246 | if previousIndex < 0 { 247 | return true 248 | } 249 | 250 | previousToken := p.getSafeToken(previousIndex) 251 | switch previousToken.tType { 252 | case tokenName: 253 | return false 254 | 255 | case tokenRegexp: 256 | return false 257 | 258 | case tokenClose: 259 | return false 260 | 261 | case tokenAsterisk: 262 | return false 263 | } 264 | 265 | return true 266 | } 267 | 268 | // https://urlpattern.spec.whatwg.org/#is-a-group-open 269 | func (p *constructorTypeParser) isGroupOpen() bool { 270 | return p.tokenList[p.tokenIndex].tType == tokenOpen 271 | } 272 | 273 | // https://urlpattern.spec.whatwg.org/#is-a-group-close 274 | func (p *constructorTypeParser) isGroupClose() bool { 275 | return p.tokenList[p.tokenIndex].tType == tokenClose 276 | } 277 | 278 | // https://urlpattern.spec.whatwg.org/#is-a-non-special-pattern-char 279 | func (p *constructorTypeParser) isNonSpecialPatternChar(index int, value string) bool { 280 | token := p.getSafeToken(index) 281 | if token.value != value { 282 | return false 283 | } 284 | 285 | return token.tType == tokenChar || token.tType == tokenEscapedChar || token.tType == tokenInvalidChar 286 | } 287 | 288 | // https://urlpattern.spec.whatwg.org/#get-a-safe-token 289 | func (p *constructorTypeParser) getSafeToken(index int) token { 290 | len := len(p.tokenList) 291 | 292 | if index < len { 293 | return p.tokenList[index] 294 | } 295 | 296 | // Assert: parser's token list's size is greater than or equal to 1. 297 | 298 | return p.tokenList[len-1] 299 | } 300 | 301 | // https://urlpattern.spec.whatwg.org/#change-state 302 | func (p *constructorTypeParser) changeState(newState state, skip int) { 303 | v := p.makeComponentString() 304 | 305 | // ignore sInit, authority and done 306 | switch p.state { 307 | case stateProtocol: 308 | p.result.Protocol = &v 309 | case stateUsername: 310 | p.result.Username = &v 311 | case statePassword: 312 | p.result.Password = &v 313 | case stateHostname: 314 | p.result.Hostname = &v 315 | case statePort: 316 | p.result.Port = &v 317 | case statePathname: 318 | p.result.Pathname = &v 319 | case stateSearch: 320 | p.result.Search = &v 321 | case stateHash: 322 | p.result.Hash = &v 323 | } 324 | 325 | if p.state != stateInit && newState != stateDone { 326 | es := "" 327 | 328 | // If parser’s state is "protocol", "authority", "username", or "password"; new state is "port", "pathname", "search", or "hash"; and parser’s result["hostname"] does not exist, then set parser’s result["hostname"] to the empty string. 329 | if p.result.Hostname == nil && 330 | (p.state == stateProtocol || p.state == sateAuthority || p.state == stateUsername || p.state == statePassword) && 331 | (newState == statePort || newState == statePathname || newState == stateSearch || newState == stateHash) { 332 | p.result.Hostname = &es 333 | } 334 | 335 | if p.result.Pathname == nil && 336 | (p.state == stateProtocol || p.state == sateAuthority || p.state == stateUsername || p.state == statePassword || p.state == stateHostname || p.state == statePort) && 337 | (newState == stateSearch || newState == stateHash) { 338 | if p.protocolMatchesASpecialScheme { 339 | sl := "/" 340 | p.result.Pathname = &sl 341 | } else { 342 | p.result.Pathname = &es 343 | } 344 | } 345 | 346 | if p.result.Search == nil && 347 | (p.state == stateProtocol || p.state == sateAuthority || p.state == stateUsername || p.state == statePassword || p.state == stateHostname || p.state == statePort || p.state == statePathname) && 348 | (newState == stateHash) { 349 | p.result.Search = &es 350 | } 351 | } 352 | 353 | p.state = newState 354 | p.tokenIndex = p.tokenIndex + skip 355 | p.componentStart = p.tokenIndex 356 | p.tokenIncrement = 0 357 | } 358 | 359 | // https://urlpattern.spec.whatwg.org/#make-a-component-string 360 | func (p *constructorTypeParser) makeComponentString() string { 361 | token := p.tokenList[p.tokenIndex] 362 | componentStartToken := p.getSafeToken(int(p.componentStart)) 363 | componentStartInputIndex := componentStartToken.index 364 | endIndex := token.index 365 | 366 | return p.input.Slice(componentStartInputIndex, endIndex) 367 | } 368 | 369 | // https://urlpattern.spec.whatwg.org/#is-a-protocol-suffix 370 | func (p *constructorTypeParser) isProtocolSuffix() bool { 371 | return p.isNonSpecialPatternChar(p.tokenIndex, ":") 372 | } 373 | 374 | // https://urlpattern.spec.whatwg.org/#compute-protocol-matches-a-special-scheme-flag 375 | func (p *constructorTypeParser) computeProtocolMatchesSpecialSchemeFlag() error { 376 | protocol := p.makeComponentString() 377 | protocolComponent, err := compileComponent(protocol, canonicalizeProtocol, options{}) 378 | if err != nil { 379 | return err 380 | } 381 | 382 | if protocolComponent.protocolComponentMatchesSpecialScheme() { 383 | p.protocolMatchesASpecialScheme = true 384 | } 385 | 386 | return nil 387 | } 388 | 389 | // https://urlpattern.spec.whatwg.org/#next-is-authority-slashes 390 | func (p *constructorTypeParser) nextIsAuthoritySlashes() bool { 391 | return p.isNonSpecialPatternChar(p.tokenIndex+1, "/") && p.isNonSpecialPatternChar(p.tokenIndex+2, "/") 392 | } 393 | 394 | // https://urlpattern.spec.whatwg.org/#is-an-identity-terminator 395 | func (p *constructorTypeParser) isIdentityTerminator() bool { 396 | return p.isNonSpecialPatternChar(p.tokenIndex, "@") 397 | } 398 | 399 | // https://urlpattern.spec.whatwg.org/#is-a-pathname-start 400 | func (p *constructorTypeParser) isPathnameStart() bool { 401 | return p.isNonSpecialPatternChar(p.tokenIndex, "/") 402 | } 403 | 404 | // https://urlpattern.spec.whatwg.org/#is-a-password-prefix 405 | func (p *constructorTypeParser) isPasswordPrefix() bool { 406 | return p.isNonSpecialPatternChar(p.tokenIndex, ":") 407 | } 408 | 409 | // https://urlpattern.spec.whatwg.org/#is-a-port-prefix 410 | func (p *constructorTypeParser) isPortPrefix() bool { 411 | return p.isNonSpecialPatternChar(p.tokenIndex, ":") 412 | } 413 | 414 | // https://urlpattern.spec.whatwg.org/#is-an-ipv6-open 415 | func (p *constructorTypeParser) isIPV6Open() bool { 416 | return p.isNonSpecialPatternChar(p.tokenIndex, "[") 417 | } 418 | 419 | // https://urlpattern.spec.whatwg.org/#is-an-ipv6-close 420 | func (p *constructorTypeParser) isIPV6Close() bool { 421 | return p.isNonSpecialPatternChar(p.tokenIndex, "]") 422 | } 423 | 424 | // https://urlpattern.spec.whatwg.org/#compile-a-component 425 | func compileComponent(input string, encodencodingCallback encodingCallback, options options) (*component, error) { 426 | partList, err := parsePatternString(input, options, encodencodingCallback) 427 | if err != nil { 428 | return nil, err 429 | } 430 | 431 | // Let (regular expression string, name list) be the result of running generate a regular expression and name list given part list and options. 432 | regularExpressionString, nameList, err := partList.generateRegularExpressionAndNameList(options) 433 | if err != nil { 434 | return nil, err 435 | } 436 | 437 | regularExpression, err := regexp.Compile(regularExpressionString) 438 | if err != nil { 439 | return nil, err 440 | } 441 | 442 | patternString, err := partList.generatePatternString(options) 443 | if err != nil { 444 | return nil, err 445 | } 446 | 447 | var hasRegexpGroups bool 448 | for _, part := range partList { 449 | if part.pType == partRegexp { 450 | hasRegexpGroups = true 451 | 452 | break 453 | } 454 | } 455 | 456 | return &component{patternString, regularExpression, nameList, hasRegexpGroups}, nil 457 | } 458 | -------------------------------------------------------------------------------- /escape.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | import "unicode/utf8" 4 | 5 | // Adapted from the regexp package (/ added to the list of special chars): https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/regexp/regexp.go;l=705-747 6 | 7 | // Copyright 2009 The Go Authors. All rights reserved. 8 | // Use of this source code is governed by a BSD-style 9 | // license that can be found at https://go.dev/LICENSE. 10 | 11 | // Bitmap used by func special to check whether a character needs to be escaped. 12 | var specialRegexpBytes [16]byte 13 | var specialPatternBytes [16]byte 14 | 15 | // specialRegexp reports whether byte b needs to be escaped by QuoteMeta. 16 | func specialRegexp(b byte) bool { 17 | return b < utf8.RuneSelf && specialRegexpBytes[b%16]&(1<<(b/16)) != 0 18 | } 19 | 20 | // specialPattern reports whether byte b needs to be escaped by QuoteMeta. 21 | func specialPattern(b byte) bool { 22 | return b < utf8.RuneSelf && specialPatternBytes[b%16]&(1<<(b/16)) != 0 23 | } 24 | 25 | func init() { 26 | for _, b := range []byte(`\.+*?()|[]{}^$/`) { 27 | specialRegexpBytes[b%16] |= 1 << (b / 16) 28 | } 29 | for _, b := range []byte(`\+*?(){}:`) { 30 | specialPatternBytes[b%16] |= 1 << (b / 16) 31 | } 32 | } 33 | 34 | // https://urlpattern.spec.whatwg.org/#escape-a-regexp-string 35 | func escapeRegexpString(s string) string { 36 | // A byte loop is correct because all metacharacters are ASCII. 37 | var i int 38 | for i = 0; i < len(s); i++ { 39 | if specialRegexp(s[i]) { 40 | break 41 | } 42 | } 43 | // No meta characters found, so return original string. 44 | if i >= len(s) { 45 | return s 46 | } 47 | 48 | b := make([]byte, 2*len(s)-i) 49 | copy(b, s[:i]) 50 | j := i 51 | for ; i < len(s); i++ { 52 | if specialRegexp(s[i]) { 53 | b[j] = '\\' 54 | j++ 55 | } 56 | b[j] = s[i] 57 | j++ 58 | } 59 | return string(b[:j]) 60 | } 61 | 62 | // https://urlpattern.spec.whatwg.org/#escape-a-pattern-string 63 | func escapePatternString(s string) string { 64 | // A byte loop is correct because all metacharacters are ASCII. 65 | var i int 66 | for i = 0; i < len(s); i++ { 67 | if specialPattern(s[i]) { 68 | break 69 | } 70 | } 71 | // No meta characters found, so return original string. 72 | if i >= len(s) { 73 | return s 74 | } 75 | 76 | b := make([]byte, 2*len(s)-i) 77 | copy(b, s[:i]) 78 | j := i 79 | for ; i < len(s); i++ { 80 | if specialPattern(s[i]) { 81 | b[j] = '\\' 82 | j++ 83 | } 84 | b[j] = s[i] 85 | j++ 86 | } 87 | return string(b[:j]) 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dunglas/go-urlpattern 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/nlnwa/whatwg-url v0.5.0 7 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 8 | ) 9 | 10 | require ( 11 | github.com/bits-and-blooms/bitset v1.14.3 // indirect 12 | golang.org/x/net v0.29.0 // indirect 13 | golang.org/x/text v0.18.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 2 | github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= 3 | github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 4 | github.com/nlnwa/whatwg-url v0.5.0 h1:l71cqfqG44+VCQZQX3wD4bwheFWicPxuwaCimLEfpDo= 5 | github.com/nlnwa/whatwg-url v0.5.0/go.mod h1:X/ejnFFVbaOWdSul+cnlsSHviCzGZJdvPkgc9zD8IY8= 6 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 9 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 10 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 11 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 12 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 13 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 14 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 15 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 17 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 18 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 19 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 20 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 21 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 22 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 23 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 26 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 35 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 37 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 38 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 39 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 40 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 41 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 45 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 46 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 47 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 48 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 49 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 50 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 53 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 54 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 55 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | // https://urlpattern.spec.whatwg.org/#options-header 4 | type options struct { 5 | // MUST be an ASCII scode point 6 | delimiterCodePoint byte 7 | prefixCodePoint byte 8 | ignoreCase bool 9 | } 10 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "unicode" 9 | 10 | "github.com/nlnwa/whatwg-url/canonicalizer" 11 | "github.com/nlnwa/whatwg-url/url" 12 | ) 13 | 14 | // https://urlpattern.spec.whatwg.org/#full-wildcard-regexp-value 15 | const fullWildcardRegexpValue = ".*" 16 | 17 | // Experimental: this symbol is exported to allow users adding new values, but may be removed in the feature. 18 | // TODO: there is nothing in the Go stdlib to find the default port associated with a protocol. 19 | // Let's just replace values for protocols in specialSchemeList for now. 20 | // This list could be completed using https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers 21 | var DefaultPorts = map[string]string{ 22 | "http": "80", 23 | "https": "443", 24 | "ws": "80", 25 | "wss": "443", 26 | "ftp": "21", 27 | } 28 | 29 | var urlParser = url.NewParser() 30 | var hostnameParser = canonicalizer.New(url.WithFailOnValidationError(), canonicalizer.WithDefaultScheme("http")) 31 | 32 | var ( 33 | NonEmptySuffixError = errors.New("suffix must be the empty string") 34 | BadParserIndexError = errors.New("parser's index must be less than parser's token list size") 35 | DuplicatePartNameError = errors.New("duplicate name") 36 | RequiredTokenError = errors.New("missing required token") 37 | InvalidIPv6HostnameError = errors.New("invalid IPv6 hostname") 38 | InvalidPortError = errors.New("invalid port") 39 | ) 40 | 41 | // https://urlpattern.spec.whatwg.org/#encoding-callback 42 | type encodingCallback func(string) (string, error) 43 | 44 | // https://urlpattern.spec.whatwg.org/#parse-a-pattern-string 45 | func parsePatternString(input string, options options, encodingCallback encodingCallback) (partList, error) { 46 | tl, err := tokenize(input, tokenizePolicyStrict) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | p := patternParser{ 52 | encodingCallback: encodingCallback, 53 | segmentWildcardRegexp: generateSegmentWildcardRegexp(options), 54 | tokenList: tl, 55 | } 56 | 57 | tls := len(tl) 58 | 59 | for p.index < tls { 60 | charToken, err := p.tryConsumeToken(tokenChar) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | nameToken, err := p.tryConsumeToken(tokenName) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | regexpOrWildcardToken, err := p.tryConsumeRegexpOrWildcardToken(nameToken) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if nameToken != nil || regexpOrWildcardToken != nil { 76 | prefix := "" 77 | if charToken != nil { 78 | prefix = charToken.value 79 | } 80 | 81 | if prefix != "" && prefix != string(options.prefixCodePoint) { 82 | p.pendingFixedValue = p.pendingFixedValue + prefix 83 | prefix = "" 84 | } 85 | 86 | if err := p.maybeAddPartFromPendingFixedValue(); err != nil { 87 | return nil, err 88 | } 89 | 90 | modifierToken, err := p.tryConsumeModifierToken() 91 | if err != nil { 92 | return nil, err 93 | } 94 | if err := p.addPart(prefix, nameToken, regexpOrWildcardToken, "", modifierToken); err != nil { 95 | return nil, err 96 | } 97 | 98 | continue 99 | } 100 | 101 | fixedToken := charToken 102 | if fixedToken == nil { 103 | fixedToken, err = p.tryConsumeToken(tokenEscapedChar) 104 | if err != nil { 105 | return nil, err 106 | } 107 | } 108 | if fixedToken != nil { 109 | p.pendingFixedValue = p.pendingFixedValue + fixedToken.value 110 | 111 | continue 112 | } 113 | 114 | openToken, err := p.tryConsumeToken(tokenOpen) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if openToken != nil { 120 | prefix, err := p.consumeText() 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | nameToken, err := p.tryConsumeToken(tokenName) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | regexpOrWildcardToken, err := p.tryConsumeRegexpOrWildcardToken(nameToken) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | suffix, err := p.consumeText() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | if _, err := p.consumeRequiredToken(tokenClose); err != nil { 141 | return nil, fmt.Errorf("missing close token: %w", err) 142 | } 143 | 144 | modifierToken, err := p.tryConsumeModifierToken() 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | if err := p.addPart(prefix, nameToken, regexpOrWildcardToken, suffix, modifierToken); err != nil { 150 | return nil, err 151 | } 152 | 153 | continue 154 | } 155 | 156 | if err := p.maybeAddPartFromPendingFixedValue(); err != nil { 157 | return nil, err 158 | } 159 | 160 | if _, err := p.consumeRequiredToken(tokenEnd); err != nil { 161 | return nil, fmt.Errorf("missing end token: %w", err) 162 | } 163 | } 164 | 165 | return p.partList, nil 166 | } 167 | 168 | type patternParser struct { 169 | tokenList []token 170 | encodingCallback encodingCallback 171 | segmentWildcardRegexp string 172 | partList partList 173 | pendingFixedValue string 174 | index int 175 | nextNumericName float64 176 | } 177 | 178 | // https://urlpattern.spec.whatwg.org/#try-to-consume-a-token 179 | func (p *patternParser) tryConsumeToken(tokenType tokenType) (*token, error) { 180 | // Assert: parser’s index is less than parser’s token list size. 181 | if p.index >= len(p.tokenList) { 182 | return nil, BadParserIndexError 183 | } 184 | 185 | nextToken := p.tokenList[p.index] 186 | if nextToken.tType != tokenType { 187 | return nil, nil 188 | } 189 | 190 | p.index++ 191 | 192 | return &nextToken, nil 193 | } 194 | 195 | // https://urlpattern.spec.whatwg.org/#try-to-consume-a-regexp-or-wildcard-token 196 | func (p *patternParser) tryConsumeRegexpOrWildcardToken(nameToken *token) (*token, error) { 197 | token, err := p.tryConsumeToken(tokenRegexp) 198 | if err != nil { 199 | return nil, err 200 | } 201 | if nameToken == nil && token == nil { 202 | token, err = p.tryConsumeToken(tokenAsterisk) 203 | if err != nil { 204 | return nil, err 205 | } 206 | } 207 | 208 | return token, nil 209 | } 210 | 211 | // https://urlpattern.spec.whatwg.org/#maybe-add-a-part-from-the-pending-fixed-value 212 | func (p *patternParser) maybeAddPartFromPendingFixedValue() error { 213 | if p.pendingFixedValue == "" { 214 | return nil 215 | } 216 | 217 | encodedValue, err := p.encodingCallback(p.pendingFixedValue) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | p.pendingFixedValue = "" 223 | 224 | part := part{pType: partFixedText, value: encodedValue, modifier: partModifierNone} 225 | p.partList = append(p.partList, part) 226 | 227 | return nil 228 | } 229 | 230 | // https://urlpattern.spec.whatwg.org/#try-to-consume-a-modifier-token 231 | func (p *patternParser) tryConsumeModifierToken() (*token, error) { 232 | token, err := p.tryConsumeToken(tokenOtherModifier) 233 | if err != nil { 234 | return nil, err 235 | } 236 | if token != nil { 237 | return token, nil 238 | } 239 | 240 | return p.tryConsumeToken(tokenAsterisk) 241 | } 242 | 243 | // https://urlpattern.spec.whatwg.org/#add-a-part 244 | func (p *patternParser) addPart(prefix string, nameToken *token, regexpOrWildcardToken *token, suffix string, modifierToken *token) error { 245 | modifier := partModifierNone 246 | if modifierToken != nil { 247 | switch modifierToken.value { 248 | case "?": 249 | modifier = partModifierOptional 250 | case "*": 251 | modifier = partModifierZeroOrMore 252 | case "+": 253 | modifier = partModifierOneOrMore 254 | } 255 | } 256 | 257 | if nameToken == nil && regexpOrWildcardToken == nil && modifier == partModifierNone { 258 | p.pendingFixedValue = p.pendingFixedValue + prefix 259 | 260 | return nil 261 | } 262 | 263 | if err := p.maybeAddPartFromPendingFixedValue(); err != nil { 264 | return err 265 | } 266 | 267 | if nameToken == nil && regexpOrWildcardToken == nil { 268 | // Assert: suffix is the empty string. 269 | if suffix != "" { 270 | return NonEmptySuffixError 271 | } 272 | 273 | if prefix == "" { 274 | return nil 275 | } 276 | 277 | encodedValue, err := p.encodingCallback(prefix) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | part := part{pType: partFixedText, value: encodedValue, modifier: modifier} 283 | p.partList = append(p.partList, part) 284 | 285 | return nil 286 | } 287 | 288 | regexpValue := "" 289 | if regexpOrWildcardToken == nil { 290 | regexpValue = p.segmentWildcardRegexp 291 | } else if regexpOrWildcardToken.tType == tokenAsterisk { 292 | regexpValue = fullWildcardRegexpValue 293 | } else { 294 | regexpValue = regexpOrWildcardToken.value 295 | } 296 | 297 | pType := partRegexp 298 | switch regexpValue { 299 | case p.segmentWildcardRegexp: 300 | pType = partSegmentWildcard 301 | regexpValue = "" 302 | case fullWildcardRegexpValue: 303 | pType = partFullWildcard 304 | regexpValue = "" 305 | 306 | } 307 | 308 | name := "" 309 | if nameToken != nil { 310 | name = nameToken.value 311 | } else if regexpOrWildcardToken != nil { 312 | name = strconv.FormatFloat(p.nextNumericName, 'f', -1, 64) 313 | p.nextNumericName++ 314 | } 315 | 316 | if p.isDuplicateName(name) { 317 | return DuplicatePartNameError 318 | } 319 | 320 | encodedPrefix, err := p.encodingCallback(prefix) 321 | if err != nil { 322 | return err 323 | } 324 | 325 | encodedSuffix, err := p.encodingCallback(suffix) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | part := part{pType: pType, value: regexpValue, modifier: modifier, name: name, prefix: encodedPrefix, suffix: encodedSuffix} 331 | p.partList = append(p.partList, part) 332 | 333 | return nil 334 | } 335 | 336 | // https://urlpattern.spec.whatwg.org/#is-a-duplicate-name 337 | func (p *patternParser) isDuplicateName(name string) bool { 338 | for _, part := range p.partList { 339 | if part.name == name { 340 | return true 341 | } 342 | } 343 | 344 | return false 345 | } 346 | 347 | // https://urlpattern.spec.whatwg.org/#consume-text 348 | func (p *patternParser) consumeText() (string, error) { 349 | var result strings.Builder 350 | for { 351 | token, err := p.tryConsumeToken(tokenChar) 352 | if err != nil { 353 | return "", err 354 | } 355 | if token == nil { 356 | token, err = p.tryConsumeToken(tokenEscapedChar) 357 | if err != nil { 358 | return "", err 359 | } 360 | } 361 | if token == nil { 362 | break 363 | } 364 | result.WriteString(token.value) 365 | } 366 | 367 | return result.String(), nil 368 | } 369 | 370 | // https://urlpattern.spec.whatwg.org/#consume-a-required-token 371 | func (p *patternParser) consumeRequiredToken(tokenType tokenType) (*token, error) { 372 | result, err := p.tryConsumeToken(tokenType) 373 | if err != nil { 374 | return nil, err 375 | } 376 | if result == nil { 377 | return nil, RequiredTokenError 378 | } 379 | 380 | return result, nil 381 | } 382 | 383 | // https://urlpattern.spec.whatwg.org/#generate-a-segment-wildcard-regexp 384 | func generateSegmentWildcardRegexp(options options) string { 385 | return "[^" + escapeRegexpString(string(options.delimiterCodePoint)) + "]+?" 386 | } 387 | 388 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-protocol 389 | func canonicalizeProtocol(value string) (string, error) { 390 | if value == "" { 391 | return value, nil 392 | } 393 | 394 | dummyURL, err := urlParser.Parse(value + "://dummy.test") 395 | if err != nil { 396 | return "", err 397 | } 398 | 399 | return dummyURL.Scheme(), nil 400 | } 401 | 402 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-username 403 | func canonicalizeUsername(value string) (string, error) { 404 | if value == "" { 405 | return value, nil 406 | } 407 | 408 | return urlParser.PercentEncodeString(value, url.UserInfoPercentEncodeSet), nil 409 | } 410 | 411 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-password 412 | func canonicalizePassword(value string) (string, error) { 413 | if value == "" { 414 | return value, nil 415 | } 416 | 417 | return urlParser.PercentEncodeString(value, url.UserInfoPercentEncodeSet), nil 418 | } 419 | 420 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-hostname 421 | // https://github.com/whatwg/urlpattern/issues/220#issuecomment-2074613501 422 | func canonicalizeHostname(hostnameValue, protocolValue string) (string, error) { 423 | if hostnameValue == "" { 424 | return hostnameValue, nil 425 | } 426 | 427 | // Dirty workaround for https://github.com/whatwg/urlpattern/issues/206 428 | if hostnameValue[:1] != "[" { 429 | for _, c := range hostnameValue { 430 | if c == '/' || c == '?' || c == '#' || c == ':' || c == '\\' { 431 | return "", errors.New("invalid hostname") 432 | } 433 | } 434 | } 435 | 436 | var ( 437 | u *url.Url 438 | err error 439 | ) 440 | 441 | if protocolValue == "" { 442 | u = hostnameParser.NewUrl() 443 | } else { 444 | u, err = hostnameParser.Parse(protocolValue + "://dummy.test") 445 | if err != nil { 446 | return "", err 447 | } 448 | } 449 | 450 | u, err = hostnameParser.BasicParser(hostnameValue, nil, u, url.StateHostname) 451 | if err != nil { 452 | return "", err 453 | } 454 | 455 | return u.Hostname(), nil 456 | } 457 | 458 | // https://github.com/whatwg/urlpattern/issues/220#issuecomment-2074613501 459 | func canonicalizeDomainName(value string) (string, error) { 460 | return canonicalizeHostname(value, "https") 461 | } 462 | 463 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-port 464 | func canonicalizePort(portValue, protocolValue string) (string, error) { 465 | if portValue == "" { 466 | return portValue, nil 467 | } 468 | 469 | var ( 470 | u *url.Url 471 | err error 472 | ) 473 | 474 | if protocolValue == "" { 475 | u = hostnameParser.NewUrl() 476 | } else { 477 | u, err = hostnameParser.Parse(protocolValue + "://dummy.test") 478 | if err != nil { 479 | return "", err 480 | } 481 | } 482 | 483 | u, err = hostnameParser.BasicParser(portValue, nil, u, url.StatePort) 484 | if err != nil { 485 | return "", err 486 | } 487 | 488 | p := u.Port() 489 | 490 | // This looks like a bug in the spec ("80 " should be considered valid), but there is a test covering this 491 | // Another dirty workaround 492 | if p != portValue { 493 | if dp, ok := DefaultPorts[protocolValue]; ok && portValue == dp { 494 | return p, nil 495 | } 496 | 497 | return "", InvalidPortError 498 | } 499 | 500 | return p, nil 501 | } 502 | 503 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-pathname 504 | // TODO: Note, implementations are free to simply disable slash prepending in their URL parsing code instead of paying the performance penalty of inserting and removing characters in this algorithm. 505 | func canonicalizePathname(value string) (string, error) { 506 | if value == "" { 507 | return value, nil 508 | } 509 | 510 | leadingSlash := []rune(value)[0] == '/' 511 | var modifiedValue strings.Builder 512 | 513 | if !leadingSlash { 514 | modifiedValue.WriteString("/-") 515 | } 516 | 517 | modifiedValue.WriteString(value) 518 | 519 | dummyURL := urlParser.NewUrl() 520 | u, err := urlParser.BasicParser(modifiedValue.String(), nil, dummyURL, url.StatePathStart) 521 | if err != nil { 522 | return "", err 523 | } 524 | 525 | result := u.Pathname() 526 | 527 | if !leadingSlash { 528 | result = result[2:] 529 | } 530 | 531 | return result, nil 532 | } 533 | 534 | // https://urlpattern.spec.whatwg.org/#canonicalize-an-opaque-pathname 535 | func canonicalizeOpaquePathname(value string) (string, error) { 536 | if value == "" { 537 | return value, nil 538 | } 539 | 540 | var err error 541 | dummyURL := urlParser.NewUrl() 542 | 543 | u, err := urlParser.BasicParser(value, nil, dummyURL, url.StateOpaquePath) 544 | if err != nil { 545 | return "", err 546 | } 547 | 548 | return u.Pathname(), nil 549 | } 550 | 551 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-search 552 | func canonicalizeSearch(value string) (string, error) { 553 | if value == "" { 554 | return value, nil 555 | } 556 | 557 | dummyURL := urlParser.NewUrl() 558 | 559 | u, err := urlParser.BasicParser(value, nil, dummyURL, url.StateQuery) 560 | if err != nil { 561 | return "", err 562 | } 563 | 564 | return u.Query(), nil 565 | } 566 | 567 | // https://urlpattern.spec.whatwg.org/#canonicalize-a-hash 568 | func canonicalizeHash(value string) (string, error) { 569 | if value == "" { 570 | return value, nil 571 | } 572 | 573 | dummyURL := urlParser.NewUrl() 574 | u, err := urlParser.BasicParser(value, nil, dummyURL, url.StateFragment) 575 | if err != nil { 576 | return "", nil 577 | } 578 | 579 | return u.Fragment(), nil 580 | } 581 | 582 | // https://urlpattern.spec.whatwg.org/#canonicalize-an-ipv6-hostname 583 | func canonicalizeIPv6Hostname(value string) (string, error) { 584 | var result strings.Builder 585 | 586 | for _, c := range value { 587 | if c != '[' && c != ']' && c != ':' && !unicode.Is(unicode.ASCII_Hex_Digit, c) { 588 | return "", InvalidIPv6HostnameError 589 | } 590 | 591 | result.WriteRune(unicode.ToLower(c)) 592 | } 593 | 594 | return result.String(), nil 595 | } 596 | -------------------------------------------------------------------------------- /parts.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | type partType uint8 10 | 11 | const ( 12 | // partFixedText represents a simple fixed text string. 13 | partFixedText partType = iota 14 | // partRegexp represents a matching group with a custom regular expression. 15 | partRegexp 16 | // partSegmentWildcard represents a matching group that matches code points up to the next separator code point. This is typically used for a named group like ":foo" that does not have a custom regular expression. 17 | partSegmentWildcard 18 | // partFullWildcard represents a matching group that greedily matches all code points. This is typically used for the "*" wildcard matching group. 19 | partFullWildcard 20 | ) 21 | 22 | var ( 23 | EmptyPartNameError = errors.New("part's name must not be empty string") 24 | InvalidModifierError = errors.New(`part's modifier must be "zero-or-more" or "one-or-more"`) 25 | InvalidPrefixOrSuffix = errors.New("part's prefix is not the empty string or part's suffix is not the empty string") 26 | InvalidPartNameError = errors.New("part's name is not the empty string or null") 27 | ) 28 | 29 | type partModifier uint8 30 | 31 | const ( 32 | // The part does not have a modifier. 33 | partModifierNone partModifier = iota 34 | // The part has an optional modifier indicated by the U+003F (?) code point. 35 | partModifierOptional 36 | // The part has a "zero or more" modifier indicated by the U+002A (*) code point. 37 | partModifierZeroOrMore 38 | // The part has a "one or more" modifier indicated by the U+002B (+) code point. 39 | partModifierOneOrMore 40 | ) 41 | 42 | type part struct { 43 | pType partType 44 | value string 45 | modifier partModifier 46 | name string 47 | prefix string 48 | suffix string 49 | } 50 | 51 | type partList []part 52 | 53 | // https://urlpattern.spec.whatwg.org/#generate-a-regular-expression-and-name-list 54 | func (pl partList) generateRegularExpressionAndNameList(options options) (string, []string, error) { 55 | var result strings.Builder 56 | nameList := make([]string, 0, len(pl)) 57 | 58 | // the v flag doesn't exist in Go 59 | if options.ignoreCase { 60 | result.WriteString("(?i)") 61 | } 62 | 63 | result.WriteString("\\A(?:") 64 | 65 | for _, p := range pl { 66 | if p.pType == partFixedText { 67 | if p.modifier == partModifierNone { 68 | result.WriteString(escapeRegexpString(p.value)) 69 | } else { 70 | result.WriteString("(?:") 71 | result.WriteString(escapeRegexpString(p.value)) 72 | result.WriteByte(')') 73 | 74 | if modifierToString := convertModifierToString(p.modifier); modifierToString != 0 { 75 | result.WriteByte(modifierToString) 76 | } 77 | } 78 | 79 | continue 80 | } 81 | 82 | // Assert: part's name is not the empty string. 83 | if p.name == "" { 84 | return "", nil, EmptyPartNameError 85 | } 86 | 87 | nameList = append(nameList, p.name) 88 | 89 | var regexpValue string 90 | switch p.pType { 91 | case partSegmentWildcard: 92 | regexpValue = generateSegmentWildcardRegexp(options) 93 | case partFullWildcard: 94 | regexpValue = fullWildcardRegexpValue 95 | default: 96 | regexpValue = p.value 97 | } 98 | 99 | if p.prefix == "" && p.suffix == "" { 100 | switch p.modifier { 101 | case partModifierNone, partModifierOptional: 102 | result.WriteByte('(') 103 | result.WriteString(regexpValue) 104 | result.WriteByte(')') 105 | 106 | if modifierToString := convertModifierToString(p.modifier); modifierToString != 0 { 107 | result.WriteByte(modifierToString) 108 | } 109 | 110 | default: 111 | result.WriteString("((?:") 112 | result.WriteString(regexpValue) 113 | result.WriteByte(')') 114 | 115 | if modifierToString := convertModifierToString(p.modifier); modifierToString != 0 { 116 | result.WriteByte(modifierToString) 117 | } 118 | 119 | result.WriteByte(')') 120 | } 121 | 122 | continue 123 | } 124 | 125 | if p.modifier == partModifierNone || p.modifier == partModifierOptional { 126 | result.WriteString("(?:") 127 | result.WriteString(escapeRegexpString((p.prefix))) 128 | result.WriteByte('(') 129 | result.WriteString(regexpValue) 130 | result.WriteByte(')') 131 | result.WriteString(escapeRegexpString((p.suffix))) 132 | result.WriteByte(')') 133 | 134 | if modifierToString := convertModifierToString(p.modifier); modifierToString != 0 { 135 | result.WriteByte(modifierToString) 136 | } 137 | 138 | continue 139 | } 140 | 141 | // Assert: part’s modifier is "zero-or-more" or "one-or-more". 142 | if p.modifier != partModifierZeroOrMore && p.modifier != partModifierOneOrMore { 143 | return "", nil, InvalidModifierError 144 | } 145 | 146 | // Assert: part’s prefix is not the empty string or part’s suffix is not the empty string. 147 | if p.prefix == "" && p.suffix == "" { 148 | return "", nil, InvalidPrefixOrSuffix 149 | } 150 | 151 | result.WriteString("(?:") 152 | result.WriteString(escapeRegexpString(p.prefix)) 153 | result.WriteString("((?:") 154 | result.WriteString(regexpValue) 155 | result.WriteString(")(?:") 156 | result.WriteString(escapeRegexpString(p.suffix)) 157 | result.WriteString(escapeRegexpString(p.prefix)) 158 | result.WriteString("(?:") 159 | result.WriteString(regexpValue) 160 | result.WriteString("))*)") 161 | result.WriteString(escapeRegexpString(p.suffix)) 162 | result.WriteByte(')') 163 | if p.modifier == partModifierZeroOrMore { 164 | result.WriteByte('?') 165 | } 166 | } 167 | 168 | result.WriteString(")\\z") 169 | 170 | return result.String(), nameList, nil 171 | } 172 | 173 | // https://urlpattern.spec.whatwg.org/#generate-a-pattern-string 174 | func (pl partList) generatePatternString(options options) (string, error) { 175 | var result strings.Builder 176 | 177 | maxIndex := len(pl) - 1 178 | var previousPart *part 179 | var nextPart *part 180 | for index, part := range pl { 181 | if index > 0 { 182 | previousPart = &pl[index-1] 183 | } 184 | if index < maxIndex { 185 | nextPart = &pl[index+1] 186 | } else { 187 | nextPart = nil 188 | } 189 | 190 | if part.pType == partFixedText { 191 | if part.modifier == partModifierNone { 192 | result.WriteString(escapePatternString(part.value)) 193 | 194 | continue 195 | } 196 | 197 | result.WriteByte('{') 198 | result.WriteString(escapePatternString(part.value)) 199 | result.WriteByte('}') 200 | if modifier := convertModifierToString(part.modifier); modifier != 0 { 201 | result.WriteByte(modifier) 202 | } 203 | 204 | continue 205 | } 206 | 207 | customName := !unicode.IsDigit([]rune(part.name)[0]) 208 | needGrouping := part.suffix != "" || (part.prefix != "" && part.prefix != string(options.prefixCodePoint)) 209 | 210 | if !needGrouping && 211 | customName && 212 | part.pType == partSegmentWildcard && 213 | part.modifier == partModifierNone && 214 | nextPart != nil && 215 | nextPart.prefix == "" && 216 | nextPart.suffix == "" { 217 | if nextPart.pType == partFixedText { 218 | if isValidNameCodePoint([]rune(nextPart.value)[0], false) { 219 | needGrouping = true 220 | } 221 | } else if unicode.IsDigit([]rune(nextPart.name)[0]) { 222 | needGrouping = true 223 | } 224 | } 225 | 226 | if !needGrouping && 227 | part.prefix == "" && 228 | previousPart != nil && 229 | previousPart.pType == partFixedText && 230 | []rune(previousPart.value)[len(previousPart.value)-1] == rune(options.prefixCodePoint) { 231 | needGrouping = true 232 | } 233 | 234 | // Assert: part’s name is not the empty string or null. 235 | if part.name == "" { 236 | return "", InvalidPartNameError 237 | } 238 | 239 | if needGrouping { 240 | result.WriteByte('{') 241 | } 242 | 243 | result.WriteString(escapePatternString(part.prefix)) 244 | 245 | if customName { 246 | result.WriteByte(':') 247 | result.WriteString(part.name) 248 | } 249 | 250 | switch part.pType { 251 | case partRegexp: 252 | result.WriteByte('(') 253 | result.WriteString(part.value) 254 | result.WriteByte(')') 255 | 256 | case partSegmentWildcard: 257 | if !customName { 258 | result.WriteByte('(') 259 | result.WriteString(generateSegmentWildcardRegexp(options)) 260 | result.WriteByte(')') 261 | } 262 | 263 | case partFullWildcard: 264 | if !customName && (previousPart == nil || 265 | previousPart.pType == partFixedText || 266 | previousPart.modifier != partModifierNone || 267 | needGrouping || 268 | part.prefix != "") { 269 | result.WriteByte('*') 270 | } else { 271 | result.WriteByte('(') 272 | result.WriteString(fullWildcardRegexpValue) 273 | result.WriteByte(')') 274 | } 275 | } 276 | 277 | if part.pType == partSegmentWildcard && 278 | customName && 279 | part.suffix != "" && 280 | isValidNameCodePoint([]rune(part.suffix)[0], false) { 281 | result.WriteByte('\\') 282 | } 283 | 284 | result.WriteString(escapePatternString(part.suffix)) 285 | 286 | if needGrouping { 287 | result.WriteByte('}') 288 | } 289 | 290 | if modifierToString := convertModifierToString(part.modifier); modifierToString != 0 { 291 | result.WriteByte(modifierToString) 292 | } 293 | } 294 | 295 | return result.String(), nil 296 | } 297 | 298 | // https://urlpattern.spec.whatwg.org/#convert-a-modifier-to-a-string 299 | func convertModifierToString(m partModifier) byte { 300 | switch m { 301 | case partModifierZeroOrMore: 302 | return '*' 303 | case partModifierOptional: 304 | return '?' 305 | case partModifierOneOrMore: 306 | return '+' 307 | default: 308 | return 0 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /testdata/urlpatterntestdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pattern": [{ "pathname": "/foo/bar" }], 4 | "inputs": [{ "pathname": "/foo/bar" }], 5 | "expected_match": { 6 | "pathname": { "input": "/foo/bar", "groups": {} } 7 | } 8 | }, 9 | { 10 | "pattern": [{ "pathname": "/foo/bar" }], 11 | "inputs": [{ "pathname": "/foo/ba" }], 12 | "expected_match": null 13 | }, 14 | { 15 | "pattern": [{ "pathname": "/foo/bar" }], 16 | "inputs": [{ "pathname": "/foo/bar/" }], 17 | "expected_match": null 18 | }, 19 | { 20 | "pattern": [{ "pathname": "/foo/bar" }], 21 | "inputs": [{ "pathname": "/foo/bar/baz" }], 22 | "expected_match": null 23 | }, 24 | { 25 | "pattern": [{ "pathname": "/foo/bar" }], 26 | "inputs": [ "https://example.com/foo/bar" ], 27 | "expected_match": { 28 | "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, 29 | "pathname": { "input": "/foo/bar", "groups": {} }, 30 | "protocol": { "input": "https", "groups": { "0": "https" } } 31 | } 32 | }, 33 | { 34 | "pattern": [{ "pathname": "/foo/bar" }], 35 | "inputs": [ "https://example.com/foo/bar/baz" ], 36 | "expected_match": null 37 | }, 38 | { 39 | "pattern": [{ "pathname": "/foo/bar" }], 40 | "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], 41 | "expected_match": { 42 | "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, 43 | "pathname": { "input": "/foo/bar", "groups": {} } 44 | } 45 | }, 46 | { 47 | "pattern": [{ "pathname": "/foo/bar" }], 48 | "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar/baz" }], 49 | "expected_match": null 50 | }, 51 | { 52 | "pattern": [{ "pathname": "/foo/bar" }], 53 | "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], 54 | "expected_match": { 55 | "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, 56 | "pathname": { "input": "/foo/bar", "groups": {} }, 57 | "protocol": { "input": "https", "groups": { "0": "https" } } 58 | } 59 | }, 60 | { 61 | "pattern": [{ "pathname": "/foo/bar" }], 62 | "inputs": [{ "pathname": "/foo/bar/baz", 63 | "baseURL": "https://example.com" }], 64 | "expected_match": null 65 | }, 66 | { 67 | "pattern": [{ "pathname": "/foo/bar", 68 | "baseURL": "https://example.com?query#hash" }], 69 | "inputs": [{ "pathname": "/foo/bar" }], 70 | "expected_match": null 71 | }, 72 | { 73 | "pattern": [{ "pathname": "/foo/bar", 74 | "baseURL": "https://example.com?query#hash" }], 75 | "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], 76 | "expected_match": null 77 | }, 78 | { 79 | "pattern": [{ "pathname": "/foo/bar", 80 | "baseURL": "https://example.com?query#hash" }], 81 | "inputs": [{ "protocol": "https", "hostname": "example.com", 82 | "pathname": "/foo/bar" }], 83 | "exactly_empty_components": [ "port" ], 84 | "expected_match": { 85 | "hostname": { "input": "example.com", "groups": {} }, 86 | "pathname": { "input": "/foo/bar", "groups": {} }, 87 | "protocol": { "input": "https", "groups": {} } 88 | } 89 | }, 90 | { 91 | "pattern": [{ "pathname": "/foo/bar", 92 | "baseURL": "https://example.com" }], 93 | "inputs": [{ "protocol": "https", "hostname": "example.com", 94 | "pathname": "/foo/bar" }], 95 | "exactly_empty_components": [ "port" ], 96 | "expected_match": { 97 | "hostname": { "input": "example.com", "groups": {} }, 98 | "pathname": { "input": "/foo/bar", "groups": {} }, 99 | "protocol": { "input": "https", "groups": {} } 100 | } 101 | }, 102 | { 103 | "pattern": [{ "pathname": "/foo/bar", 104 | "baseURL": "https://example.com" }], 105 | "inputs": [{ "protocol": "https", "hostname": "example.com", 106 | "pathname": "/foo/bar/baz" }], 107 | "expected_match": null 108 | }, 109 | { 110 | "pattern": [{ "pathname": "/foo/bar", 111 | "baseURL": "https://example.com?query#hash" }], 112 | "inputs": [{ "protocol": "https", "hostname": "example.com", 113 | "pathname": "/foo/bar", "search": "otherquery", 114 | "hash": "otherhash" }], 115 | "exactly_empty_components": [ "port" ], 116 | "expected_match": { 117 | "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, 118 | "hostname": { "input": "example.com", "groups": {} }, 119 | "pathname": { "input": "/foo/bar", "groups": {} }, 120 | "protocol": { "input": "https", "groups": {} }, 121 | "search": { "input": "otherquery", "groups": { "0": "otherquery" } } 122 | } 123 | }, 124 | { 125 | "pattern": [{ "pathname": "/foo/bar", 126 | "baseURL": "https://example.com" }], 127 | "inputs": [{ "protocol": "https", "hostname": "example.com", 128 | "pathname": "/foo/bar", "search": "otherquery", 129 | "hash": "otherhash" }], 130 | "exactly_empty_components": [ "port" ], 131 | "expected_match": { 132 | "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, 133 | "hostname": { "input": "example.com", "groups": {} }, 134 | "pathname": { "input": "/foo/bar", "groups": {} }, 135 | "protocol": { "input": "https", "groups": {} }, 136 | "search": { "input": "otherquery", "groups": { "0": "otherquery" } } 137 | } 138 | }, 139 | { 140 | "pattern": [{ "pathname": "/foo/bar", 141 | "baseURL": "https://example.com?otherquery#otherhash" }], 142 | "inputs": [{ "protocol": "https", "hostname": "example.com", 143 | "pathname": "/foo/bar", "search": "otherquery", 144 | "hash": "otherhash" }], 145 | "exactly_empty_components": [ "port" ], 146 | "expected_match": { 147 | "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, 148 | "hostname": { "input": "example.com", "groups": {} }, 149 | "pathname": { "input": "/foo/bar", "groups": {} }, 150 | "protocol": { "input": "https", "groups": {} }, 151 | "search": { "input": "otherquery", "groups": { "0": "otherquery" } } 152 | } 153 | }, 154 | { 155 | "pattern": [{ "pathname": "/foo/bar", 156 | "baseURL": "https://example.com?query#hash" }], 157 | "inputs": [ "https://example.com/foo/bar" ], 158 | "exactly_empty_components": [ "port" ], 159 | "expected_match": { 160 | "hostname": { "input": "example.com", "groups": {} }, 161 | "pathname": { "input": "/foo/bar", "groups": {} }, 162 | "protocol": { "input": "https", "groups": {} } 163 | } 164 | }, 165 | { 166 | "pattern": [{ "pathname": "/foo/bar", 167 | "baseURL": "https://example.com?query#hash" }], 168 | "inputs": [ "https://example.com/foo/bar?otherquery#otherhash" ], 169 | "exactly_empty_components": [ "port" ], 170 | "expected_match": { 171 | "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, 172 | "hostname": { "input": "example.com", "groups": {} }, 173 | "pathname": { "input": "/foo/bar", "groups": {} }, 174 | "protocol": { "input": "https", "groups": {} }, 175 | "search": { "input": "otherquery", "groups": { "0": "otherquery" } } 176 | } 177 | }, 178 | { 179 | "pattern": [{ "pathname": "/foo/bar", 180 | "baseURL": "https://example.com?query#hash" }], 181 | "inputs": [ "https://example.com/foo/bar?query#hash" ], 182 | "exactly_empty_components": [ "port" ], 183 | "expected_match": { 184 | "hash": { "input": "hash", "groups": { "0": "hash" } }, 185 | "hostname": { "input": "example.com", "groups": {} }, 186 | "pathname": { "input": "/foo/bar", "groups": {} }, 187 | "protocol": { "input": "https", "groups": {} }, 188 | "search": { "input": "query", "groups": { "0": "query" } } 189 | } 190 | }, 191 | { 192 | "pattern": [{ "pathname": "/foo/bar", 193 | "baseURL": "https://example.com?query#hash" }], 194 | "inputs": [ "https://example.com/foo/bar/baz" ], 195 | "expected_match": null 196 | }, 197 | { 198 | "pattern": [{ "pathname": "/foo/bar", 199 | "baseURL": "https://example.com?query#hash" }], 200 | "inputs": [ "https://other.com/foo/bar" ], 201 | "expected_match": null 202 | }, 203 | { 204 | "pattern": [{ "pathname": "/foo/bar", 205 | "baseURL": "https://example.com?query#hash" }], 206 | "inputs": [ "http://other.com/foo/bar" ], 207 | "expected_match": null 208 | }, 209 | { 210 | "pattern": [{ "pathname": "/foo/bar", 211 | "baseURL": "https://example.com?query#hash" }], 212 | "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], 213 | "exactly_empty_components": [ "port" ], 214 | "expected_match": { 215 | "hostname": { "input": "example.com", "groups": {} }, 216 | "pathname": { "input": "/foo/bar", "groups": {} }, 217 | "protocol": { "input": "https", "groups": {} } 218 | } 219 | }, 220 | { 221 | "pattern": [{ "pathname": "/foo/bar", 222 | "baseURL": "https://example.com?query#hash" }], 223 | "inputs": [{ "pathname": "/foo/bar", 224 | "baseURL": "https://example.com?query#hash" }], 225 | "exactly_empty_components": [ "port" ], 226 | "expected_match": { 227 | "hostname": { "input": "example.com", "groups": {} }, 228 | "pathname": { "input": "/foo/bar", "groups": {} }, 229 | "protocol": { "input": "https", "groups": {} } 230 | } 231 | }, 232 | { 233 | "pattern": [{ "pathname": "/foo/bar", 234 | "baseURL": "https://example.com?query#hash" }], 235 | "inputs": [{ "pathname": "/foo/bar/baz", 236 | "baseURL": "https://example.com" }], 237 | "expected_match": null 238 | }, 239 | { 240 | "pattern": [{ "pathname": "/foo/bar", 241 | "baseURL": "https://example.com?query#hash" }], 242 | "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://other.com" }], 243 | "expected_match": null 244 | }, 245 | { 246 | "pattern": [{ "pathname": "/foo/bar", 247 | "baseURL": "https://example.com?query#hash" }], 248 | "inputs": [{ "pathname": "/foo/bar", "baseURL": "http://example.com" }], 249 | "expected_match": null 250 | }, 251 | { 252 | "pattern": [{ "pathname": "/foo/:bar" }], 253 | "inputs": [{ "pathname": "/foo/bar" }], 254 | "expected_match": { 255 | "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } 256 | } 257 | }, 258 | { 259 | "pattern": [{ "pathname": "/foo/([^\\/]+?)" }], 260 | "inputs": [{ "pathname": "/foo/bar" }], 261 | "expected_match": { 262 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 263 | } 264 | }, 265 | { 266 | "pattern": [{ "pathname": "/foo/:bar" }], 267 | "inputs": [{ "pathname": "/foo/index.html" }], 268 | "expected_match": { 269 | "pathname": { "input": "/foo/index.html", "groups": { "bar": "index.html" } } 270 | } 271 | }, 272 | { 273 | "pattern": [{ "pathname": "/foo/:bar" }], 274 | "inputs": [{ "pathname": "/foo/bar/" }], 275 | "expected_match": null 276 | }, 277 | { 278 | "pattern": [{ "pathname": "/foo/:bar" }], 279 | "inputs": [{ "pathname": "/foo/" }], 280 | "expected_match": null 281 | }, 282 | { 283 | "pattern": [{ "pathname": "/foo/(.*)" }], 284 | "inputs": [{ "pathname": "/foo/bar" }], 285 | "expected_obj": { 286 | "pathname": "/foo/*" 287 | }, 288 | "expected_match": { 289 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 290 | } 291 | }, 292 | { 293 | "pattern": [{ "pathname": "/foo/*" }], 294 | "inputs": [{ "pathname": "/foo/bar" }], 295 | "expected_match": { 296 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 297 | } 298 | }, 299 | { 300 | "pattern": [{ "pathname": "/foo/(.*)" }], 301 | "inputs": [{ "pathname": "/foo/bar/baz" }], 302 | "expected_obj": { 303 | "pathname": "/foo/*" 304 | }, 305 | "expected_match": { 306 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 307 | } 308 | }, 309 | { 310 | "pattern": [{ "pathname": "/foo/*" }], 311 | "inputs": [{ "pathname": "/foo/bar/baz" }], 312 | "expected_match": { 313 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 314 | } 315 | }, 316 | { 317 | "pattern": [{ "pathname": "/foo/(.*)" }], 318 | "inputs": [{ "pathname": "/foo/" }], 319 | "expected_obj": { 320 | "pathname": "/foo/*" 321 | }, 322 | "expected_match": { 323 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 324 | } 325 | }, 326 | { 327 | "pattern": [{ "pathname": "/foo/*" }], 328 | "inputs": [{ "pathname": "/foo/" }], 329 | "expected_match": { 330 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 331 | } 332 | }, 333 | { 334 | "pattern": [{ "pathname": "/foo/(.*)" }], 335 | "inputs": [{ "pathname": "/foo" }], 336 | "expected_obj": { 337 | "pathname": "/foo/*" 338 | }, 339 | "expected_match": null 340 | }, 341 | { 342 | "pattern": [{ "pathname": "/foo/*" }], 343 | "inputs": [{ "pathname": "/foo" }], 344 | "expected_match": null 345 | }, 346 | { 347 | "pattern": [{ "pathname": "/foo/:bar(.*)" }], 348 | "inputs": [{ "pathname": "/foo/bar" }], 349 | "expected_match": { 350 | "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } 351 | } 352 | }, 353 | { 354 | "pattern": [{ "pathname": "/foo/:bar(.*)" }], 355 | "inputs": [{ "pathname": "/foo/bar/baz" }], 356 | "expected_match": { 357 | "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } 358 | } 359 | }, 360 | { 361 | "pattern": [{ "pathname": "/foo/:bar(.*)" }], 362 | "inputs": [{ "pathname": "/foo/" }], 363 | "expected_match": { 364 | "pathname": { "input": "/foo/", "groups": { "bar": "" } } 365 | } 366 | }, 367 | { 368 | "pattern": [{ "pathname": "/foo/:bar(.*)" }], 369 | "inputs": [{ "pathname": "/foo" }], 370 | "expected_match": null 371 | }, 372 | { 373 | "pattern": [{ "pathname": "/foo/:bar?" }], 374 | "inputs": [{ "pathname": "/foo/bar" }], 375 | "expected_match": { 376 | "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } 377 | } 378 | }, 379 | { 380 | "pattern": [{ "pathname": "/foo/:bar?" }], 381 | "inputs": [{ "pathname": "/foo" }], 382 | "//": "The `null` below is translated to undefined in the test harness.", 383 | "expected_match": { 384 | "pathname": { "input": "/foo", "groups": { "bar": null } } 385 | } 386 | }, 387 | { 388 | "pattern": [{ "pathname": "/foo/:bar?" }], 389 | "inputs": [{ "pathname": "/foo/" }], 390 | "expected_match": null 391 | }, 392 | { 393 | "pattern": [{ "pathname": "/foo/:bar?" }], 394 | "inputs": [{ "pathname": "/foobar" }], 395 | "expected_match": null 396 | }, 397 | { 398 | "pattern": [{ "pathname": "/foo/:bar?" }], 399 | "inputs": [{ "pathname": "/foo/bar/baz" }], 400 | "expected_match": null 401 | }, 402 | { 403 | "pattern": [{ "pathname": "/foo/:bar+" }], 404 | "inputs": [{ "pathname": "/foo/bar" }], 405 | "expected_match": { 406 | "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } 407 | } 408 | }, 409 | { 410 | "pattern": [{ "pathname": "/foo/:bar+" }], 411 | "inputs": [{ "pathname": "/foo/bar/baz" }], 412 | "expected_match": { 413 | "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } 414 | } 415 | }, 416 | { 417 | "pattern": [{ "pathname": "/foo/:bar+" }], 418 | "inputs": [{ "pathname": "/foo" }], 419 | "expected_match": null 420 | }, 421 | { 422 | "pattern": [{ "pathname": "/foo/:bar+" }], 423 | "inputs": [{ "pathname": "/foo/" }], 424 | "expected_match": null 425 | }, 426 | { 427 | "pattern": [{ "pathname": "/foo/:bar+" }], 428 | "inputs": [{ "pathname": "/foobar" }], 429 | "expected_match": null 430 | }, 431 | { 432 | "pattern": [{ "pathname": "/foo/:bar*" }], 433 | "inputs": [{ "pathname": "/foo/bar" }], 434 | "expected_match": { 435 | "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } 436 | } 437 | }, 438 | { 439 | "pattern": [{ "pathname": "/foo/:bar*" }], 440 | "inputs": [{ "pathname": "/foo/bar/baz" }], 441 | "expected_match": { 442 | "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } 443 | } 444 | }, 445 | { 446 | "pattern": [{ "pathname": "/foo/:bar*" }], 447 | "inputs": [{ "pathname": "/foo" }], 448 | "//": "The `null` below is translated to undefined in the test harness.", 449 | "expected_match": { 450 | "pathname": { "input": "/foo", "groups": { "bar": null } } 451 | } 452 | }, 453 | { 454 | "pattern": [{ "pathname": "/foo/:bar*" }], 455 | "inputs": [{ "pathname": "/foo/" }], 456 | "expected_match": null 457 | }, 458 | { 459 | "pattern": [{ "pathname": "/foo/:bar*" }], 460 | "inputs": [{ "pathname": "/foobar" }], 461 | "expected_match": null 462 | }, 463 | { 464 | "pattern": [{ "pathname": "/foo/(.*)?" }], 465 | "inputs": [{ "pathname": "/foo/bar" }], 466 | "expected_obj": { 467 | "pathname": "/foo/*?" 468 | }, 469 | "expected_match": { 470 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 471 | } 472 | }, 473 | { 474 | "pattern": [{ "pathname": "/foo/*?" }], 475 | "inputs": [{ "pathname": "/foo/bar" }], 476 | "expected_match": { 477 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 478 | } 479 | }, 480 | { 481 | "pattern": [{ "pathname": "/foo/(.*)?" }], 482 | "inputs": [{ "pathname": "/foo/bar/baz" }], 483 | "expected_obj": { 484 | "pathname": "/foo/*?" 485 | }, 486 | "expected_match": { 487 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 488 | } 489 | }, 490 | { 491 | "pattern": [{ "pathname": "/foo/*?" }], 492 | "inputs": [{ "pathname": "/foo/bar/baz" }], 493 | "expected_match": { 494 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 495 | } 496 | }, 497 | { 498 | "pattern": [{ "pathname": "/foo/(.*)?" }], 499 | "inputs": [{ "pathname": "/foo" }], 500 | "expected_obj": { 501 | "pathname": "/foo/*?" 502 | }, 503 | "//": "The `null` below is translated to undefined in the test harness.", 504 | "expected_match": { 505 | "pathname": { "input": "/foo", "groups": { "0": null } } 506 | } 507 | }, 508 | { 509 | "pattern": [{ "pathname": "/foo/*?" }], 510 | "inputs": [{ "pathname": "/foo" }], 511 | "//": "The `null` below is translated to undefined in the test harness.", 512 | "expected_match": { 513 | "pathname": { "input": "/foo", "groups": { "0": null } } 514 | } 515 | }, 516 | { 517 | "pattern": [{ "pathname": "/foo/(.*)?" }], 518 | "inputs": [{ "pathname": "/foo/" }], 519 | "expected_obj": { 520 | "pathname": "/foo/*?" 521 | }, 522 | "expected_match": { 523 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 524 | } 525 | }, 526 | { 527 | "pattern": [{ "pathname": "/foo/*?" }], 528 | "inputs": [{ "pathname": "/foo/" }], 529 | "expected_match": { 530 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 531 | } 532 | }, 533 | { 534 | "pattern": [{ "pathname": "/foo/(.*)?" }], 535 | "inputs": [{ "pathname": "/foobar" }], 536 | "expected_obj": { 537 | "pathname": "/foo/*?" 538 | }, 539 | "expected_match": null 540 | }, 541 | { 542 | "pattern": [{ "pathname": "/foo/*?" }], 543 | "inputs": [{ "pathname": "/foobar" }], 544 | "expected_match": null 545 | }, 546 | { 547 | "pattern": [{ "pathname": "/foo/(.*)?" }], 548 | "inputs": [{ "pathname": "/fo" }], 549 | "expected_obj": { 550 | "pathname": "/foo/*?" 551 | }, 552 | "expected_match": null 553 | }, 554 | { 555 | "pattern": [{ "pathname": "/foo/*?" }], 556 | "inputs": [{ "pathname": "/fo" }], 557 | "expected_match": null 558 | }, 559 | { 560 | "pattern": [{ "pathname": "/foo/(.*)+" }], 561 | "inputs": [{ "pathname": "/foo/bar" }], 562 | "expected_obj": { 563 | "pathname": "/foo/*+" 564 | }, 565 | "expected_match": { 566 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 567 | } 568 | }, 569 | { 570 | "pattern": [{ "pathname": "/foo/*+" }], 571 | "inputs": [{ "pathname": "/foo/bar" }], 572 | "expected_match": { 573 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 574 | } 575 | }, 576 | { 577 | "pattern": [{ "pathname": "/foo/(.*)+" }], 578 | "inputs": [{ "pathname": "/foo/bar/baz" }], 579 | "expected_obj": { 580 | "pathname": "/foo/*+" 581 | }, 582 | "expected_match": { 583 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 584 | } 585 | }, 586 | { 587 | "pattern": [{ "pathname": "/foo/*+" }], 588 | "inputs": [{ "pathname": "/foo/bar/baz" }], 589 | "expected_match": { 590 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 591 | } 592 | }, 593 | { 594 | "pattern": [{ "pathname": "/foo/(.*)+" }], 595 | "inputs": [{ "pathname": "/foo" }], 596 | "expected_obj": { 597 | "pathname": "/foo/*+" 598 | }, 599 | "expected_match": null 600 | }, 601 | { 602 | "pattern": [{ "pathname": "/foo/*+" }], 603 | "inputs": [{ "pathname": "/foo" }], 604 | "expected_match": null 605 | }, 606 | { 607 | "pattern": [{ "pathname": "/foo/(.*)+" }], 608 | "inputs": [{ "pathname": "/foo/" }], 609 | "expected_obj": { 610 | "pathname": "/foo/*+" 611 | }, 612 | "expected_match": { 613 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 614 | } 615 | }, 616 | { 617 | "pattern": [{ "pathname": "/foo/*+" }], 618 | "inputs": [{ "pathname": "/foo/" }], 619 | "expected_match": { 620 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 621 | } 622 | }, 623 | { 624 | "pattern": [{ "pathname": "/foo/(.*)+" }], 625 | "inputs": [{ "pathname": "/foobar" }], 626 | "expected_obj": { 627 | "pathname": "/foo/*+" 628 | }, 629 | "expected_match": null 630 | }, 631 | { 632 | "pattern": [{ "pathname": "/foo/*+" }], 633 | "inputs": [{ "pathname": "/foobar" }], 634 | "expected_match": null 635 | }, 636 | { 637 | "pattern": [{ "pathname": "/foo/(.*)+" }], 638 | "inputs": [{ "pathname": "/fo" }], 639 | "expected_obj": { 640 | "pathname": "/foo/*+" 641 | }, 642 | "expected_match": null 643 | }, 644 | { 645 | "pattern": [{ "pathname": "/foo/*+" }], 646 | "inputs": [{ "pathname": "/fo" }], 647 | "expected_match": null 648 | }, 649 | { 650 | "pattern": [{ "pathname": "/foo/(.*)*" }], 651 | "inputs": [{ "pathname": "/foo/bar" }], 652 | "expected_obj": { 653 | "pathname": "/foo/**" 654 | }, 655 | "expected_match": { 656 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 657 | } 658 | }, 659 | { 660 | "pattern": [{ "pathname": "/foo/**" }], 661 | "inputs": [{ "pathname": "/foo/bar" }], 662 | "expected_match": { 663 | "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } 664 | } 665 | }, 666 | { 667 | "pattern": [{ "pathname": "/foo/(.*)*" }], 668 | "inputs": [{ "pathname": "/foo/bar/baz" }], 669 | "expected_obj": { 670 | "pathname": "/foo/**" 671 | }, 672 | "expected_match": { 673 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 674 | } 675 | }, 676 | { 677 | "pattern": [{ "pathname": "/foo/**" }], 678 | "inputs": [{ "pathname": "/foo/bar/baz" }], 679 | "expected_match": { 680 | "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } 681 | } 682 | }, 683 | { 684 | "pattern": [{ "pathname": "/foo/(.*)*" }], 685 | "inputs": [{ "pathname": "/foo" }], 686 | "expected_obj": { 687 | "pathname": "/foo/**" 688 | }, 689 | "//": "The `null` below is translated to undefined in the test harness.", 690 | "expected_match": { 691 | "pathname": { "input": "/foo", "groups": { "0": null } } 692 | } 693 | }, 694 | { 695 | "pattern": [{ "pathname": "/foo/**" }], 696 | "inputs": [{ "pathname": "/foo" }], 697 | "//": "The `null` below is translated to undefined in the test harness.", 698 | "expected_match": { 699 | "pathname": { "input": "/foo", "groups": { "0": null } } 700 | } 701 | }, 702 | { 703 | "pattern": [{ "pathname": "/foo/(.*)*" }], 704 | "inputs": [{ "pathname": "/foo/" }], 705 | "expected_obj": { 706 | "pathname": "/foo/**" 707 | }, 708 | "expected_match": { 709 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 710 | } 711 | }, 712 | { 713 | "pattern": [{ "pathname": "/foo/**" }], 714 | "inputs": [{ "pathname": "/foo/" }], 715 | "expected_match": { 716 | "pathname": { "input": "/foo/", "groups": { "0": "" } } 717 | } 718 | }, 719 | { 720 | "pattern": [{ "pathname": "/foo/(.*)*" }], 721 | "inputs": [{ "pathname": "/foobar" }], 722 | "expected_obj": { 723 | "pathname": "/foo/**" 724 | }, 725 | "expected_match": null 726 | }, 727 | { 728 | "pattern": [{ "pathname": "/foo/**" }], 729 | "inputs": [{ "pathname": "/foobar" }], 730 | "expected_match": null 731 | }, 732 | { 733 | "pattern": [{ "pathname": "/foo/(.*)*" }], 734 | "inputs": [{ "pathname": "/fo" }], 735 | "expected_obj": { 736 | "pathname": "/foo/**" 737 | }, 738 | "expected_match": null 739 | }, 740 | { 741 | "pattern": [{ "pathname": "/foo/**" }], 742 | "inputs": [{ "pathname": "/fo" }], 743 | "expected_match": null 744 | }, 745 | { 746 | "pattern": [{ "pathname": "/foo{/bar}" }], 747 | "inputs": [{ "pathname": "/foo/bar" }], 748 | "expected_obj": { 749 | "pathname": "/foo/bar" 750 | }, 751 | "expected_match": { 752 | "pathname": { "input": "/foo/bar", "groups": {} } 753 | } 754 | }, 755 | { 756 | "pattern": [{ "pathname": "/foo{/bar}" }], 757 | "inputs": [{ "pathname": "/foo/bar/baz" }], 758 | "expected_obj": { 759 | "pathname": "/foo/bar" 760 | }, 761 | "expected_match": null 762 | }, 763 | { 764 | "pattern": [{ "pathname": "/foo{/bar}" }], 765 | "inputs": [{ "pathname": "/foo" }], 766 | "expected_obj": { 767 | "pathname": "/foo/bar" 768 | }, 769 | "expected_match": null 770 | }, 771 | { 772 | "pattern": [{ "pathname": "/foo{/bar}" }], 773 | "inputs": [{ "pathname": "/foo/" }], 774 | "expected_obj": { 775 | "pathname": "/foo/bar" 776 | }, 777 | "expected_match": null 778 | }, 779 | { 780 | "pattern": [{ "pathname": "/foo{/bar}?" }], 781 | "inputs": [{ "pathname": "/foo/bar" }], 782 | "expected_match": { 783 | "pathname": { "input": "/foo/bar", "groups": {} } 784 | } 785 | }, 786 | { 787 | "pattern": [{ "pathname": "/foo{/bar}?" }], 788 | "inputs": [{ "pathname": "/foo/bar/baz" }], 789 | "expected_match": null 790 | }, 791 | { 792 | "pattern": [{ "pathname": "/foo{/bar}?" }], 793 | "inputs": [{ "pathname": "/foo" }], 794 | "expected_match": { 795 | "pathname": { "input": "/foo", "groups": {} } 796 | } 797 | }, 798 | { 799 | "pattern": [{ "pathname": "/foo{/bar}?" }], 800 | "inputs": [{ "pathname": "/foo/" }], 801 | "expected_match": null 802 | }, 803 | { 804 | "pattern": [{ "pathname": "/foo{/bar}+" }], 805 | "inputs": [{ "pathname": "/foo/bar" }], 806 | "expected_match": { 807 | "pathname": { "input": "/foo/bar", "groups": {} } 808 | } 809 | }, 810 | { 811 | "pattern": [{ "pathname": "/foo{/bar}+" }], 812 | "inputs": [{ "pathname": "/foo/bar/bar" }], 813 | "expected_match": { 814 | "pathname": { "input": "/foo/bar/bar", "groups": {} } 815 | } 816 | }, 817 | { 818 | "pattern": [{ "pathname": "/foo{/bar}+" }], 819 | "inputs": [{ "pathname": "/foo/bar/baz" }], 820 | "expected_match": null 821 | }, 822 | { 823 | "pattern": [{ "pathname": "/foo{/bar}+" }], 824 | "inputs": [{ "pathname": "/foo" }], 825 | "expected_match": null 826 | }, 827 | { 828 | "pattern": [{ "pathname": "/foo{/bar}+" }], 829 | "inputs": [{ "pathname": "/foo/" }], 830 | "expected_match": null 831 | }, 832 | { 833 | "pattern": [{ "pathname": "/foo{/bar}*" }], 834 | "inputs": [{ "pathname": "/foo/bar" }], 835 | "expected_match": { 836 | "pathname": { "input": "/foo/bar", "groups": {} } 837 | } 838 | }, 839 | { 840 | "pattern": [{ "pathname": "/foo{/bar}*" }], 841 | "inputs": [{ "pathname": "/foo/bar/bar" }], 842 | "expected_match": { 843 | "pathname": { "input": "/foo/bar/bar", "groups": {} } 844 | } 845 | }, 846 | { 847 | "pattern": [{ "pathname": "/foo{/bar}*" }], 848 | "inputs": [{ "pathname": "/foo/bar/baz" }], 849 | "expected_match": null 850 | }, 851 | { 852 | "pattern": [{ "pathname": "/foo{/bar}*" }], 853 | "inputs": [{ "pathname": "/foo" }], 854 | "expected_match": { 855 | "pathname": { "input": "/foo", "groups": {} } 856 | } 857 | }, 858 | { 859 | "pattern": [{ "pathname": "/foo{/bar}*" }], 860 | "inputs": [{ "pathname": "/foo/" }], 861 | "expected_match": null 862 | }, 863 | { 864 | "pattern": [{ "protocol": "(café)" }], 865 | "expected_obj": "error" 866 | }, 867 | { 868 | "pattern": [{ "username": "(café)" }], 869 | "expected_obj": "error" 870 | }, 871 | { 872 | "pattern": [{ "password": "(café)" }], 873 | "expected_obj": "error" 874 | }, 875 | { 876 | "pattern": [{ "hostname": "(café)" }], 877 | "expected_obj": "error" 878 | }, 879 | { 880 | "pattern": [{ "pathname": "(café)" }], 881 | "expected_obj": "error" 882 | }, 883 | { 884 | "pattern": [{ "search": "(café)" }], 885 | "expected_obj": "error" 886 | }, 887 | { 888 | "pattern": [{ "hash": "(café)" }], 889 | "expected_obj": "error" 890 | }, 891 | { 892 | "pattern": [{ "protocol": ":café" }], 893 | "inputs": [{ "protocol": "foo" }], 894 | "expected_match": { 895 | "protocol": { "input": "foo", "groups": { "café": "foo" } } 896 | } 897 | }, 898 | { 899 | "pattern": [{ "username": ":café" }], 900 | "inputs": [{ "username": "foo" }], 901 | "expected_match": { 902 | "username": { "input": "foo", "groups": { "café": "foo" } } 903 | } 904 | }, 905 | { 906 | "pattern": [{ "password": ":café" }], 907 | "inputs": [{ "password": "foo" }], 908 | "expected_match": { 909 | "password": { "input": "foo", "groups": { "café": "foo" } } 910 | } 911 | }, 912 | { 913 | "pattern": [{ "hostname": ":café" }], 914 | "inputs": [{ "hostname": "foo" }], 915 | "expected_match": { 916 | "hostname": { "input": "foo", "groups": { "café": "foo" } } 917 | } 918 | }, 919 | { 920 | "pattern": [{ "pathname": "/:café" }], 921 | "inputs": [{ "pathname": "/foo" }], 922 | "expected_match": { 923 | "pathname": { "input": "/foo", "groups": { "café": "foo" } } 924 | } 925 | }, 926 | { 927 | "pattern": [{ "search": ":café" }], 928 | "inputs": [{ "search": "foo" }], 929 | "expected_match": { 930 | "search": { "input": "foo", "groups": { "café": "foo" } } 931 | } 932 | }, 933 | { 934 | "pattern": [{ "hash": ":café" }], 935 | "inputs": [{ "hash": "foo" }], 936 | "expected_match": { 937 | "hash": { "input": "foo", "groups": { "café": "foo" } } 938 | } 939 | }, 940 | { 941 | "pattern": [{ "protocol": ":\u2118" }], 942 | "inputs": [{ "protocol": "foo" }], 943 | "expected_match": { 944 | "protocol": { "input": "foo", "groups": { "\u2118": "foo" } } 945 | } 946 | }, 947 | { 948 | "pattern": [{ "username": ":\u2118" }], 949 | "inputs": [{ "username": "foo" }], 950 | "expected_match": { 951 | "username": { "input": "foo", "groups": { "\u2118": "foo" } } 952 | } 953 | }, 954 | { 955 | "pattern": [{ "password": ":\u2118" }], 956 | "inputs": [{ "password": "foo" }], 957 | "expected_match": { 958 | "password": { "input": "foo", "groups": { "\u2118": "foo" } } 959 | } 960 | }, 961 | { 962 | "pattern": [{ "hostname": ":\u2118" }], 963 | "inputs": [{ "hostname": "foo" }], 964 | "expected_match": { 965 | "hostname": { "input": "foo", "groups": { "\u2118": "foo" } } 966 | } 967 | }, 968 | { 969 | "pattern": [{ "pathname": "/:\u2118" }], 970 | "inputs": [{ "pathname": "/foo" }], 971 | "expected_match": { 972 | "pathname": { "input": "/foo", "groups": { "\u2118": "foo" } } 973 | } 974 | }, 975 | { 976 | "pattern": [{ "search": ":\u2118" }], 977 | "inputs": [{ "search": "foo" }], 978 | "expected_match": { 979 | "search": { "input": "foo", "groups": { "\u2118": "foo" } } 980 | } 981 | }, 982 | { 983 | "pattern": [{ "hash": ":\u2118" }], 984 | "inputs": [{ "hash": "foo" }], 985 | "expected_match": { 986 | "hash": { "input": "foo", "groups": { "\u2118": "foo" } } 987 | } 988 | }, 989 | { 990 | "pattern": [{ "protocol": ":\u3400" }], 991 | "inputs": [{ "protocol": "foo" }], 992 | "expected_match": { 993 | "protocol": { "input": "foo", "groups": { "\u3400": "foo" } } 994 | } 995 | }, 996 | { 997 | "pattern": [{ "username": ":\u3400" }], 998 | "inputs": [{ "username": "foo" }], 999 | "expected_match": { 1000 | "username": { "input": "foo", "groups": { "\u3400": "foo" } } 1001 | } 1002 | }, 1003 | { 1004 | "pattern": [{ "password": ":\u3400" }], 1005 | "inputs": [{ "password": "foo" }], 1006 | "expected_match": { 1007 | "password": { "input": "foo", "groups": { "\u3400": "foo" } } 1008 | } 1009 | }, 1010 | { 1011 | "pattern": [{ "hostname": ":\u3400" }], 1012 | "inputs": [{ "hostname": "foo" }], 1013 | "expected_match": { 1014 | "hostname": { "input": "foo", "groups": { "\u3400": "foo" } } 1015 | } 1016 | }, 1017 | { 1018 | "pattern": [{ "pathname": "/:\u3400" }], 1019 | "inputs": [{ "pathname": "/foo" }], 1020 | "expected_match": { 1021 | "pathname": { "input": "/foo", "groups": { "\u3400": "foo" } } 1022 | } 1023 | }, 1024 | { 1025 | "pattern": [{ "search": ":\u3400" }], 1026 | "inputs": [{ "search": "foo" }], 1027 | "expected_match": { 1028 | "search": { "input": "foo", "groups": { "\u3400": "foo" } } 1029 | } 1030 | }, 1031 | { 1032 | "pattern": [{ "hash": ":\u3400" }], 1033 | "inputs": [{ "hash": "foo" }], 1034 | "expected_match": { 1035 | "hash": { "input": "foo", "groups": { "\u3400": "foo" } } 1036 | } 1037 | }, 1038 | { 1039 | "pattern": [{ "protocol": "(.*)" }], 1040 | "inputs": [{ "protocol" : "café" }], 1041 | "expected_obj": { 1042 | "protocol": "*" 1043 | }, 1044 | "expected_match": null 1045 | }, 1046 | { 1047 | "pattern": [{ "protocol": "(.*)" }], 1048 | "inputs": [{ "protocol": "cafe" }], 1049 | "expected_obj": { 1050 | "protocol": "*" 1051 | }, 1052 | "expected_match": { 1053 | "protocol": { "input": "cafe", "groups": { "0": "cafe" }} 1054 | } 1055 | }, 1056 | { 1057 | "pattern": [{ "protocol": "foo-bar" }], 1058 | "inputs": [{ "protocol": "foo-bar" }], 1059 | "expected_match": { 1060 | "protocol": { "input": "foo-bar", "groups": {} } 1061 | } 1062 | }, 1063 | { 1064 | "pattern": [{ "username": "caf%C3%A9" }], 1065 | "inputs": [{ "username" : "café" }], 1066 | "expected_match": { 1067 | "username": { "input": "caf%C3%A9", "groups": {}} 1068 | } 1069 | }, 1070 | { 1071 | "pattern": [{ "username": "café" }], 1072 | "inputs": [{ "username" : "café" }], 1073 | "expected_obj": { 1074 | "username": "caf%C3%A9" 1075 | }, 1076 | "expected_match": { 1077 | "username": { "input": "caf%C3%A9", "groups": {}} 1078 | } 1079 | }, 1080 | { 1081 | "pattern": [{ "username": "caf%c3%a9" }], 1082 | "inputs": [{ "username" : "café" }], 1083 | "expected_match": null 1084 | }, 1085 | { 1086 | "pattern": [{ "password": "caf%C3%A9" }], 1087 | "inputs": [{ "password" : "café" }], 1088 | "expected_match": { 1089 | "password": { "input": "caf%C3%A9", "groups": {}} 1090 | } 1091 | }, 1092 | { 1093 | "pattern": [{ "password": "café" }], 1094 | "inputs": [{ "password" : "café" }], 1095 | "expected_obj": { 1096 | "password": "caf%C3%A9" 1097 | }, 1098 | "expected_match": { 1099 | "password": { "input": "caf%C3%A9", "groups": {}} 1100 | } 1101 | }, 1102 | { 1103 | "pattern": [{ "password": "caf%c3%a9" }], 1104 | "inputs": [{ "password" : "café" }], 1105 | "expected_match": null 1106 | }, 1107 | { 1108 | "pattern": [{ "hostname": "xn--caf-dma.com" }], 1109 | "inputs": [{ "hostname" : "café.com" }], 1110 | "expected_match": { 1111 | "hostname": { "input": "xn--caf-dma.com", "groups": {}} 1112 | } 1113 | }, 1114 | { 1115 | "pattern": [{ "hostname": "café.com" }], 1116 | "inputs": [{ "hostname" : "café.com" }], 1117 | "expected_obj": { 1118 | "hostname": "xn--caf-dma.com" 1119 | }, 1120 | "expected_match": { 1121 | "hostname": { "input": "xn--caf-dma.com", "groups": {}} 1122 | } 1123 | }, 1124 | { 1125 | "pattern": [{ "port": "" }], 1126 | "inputs": [{ "protocol": "http", "port": "80" }], 1127 | "exactly_empty_components": [ "port" ], 1128 | "expected_match": { 1129 | "protocol": { "input": "http", "groups": { "0": "http" }} 1130 | } 1131 | }, 1132 | { 1133 | "pattern": [{ "protocol": "http", "port": "80" }], 1134 | "inputs": [{ "protocol": "http", "port": "80" }], 1135 | "exactly_empty_components": [ "port" ], 1136 | "expected_match": { 1137 | "protocol": { "input": "http", "groups": {}} 1138 | } 1139 | }, 1140 | { 1141 | "pattern": [{ "protocol": "http", "port": "80{20}?" }], 1142 | "inputs": [{ "protocol": "http", "port": "80" }], 1143 | "expected_match": null 1144 | }, 1145 | { 1146 | "pattern": [{ "protocol": "http", "port": "80 " }], 1147 | "inputs": [{ "protocol": "http", "port": "80" }], 1148 | "expected_obj": "error" 1149 | }, 1150 | { 1151 | "pattern": [{ "port": "80" }], 1152 | "inputs": [{ "protocol": "http", "port": "80" }], 1153 | "expected_match": null 1154 | }, 1155 | { 1156 | "pattern": [{ "protocol": "http{s}?", "port": "80" }], 1157 | "inputs": [{ "protocol": "http", "port": "80" }], 1158 | "expected_match": null 1159 | }, 1160 | { 1161 | "pattern": [{ "port": "80" }], 1162 | "inputs": [{ "port": "80" }], 1163 | "expected_match": { 1164 | "port": { "input": "80", "groups": {}} 1165 | } 1166 | }, 1167 | { 1168 | "pattern": [{ "port": "(.*)" }], 1169 | "inputs": [{ "port": "invalid80" }], 1170 | "expected_obj": { 1171 | "port": "*" 1172 | }, 1173 | "expected_match": null 1174 | }, 1175 | { 1176 | "pattern": [{ "pathname": "/foo/bar" }], 1177 | "inputs": [{ "pathname": "/foo/./bar" }], 1178 | "expected_match": { 1179 | "pathname": { "input": "/foo/bar", "groups": {}} 1180 | } 1181 | }, 1182 | { 1183 | "pattern": [{ "pathname": "/foo/baz" }], 1184 | "inputs": [{ "pathname": "/foo/bar/../baz" }], 1185 | "expected_match": { 1186 | "pathname": { "input": "/foo/baz", "groups": {}} 1187 | } 1188 | }, 1189 | { 1190 | "pattern": [{ "pathname": "/caf%C3%A9" }], 1191 | "inputs": [{ "pathname": "/café" }], 1192 | "expected_match": { 1193 | "pathname": { "input": "/caf%C3%A9", "groups": {}} 1194 | } 1195 | }, 1196 | { 1197 | "pattern": [{ "pathname": "/café" }], 1198 | "inputs": [{ "pathname": "/café" }], 1199 | "expected_obj": { 1200 | "pathname": "/caf%C3%A9" 1201 | }, 1202 | "expected_match": { 1203 | "pathname": { "input": "/caf%C3%A9", "groups": {}} 1204 | } 1205 | }, 1206 | { 1207 | "pattern": [{ "pathname": "/caf%c3%a9" }], 1208 | "inputs": [{ "pathname": "/café" }], 1209 | "expected_match": null 1210 | }, 1211 | { 1212 | "pattern": [{ "pathname": "/foo/bar" }], 1213 | "inputs": [{ "pathname": "foo/bar" }], 1214 | "expected_match": null 1215 | }, 1216 | { 1217 | "pattern": [{ "pathname": "/foo/bar" }], 1218 | "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], 1219 | "expected_match": { 1220 | "protocol": { "input": "https", "groups": { "0": "https" }}, 1221 | "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, 1222 | "pathname": { "input": "/foo/bar", "groups": {}} 1223 | } 1224 | }, 1225 | { 1226 | "pattern": [{ "pathname": "/foo/../bar" }], 1227 | "inputs": [{ "pathname": "/bar" }], 1228 | "expected_obj": { 1229 | "pathname": "/bar" 1230 | }, 1231 | "expected_match": { 1232 | "pathname": { "input": "/bar", "groups": {}} 1233 | } 1234 | }, 1235 | { 1236 | "pattern": [{ "pathname": "./foo/bar", "baseURL": "https://example.com" }], 1237 | "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], 1238 | "exactly_empty_components": [ "port" ], 1239 | "expected_obj": { 1240 | "pathname": "/foo/bar" 1241 | }, 1242 | "expected_match": { 1243 | "protocol": { "input": "https", "groups": {}}, 1244 | "hostname": { "input": "example.com", "groups": {}}, 1245 | "pathname": { "input": "/foo/bar", "groups": {}} 1246 | } 1247 | }, 1248 | { 1249 | "pattern": [{ "pathname": "", "baseURL": "https://example.com" }], 1250 | "inputs": [{ "pathname": "/", "baseURL": "https://example.com" }], 1251 | "exactly_empty_components": [ "port" ], 1252 | "expected_obj": { 1253 | "pathname": "/" 1254 | }, 1255 | "expected_match": { 1256 | "protocol": { "input": "https", "groups": {}}, 1257 | "hostname": { "input": "example.com", "groups": {}}, 1258 | "pathname": { "input": "/", "groups": {}} 1259 | } 1260 | }, 1261 | { 1262 | "pattern": [{ "pathname": "{/bar}", "baseURL": "https://example.com/foo/" }], 1263 | "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], 1264 | "exactly_empty_components": [ "port" ], 1265 | "expected_obj": { 1266 | "pathname": "/bar" 1267 | }, 1268 | "expected_match": null 1269 | }, 1270 | { 1271 | "pattern": [{ "pathname": "\\/bar", "baseURL": "https://example.com/foo/" }], 1272 | "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], 1273 | "exactly_empty_components": [ "port" ], 1274 | "expected_obj": { 1275 | "pathname": "/bar" 1276 | }, 1277 | "expected_match": null 1278 | }, 1279 | { 1280 | "pattern": [{ "pathname": "b", "baseURL": "https://example.com/foo/" }], 1281 | "inputs": [{ "pathname": "./b", "baseURL": "https://example.com/foo/" }], 1282 | "exactly_empty_components": [ "port" ], 1283 | "expected_obj": { 1284 | "pathname": "/foo/b" 1285 | }, 1286 | "expected_match": { 1287 | "protocol": { "input": "https", "groups": {}}, 1288 | "hostname": { "input": "example.com", "groups": {}}, 1289 | "pathname": { "input": "/foo/b", "groups": {}} 1290 | } 1291 | }, 1292 | { 1293 | "pattern": [{ "pathname": "foo/bar" }], 1294 | "inputs": [ "https://example.com/foo/bar" ], 1295 | "expected_match": null 1296 | }, 1297 | { 1298 | "pattern": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], 1299 | "inputs": [ "https://example.com/foo/bar" ], 1300 | "exactly_empty_components": [ "port" ], 1301 | "expected_obj": { 1302 | "pathname": "/foo/bar" 1303 | }, 1304 | "expected_match": { 1305 | "protocol": { "input": "https", "groups": {}}, 1306 | "hostname": { "input": "example.com", "groups": {}}, 1307 | "pathname": { "input": "/foo/bar", "groups": {}} 1308 | } 1309 | }, 1310 | { 1311 | "pattern": [{ "pathname": ":name.html", "baseURL": "https://example.com" }], 1312 | "inputs": [ "https://example.com/foo.html"] , 1313 | "exactly_empty_components": [ "port" ], 1314 | "expected_obj": { 1315 | "pathname": "/:name.html" 1316 | }, 1317 | "expected_match": { 1318 | "protocol": { "input": "https", "groups": {}}, 1319 | "hostname": { "input": "example.com", "groups": {}}, 1320 | "pathname": { "input": "/foo.html", "groups": { "name": "foo" }} 1321 | } 1322 | }, 1323 | { 1324 | "pattern": [{ "search": "q=caf%C3%A9" }], 1325 | "inputs": [{ "search": "q=café" }], 1326 | "expected_match": { 1327 | "search": { "input": "q=caf%C3%A9", "groups": {}} 1328 | } 1329 | }, 1330 | { 1331 | "pattern": [{ "search": "q=café" }], 1332 | "inputs": [{ "search": "q=café" }], 1333 | "expected_obj": { 1334 | "search": "q=caf%C3%A9" 1335 | }, 1336 | "expected_match": { 1337 | "search": { "input": "q=caf%C3%A9", "groups": {}} 1338 | } 1339 | }, 1340 | { 1341 | "pattern": [{ "search": "q=caf%c3%a9" }], 1342 | "inputs": [{ "search": "q=café" }], 1343 | "expected_match": null 1344 | }, 1345 | { 1346 | "pattern": [{ "hash": "caf%C3%A9" }], 1347 | "inputs": [{ "hash": "café" }], 1348 | "expected_match": { 1349 | "hash": { "input": "caf%C3%A9", "groups": {}} 1350 | } 1351 | }, 1352 | { 1353 | "pattern": [{ "hash": "café" }], 1354 | "inputs": [{ "hash": "café" }], 1355 | "expected_obj": { 1356 | "hash": "caf%C3%A9" 1357 | }, 1358 | "expected_match": { 1359 | "hash": { "input": "caf%C3%A9", "groups": {}} 1360 | } 1361 | }, 1362 | { 1363 | "pattern": [{ "hash": "caf%c3%a9" }], 1364 | "inputs": [{ "hash": "café" }], 1365 | "expected_match": null 1366 | }, 1367 | { 1368 | "pattern": [{ "protocol": "about", "pathname": "(blank|sourcedoc)" }], 1369 | "inputs": [ "about:blank" ], 1370 | "expected_match": { 1371 | "protocol": { "input": "about", "groups": {}}, 1372 | "pathname": { "input": "blank", "groups": { "0": "blank" }} 1373 | } 1374 | }, 1375 | { 1376 | "pattern": [{ "protocol": "data", "pathname": ":number([0-9]+)" }], 1377 | "inputs": [ "data:8675309" ], 1378 | "expected_match": { 1379 | "protocol": { "input": "data", "groups": {}}, 1380 | "pathname": { "input": "8675309", "groups": { "number": "8675309" }} 1381 | } 1382 | }, 1383 | { 1384 | "pattern": [{ "pathname": "/(\\m)" }], 1385 | "expected_obj": "error" 1386 | }, 1387 | { 1388 | "pattern": [{ "pathname": "/foo!" }], 1389 | "inputs": [{ "pathname": "/foo!" }], 1390 | "expected_match": { 1391 | "pathname": { "input": "/foo!", "groups": {}} 1392 | } 1393 | }, 1394 | { 1395 | "pattern": [{ "pathname": "/foo\\:" }], 1396 | "inputs": [{ "pathname": "/foo:" }], 1397 | "expected_match": { 1398 | "pathname": { "input": "/foo:", "groups": {}} 1399 | } 1400 | }, 1401 | { 1402 | "pattern": [{ "pathname": "/foo\\{" }], 1403 | "inputs": [{ "pathname": "/foo{" }], 1404 | "expected_obj": { 1405 | "pathname": "/foo%7B" 1406 | }, 1407 | "expected_match": { 1408 | "pathname": { "input": "/foo%7B", "groups": {}} 1409 | } 1410 | }, 1411 | { 1412 | "pattern": [{ "pathname": "/foo\\(" }], 1413 | "inputs": [{ "pathname": "/foo(" }], 1414 | "expected_match": { 1415 | "pathname": { "input": "/foo(", "groups": {}} 1416 | } 1417 | }, 1418 | { 1419 | "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1420 | "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1421 | "expected_match": { 1422 | "protocol": { "input": "javascript", "groups": {}}, 1423 | "pathname": { "input": "var x = 1;", "groups": {}} 1424 | } 1425 | }, 1426 | { 1427 | "pattern": [{ "pathname": "var x = 1;" }], 1428 | "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1429 | "expected_obj": { 1430 | "pathname": "var%20x%20=%201;" 1431 | }, 1432 | "expected_match": null 1433 | }, 1434 | { 1435 | "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1436 | "inputs": [{ "baseURL": "javascript:var x = 1;" }], 1437 | "expected_match": { 1438 | "protocol": { "input": "javascript", "groups": {}}, 1439 | "pathname": { "input": "var x = 1;", "groups": {}} 1440 | } 1441 | }, 1442 | { 1443 | "pattern": [{ "protocol": "(data|javascript)", "pathname": "var x = 1;" }], 1444 | "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1445 | "expected_match": { 1446 | "protocol": { "input": "javascript", "groups": {"0": "javascript"}}, 1447 | "pathname": { "input": "var x = 1;", "groups": {}} 1448 | } 1449 | }, 1450 | { 1451 | "pattern": [{ "protocol": "(https|javascript)", "pathname": "var x = 1;" }], 1452 | "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], 1453 | "expected_obj": { 1454 | "pathname": "var%20x%20=%201;" 1455 | }, 1456 | "expected_match": null 1457 | }, 1458 | { 1459 | "pattern": [{ "pathname": "var x = 1;" }], 1460 | "inputs": [{ "pathname": "var x = 1;" }], 1461 | "expected_obj": { 1462 | "pathname": "var%20x%20=%201;" 1463 | }, 1464 | "expected_match": { 1465 | "pathname": { "input": "var%20x%20=%201;", "groups": {}} 1466 | } 1467 | }, 1468 | { 1469 | "pattern": [{ "pathname": "/foo/bar" }], 1470 | "inputs": [ "./foo/bar", "https://example.com" ], 1471 | "expected_match": { 1472 | "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, 1473 | "pathname": { "input": "/foo/bar", "groups": {} }, 1474 | "protocol": { "input": "https", "groups": { "0": "https" } } 1475 | } 1476 | }, 1477 | { 1478 | "pattern": [{ "pathname": "/foo/bar" }], 1479 | "inputs": [ { "pathname": "/foo/bar" }, "https://example.com" ], 1480 | "expected_match": "error" 1481 | }, 1482 | { 1483 | "pattern": [ "https://example.com:8080/foo?bar#baz" ], 1484 | "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", 1485 | "baseURL": "https://example.com:8080" }], 1486 | "expected_obj": { 1487 | "protocol": "https", 1488 | "username": "*", 1489 | "password": "*", 1490 | "hostname": "example.com", 1491 | "port": "8080", 1492 | "pathname": "/foo", 1493 | "search": "bar", 1494 | "hash": "baz" 1495 | }, 1496 | "expected_match": { 1497 | "protocol": { "input": "https", "groups": {} }, 1498 | "hostname": { "input": "example.com", "groups": {} }, 1499 | "port": { "input": "8080", "groups": {} }, 1500 | "pathname": { "input": "/foo", "groups": {} }, 1501 | "search": { "input": "bar", "groups": {} }, 1502 | "hash": { "input": "baz", "groups": {} } 1503 | } 1504 | }, 1505 | { 1506 | "pattern": [ "/foo?bar#baz", "https://example.com:8080" ], 1507 | "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", 1508 | "baseURL": "https://example.com:8080" }], 1509 | "expected_obj": { 1510 | "pathname": "/foo", 1511 | "search": "bar", 1512 | "hash": "baz" 1513 | }, 1514 | "expected_match": { 1515 | "protocol": { "input": "https", "groups": {} }, 1516 | "hostname": { "input": "example.com", "groups": {} }, 1517 | "port": { "input": "8080", "groups": {} }, 1518 | "pathname": { "input": "/foo", "groups": {} }, 1519 | "search": { "input": "bar", "groups": {} }, 1520 | "hash": { "input": "baz", "groups": {} } 1521 | } 1522 | }, 1523 | { 1524 | "pattern": [ "/foo" ], 1525 | "expected_obj": "error" 1526 | }, 1527 | { 1528 | "pattern": [ "example.com/foo" ], 1529 | "expected_obj": "error" 1530 | }, 1531 | { 1532 | "pattern": [ "http{s}?://{*.}?example.com/:product/:endpoint" ], 1533 | "inputs": [ "https://sub.example.com/foo/bar" ], 1534 | "exactly_empty_components": [ "port" ], 1535 | "expected_obj": { 1536 | "protocol": "http{s}?", 1537 | "hostname": "{*.}?example.com", 1538 | "pathname": "/:product/:endpoint" 1539 | }, 1540 | "expected_match": { 1541 | "protocol": { "input": "https", "groups": {} }, 1542 | "hostname": { "input": "sub.example.com", "groups": { "0": "sub" } }, 1543 | "pathname": { "input": "/foo/bar", "groups": { "product": "foo", 1544 | "endpoint": "bar" } } 1545 | } 1546 | }, 1547 | { 1548 | "pattern": [ "https://example.com?foo" ], 1549 | "inputs": [ "https://example.com/?foo" ], 1550 | "exactly_empty_components": [ "port" ], 1551 | "expected_obj": { 1552 | "protocol": "https", 1553 | "hostname": "example.com", 1554 | "pathname": "/", 1555 | "search": "foo" 1556 | }, 1557 | "expected_match": { 1558 | "protocol": { "input": "https", "groups": {} }, 1559 | "hostname": { "input": "example.com", "groups": {} }, 1560 | "pathname": { "input": "/", "groups": {} }, 1561 | "search": { "input": "foo", "groups": {} } 1562 | } 1563 | }, 1564 | { 1565 | "pattern": [ "https://example.com#foo" ], 1566 | "inputs": [ "https://example.com/#foo" ], 1567 | "exactly_empty_components": [ "port", "search" ], 1568 | "expected_obj": { 1569 | "protocol": "https", 1570 | "hostname": "example.com", 1571 | "pathname": "/", 1572 | "hash": "foo" 1573 | }, 1574 | "expected_match": { 1575 | "protocol": { "input": "https", "groups": {} }, 1576 | "hostname": { "input": "example.com", "groups": {} }, 1577 | "pathname": { "input": "/", "groups": {} }, 1578 | "hash": { "input": "foo", "groups": {} } 1579 | } 1580 | }, 1581 | { 1582 | "pattern": [ "https://example.com:8080?foo" ], 1583 | "inputs": [ "https://example.com:8080/?foo" ], 1584 | "expected_obj": { 1585 | "protocol": "https", 1586 | "hostname": "example.com", 1587 | "port": "8080", 1588 | "pathname": "/", 1589 | "search": "foo" 1590 | }, 1591 | "expected_match": { 1592 | "protocol": { "input": "https", "groups": {} }, 1593 | "hostname": { "input": "example.com", "groups": {} }, 1594 | "port": { "input": "8080", "groups": {} }, 1595 | "pathname": { "input": "/", "groups": {} }, 1596 | "search": { "input": "foo", "groups": {} } 1597 | } 1598 | }, 1599 | { 1600 | "pattern": [ "https://example.com:8080#foo" ], 1601 | "inputs": [ "https://example.com:8080/#foo" ], 1602 | "exactly_empty_components": [ "search" ], 1603 | "expected_obj": { 1604 | "protocol": "https", 1605 | "hostname": "example.com", 1606 | "port": "8080", 1607 | "pathname": "/", 1608 | "hash": "foo" 1609 | }, 1610 | "expected_match": { 1611 | "protocol": { "input": "https", "groups": {} }, 1612 | "hostname": { "input": "example.com", "groups": {} }, 1613 | "port": { "input": "8080", "groups": {} }, 1614 | "pathname": { "input": "/", "groups": {} }, 1615 | "hash": { "input": "foo", "groups": {} } 1616 | } 1617 | }, 1618 | { 1619 | "pattern": [ "https://example.com/?foo" ], 1620 | "inputs": [ "https://example.com/?foo" ], 1621 | "exactly_empty_components": [ "port" ], 1622 | "expected_obj": { 1623 | "protocol": "https", 1624 | "hostname": "example.com", 1625 | "pathname": "/", 1626 | "search": "foo" 1627 | }, 1628 | "expected_match": { 1629 | "protocol": { "input": "https", "groups": {} }, 1630 | "hostname": { "input": "example.com", "groups": {} }, 1631 | "pathname": { "input": "/", "groups": {} }, 1632 | "search": { "input": "foo", "groups": {} } 1633 | } 1634 | }, 1635 | { 1636 | "pattern": [ "https://example.com/#foo" ], 1637 | "inputs": [ "https://example.com/#foo" ], 1638 | "exactly_empty_components": [ "port", "search" ], 1639 | "expected_obj": { 1640 | "protocol": "https", 1641 | "hostname": "example.com", 1642 | "pathname": "/", 1643 | "hash": "foo" 1644 | }, 1645 | "expected_match": { 1646 | "protocol": { "input": "https", "groups": {} }, 1647 | "hostname": { "input": "example.com", "groups": {} }, 1648 | "pathname": { "input": "/", "groups": {} }, 1649 | "hash": { "input": "foo", "groups": {} } 1650 | } 1651 | }, 1652 | { 1653 | "pattern": [ "https://example.com/*?foo" ], 1654 | "inputs": [ "https://example.com/?foo" ], 1655 | "exactly_empty_components": [ "port" ], 1656 | "expected_obj": { 1657 | "protocol": "https", 1658 | "hostname": "example.com", 1659 | "pathname": "/*?foo" 1660 | }, 1661 | "expected_match": null 1662 | }, 1663 | { 1664 | "pattern": [ "https://example.com/*\\?foo" ], 1665 | "inputs": [ "https://example.com/?foo" ], 1666 | "exactly_empty_components": [ "port" ], 1667 | "expected_obj": { 1668 | "protocol": "https", 1669 | "hostname": "example.com", 1670 | "pathname": "/*", 1671 | "search": "foo" 1672 | }, 1673 | "expected_match": { 1674 | "protocol": { "input": "https", "groups": {} }, 1675 | "hostname": { "input": "example.com", "groups": {} }, 1676 | "pathname": { "input": "/", "groups": { "0": "" } }, 1677 | "search": { "input": "foo", "groups": {} } 1678 | } 1679 | }, 1680 | { 1681 | "pattern": [ "https://example.com/:name?foo" ], 1682 | "inputs": [ "https://example.com/bar?foo" ], 1683 | "exactly_empty_components": [ "port" ], 1684 | "expected_obj": { 1685 | "protocol": "https", 1686 | "hostname": "example.com", 1687 | "pathname": "/:name?foo" 1688 | }, 1689 | "expected_match": null 1690 | }, 1691 | { 1692 | "pattern": [ "https://example.com/:name\\?foo" ], 1693 | "inputs": [ "https://example.com/bar?foo" ], 1694 | "exactly_empty_components": [ "port" ], 1695 | "expected_obj": { 1696 | "protocol": "https", 1697 | "hostname": "example.com", 1698 | "pathname": "/:name", 1699 | "search": "foo" 1700 | }, 1701 | "expected_match": { 1702 | "protocol": { "input": "https", "groups": {} }, 1703 | "hostname": { "input": "example.com", "groups": {} }, 1704 | "pathname": { "input": "/bar", "groups": { "name": "bar" } }, 1705 | "search": { "input": "foo", "groups": {} } 1706 | } 1707 | }, 1708 | { 1709 | "pattern": [ "https://example.com/(bar)?foo" ], 1710 | "inputs": [ "https://example.com/bar?foo" ], 1711 | "exactly_empty_components": [ "port" ], 1712 | "expected_obj": { 1713 | "protocol": "https", 1714 | "hostname": "example.com", 1715 | "pathname": "/(bar)?foo" 1716 | }, 1717 | "expected_match": null 1718 | }, 1719 | { 1720 | "pattern": [ "https://example.com/(bar)\\?foo" ], 1721 | "inputs": [ "https://example.com/bar?foo" ], 1722 | "exactly_empty_components": [ "port" ], 1723 | "expected_obj": { 1724 | "protocol": "https", 1725 | "hostname": "example.com", 1726 | "pathname": "/(bar)", 1727 | "search": "foo" 1728 | }, 1729 | "expected_match": { 1730 | "protocol": { "input": "https", "groups": {} }, 1731 | "hostname": { "input": "example.com", "groups": {} }, 1732 | "pathname": { "input": "/bar", "groups": { "0": "bar" } }, 1733 | "search": { "input": "foo", "groups": {} } 1734 | } 1735 | }, 1736 | { 1737 | "pattern": [ "https://example.com/{bar}?foo" ], 1738 | "inputs": [ "https://example.com/bar?foo" ], 1739 | "exactly_empty_components": [ "port" ], 1740 | "expected_obj": { 1741 | "protocol": "https", 1742 | "hostname": "example.com", 1743 | "pathname": "/{bar}?foo" 1744 | }, 1745 | "expected_match": null 1746 | }, 1747 | { 1748 | "pattern": [ "https://example.com/{bar}\\?foo" ], 1749 | "inputs": [ "https://example.com/bar?foo" ], 1750 | "exactly_empty_components": [ "port" ], 1751 | "expected_obj": { 1752 | "protocol": "https", 1753 | "hostname": "example.com", 1754 | "pathname": "/bar", 1755 | "search": "foo" 1756 | }, 1757 | "expected_match": { 1758 | "protocol": { "input": "https", "groups": {} }, 1759 | "hostname": { "input": "example.com", "groups": {} }, 1760 | "pathname": { "input": "/bar", "groups": {} }, 1761 | "search": { "input": "foo", "groups": {} } 1762 | } 1763 | }, 1764 | { 1765 | "pattern": [ "https://example.com/" ], 1766 | "inputs": [ "https://example.com:8080/" ], 1767 | "exactly_empty_components": [ "port" ], 1768 | "expected_obj": { 1769 | "protocol": "https", 1770 | "hostname": "example.com", 1771 | "port": "", 1772 | "pathname": "/" 1773 | }, 1774 | "expected_match": null 1775 | }, 1776 | { 1777 | "pattern": [ "data:foobar" ], 1778 | "inputs": [ "data:foobar" ], 1779 | "expected_obj": "error" 1780 | }, 1781 | { 1782 | "pattern": [ "data\\:foobar" ], 1783 | "inputs": [ "data:foobar" ], 1784 | "exactly_empty_components": [ "hostname", "port" ], 1785 | "expected_obj": { 1786 | "protocol": "data", 1787 | "pathname": "foobar" 1788 | }, 1789 | "expected_match": { 1790 | "protocol": { "input": "data", "groups": {} }, 1791 | "pathname": { "input": "foobar", "groups": {} } 1792 | } 1793 | }, 1794 | { 1795 | "pattern": [ "https://{sub.}?example.com/foo" ], 1796 | "inputs": [ "https://example.com/foo" ], 1797 | "exactly_empty_components": [ "port" ], 1798 | "expected_obj": { 1799 | "protocol": "https", 1800 | "hostname": "{sub.}?example.com", 1801 | "pathname": "/foo" 1802 | }, 1803 | "expected_match": { 1804 | "protocol": { "input": "https", "groups": {} }, 1805 | "hostname": { "input": "example.com", "groups": {} }, 1806 | "pathname": { "input": "/foo", "groups": {} } 1807 | } 1808 | }, 1809 | { 1810 | "pattern": [ "https://{sub.}?example{.com/}foo" ], 1811 | "inputs": [ "https://example.com/foo" ], 1812 | "expected_obj": "error" 1813 | }, 1814 | { 1815 | "pattern": [ "{https://}example.com/foo" ], 1816 | "inputs": [ "https://example.com/foo" ], 1817 | "expected_obj": "error" 1818 | }, 1819 | { 1820 | "pattern": [ "https://(sub.)?example.com/foo" ], 1821 | "inputs": [ "https://example.com/foo" ], 1822 | "exactly_empty_components": [ "port" ], 1823 | "expected_obj": { 1824 | "protocol": "https", 1825 | "hostname": "(sub.)?example.com", 1826 | "pathname": "/foo" 1827 | }, 1828 | "//": "The `null` below is translated to undefined in the test harness.", 1829 | "expected_match": { 1830 | "protocol": { "input": "https", "groups": {} }, 1831 | "hostname": { "input": "example.com", "groups": { "0": null } }, 1832 | "pathname": { "input": "/foo", "groups": {} } 1833 | } 1834 | }, 1835 | { 1836 | "pattern": [ "https://(sub.)?example(.com/)foo" ], 1837 | "inputs": [ "https://example.com/foo" ], 1838 | "exactly_empty_components": [ "port" ], 1839 | "expected_obj": { 1840 | "protocol": "https", 1841 | "hostname": "(sub.)?example(.com/)foo", 1842 | "pathname": "*" 1843 | }, 1844 | "expected_match": null 1845 | }, 1846 | { 1847 | "pattern": [ "(https://)example.com/foo" ], 1848 | "inputs": [ "https://example.com/foo" ], 1849 | "expected_obj": "error" 1850 | }, 1851 | { 1852 | "pattern": [ "https://{sub{.}}example.com/foo" ], 1853 | "inputs": [ "https://example.com/foo" ], 1854 | "expected_obj": "error" 1855 | }, 1856 | { 1857 | "pattern": [ "https://(sub(?:.))?example.com/foo" ], 1858 | "inputs": [ "https://example.com/foo" ], 1859 | "exactly_empty_components": [ "port" ], 1860 | "expected_obj": { 1861 | "protocol": "https", 1862 | "hostname": "(sub(?:.))?example.com", 1863 | "pathname": "/foo" 1864 | }, 1865 | "//": "The `null` below is translated to undefined in the test harness.", 1866 | "expected_match": { 1867 | "protocol": { "input": "https", "groups": {} }, 1868 | "hostname": { "input": "example.com", "groups": { "0": null } }, 1869 | "pathname": { "input": "/foo", "groups": {} } 1870 | } 1871 | }, 1872 | { 1873 | "pattern": [ "file:///foo/bar" ], 1874 | "inputs": [ "file:///foo/bar" ], 1875 | "exactly_empty_components": [ "hostname", "port" ], 1876 | "expected_obj": { 1877 | "protocol": "file", 1878 | "pathname": "/foo/bar" 1879 | }, 1880 | "expected_match": { 1881 | "protocol": { "input": "file", "groups": {} }, 1882 | "pathname": { "input": "/foo/bar", "groups": {} } 1883 | } 1884 | }, 1885 | { 1886 | "pattern": [ "data:" ], 1887 | "inputs": [ "data:" ], 1888 | "exactly_empty_components": [ "hostname", "port", "pathname" ], 1889 | "expected_obj": { 1890 | "protocol": "data" 1891 | }, 1892 | "expected_match": { 1893 | "protocol": { "input": "data", "groups": {} } 1894 | } 1895 | }, 1896 | { 1897 | "pattern": [ "foo://bar" ], 1898 | "inputs": [ "foo://bad_url_browser_interop" ], 1899 | "exactly_empty_components": [ "port" ], 1900 | "expected_obj": { 1901 | "protocol": "foo", 1902 | "hostname": "bar" 1903 | }, 1904 | "expected_match": null 1905 | }, 1906 | { 1907 | "pattern": [ "(café)://foo" ], 1908 | "expected_obj": "error" 1909 | }, 1910 | { 1911 | "pattern": [ "https://example.com/foo?bar#baz" ], 1912 | "inputs": [{ "protocol": "https:", 1913 | "search": "?bar", 1914 | "hash": "#baz", 1915 | "baseURL": "http://example.com/foo" }], 1916 | "exactly_empty_components": [ "port" ], 1917 | "expected_obj": { 1918 | "protocol": "https", 1919 | "hostname": "example.com", 1920 | "pathname": "/foo", 1921 | "search": "bar", 1922 | "hash": "baz" 1923 | }, 1924 | "expected_match": null 1925 | }, 1926 | { 1927 | "pattern": [{ "protocol": "http{s}?:", 1928 | "search": "?bar", 1929 | "hash": "#baz" }], 1930 | "inputs": [ "http://example.com/foo?bar#baz" ], 1931 | "expected_obj": { 1932 | "protocol": "http{s}?", 1933 | "search": "bar", 1934 | "hash": "baz" 1935 | }, 1936 | "expected_match": { 1937 | "protocol": { "input": "http", "groups": {} }, 1938 | "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, 1939 | "pathname": { "input": "/foo", "groups": { "0": "/foo" }}, 1940 | "search": { "input": "bar", "groups": {} }, 1941 | "hash": { "input": "baz", "groups": {} } 1942 | } 1943 | }, 1944 | { 1945 | "pattern": [ "?bar#baz", "https://example.com/foo" ], 1946 | "inputs": [ "?bar#baz", "https://example.com/foo" ], 1947 | "exactly_empty_components": [ "port" ], 1948 | "expected_obj": { 1949 | "protocol": "https", 1950 | "hostname": "example.com", 1951 | "pathname": "/foo", 1952 | "search": "bar", 1953 | "hash": "baz" 1954 | }, 1955 | "expected_match": { 1956 | "protocol": { "input": "https", "groups": {} }, 1957 | "hostname": { "input": "example.com", "groups": {} }, 1958 | "pathname": { "input": "/foo", "groups": {} }, 1959 | "search": { "input": "bar", "groups": {} }, 1960 | "hash": { "input": "baz", "groups": {} } 1961 | } 1962 | }, 1963 | { 1964 | "pattern": [ "?bar", "https://example.com/foo#baz" ], 1965 | "inputs": [ "?bar", "https://example.com/foo#snafu" ], 1966 | "exactly_empty_components": [ "port" ], 1967 | "expected_obj": { 1968 | "protocol": "https", 1969 | "hostname": "example.com", 1970 | "pathname": "/foo", 1971 | "search": "bar", 1972 | "hash": "*" 1973 | }, 1974 | "expected_match": { 1975 | "protocol": { "input": "https", "groups": {} }, 1976 | "hostname": { "input": "example.com", "groups": {} }, 1977 | "pathname": { "input": "/foo", "groups": {} }, 1978 | "search": { "input": "bar", "groups": {} } 1979 | } 1980 | }, 1981 | { 1982 | "pattern": [ "#baz", "https://example.com/foo?bar" ], 1983 | "inputs": [ "#baz", "https://example.com/foo?bar" ], 1984 | "exactly_empty_components": [ "port" ], 1985 | "expected_obj": { 1986 | "protocol": "https", 1987 | "hostname": "example.com", 1988 | "pathname": "/foo", 1989 | "search": "bar", 1990 | "hash": "baz" 1991 | }, 1992 | "expected_match": { 1993 | "protocol": { "input": "https", "groups": {} }, 1994 | "hostname": { "input": "example.com", "groups": {} }, 1995 | "pathname": { "input": "/foo", "groups": {} }, 1996 | "search": { "input": "bar", "groups": {} }, 1997 | "hash": { "input": "baz", "groups": {} } 1998 | } 1999 | }, 2000 | { 2001 | "pattern": [ "#baz", "https://example.com/foo" ], 2002 | "inputs": [ "#baz", "https://example.com/foo" ], 2003 | "exactly_empty_components": [ "port", "search" ], 2004 | "expected_obj": { 2005 | "protocol": "https", 2006 | "hostname": "example.com", 2007 | "pathname": "/foo", 2008 | "hash": "baz" 2009 | }, 2010 | "expected_match": { 2011 | "protocol": { "input": "https", "groups": {} }, 2012 | "hostname": { "input": "example.com", "groups": {} }, 2013 | "pathname": { "input": "/foo", "groups": {} }, 2014 | "hash": { "input": "baz", "groups": {} } 2015 | } 2016 | }, 2017 | { 2018 | "pattern": [{ "pathname": "*" }], 2019 | "inputs": [ "foo", "data:data-urls-cannot-be-base-urls" ], 2020 | "expected_match": null 2021 | }, 2022 | { 2023 | "pattern": [{ "pathname": "*" }], 2024 | "inputs": [ "foo", "not|a|valid|url" ], 2025 | "expected_match": null 2026 | }, 2027 | { 2028 | "pattern": [ "https://foo\\:bar@example.com" ], 2029 | "inputs": [ "https://foo:bar@example.com" ], 2030 | "exactly_empty_components": [ "port" ], 2031 | "expected_obj": { 2032 | "protocol": "https", 2033 | "username": "foo", 2034 | "password": "bar", 2035 | "hostname": "example.com", 2036 | "pathname": "*" 2037 | }, 2038 | "expected_match": { 2039 | "protocol": { "input": "https", "groups": {} }, 2040 | "username": { "input": "foo", "groups": {} }, 2041 | "password": { "input": "bar", "groups": {} }, 2042 | "hostname": { "input": "example.com", "groups": {} }, 2043 | "pathname": { "input": "/", "groups": { "0": "/" } } 2044 | } 2045 | }, 2046 | { 2047 | "pattern": [ "https://foo@example.com" ], 2048 | "inputs": [ "https://foo@example.com" ], 2049 | "exactly_empty_components": [ "port" ], 2050 | "expected_obj": { 2051 | "protocol": "https", 2052 | "username": "foo", 2053 | "hostname": "example.com", 2054 | "pathname": "*" 2055 | }, 2056 | "expected_match": { 2057 | "protocol": { "input": "https", "groups": {} }, 2058 | "username": { "input": "foo", "groups": {} }, 2059 | "hostname": { "input": "example.com", "groups": {} }, 2060 | "pathname": { "input": "/", "groups": { "0": "/" } } 2061 | } 2062 | }, 2063 | { 2064 | "pattern": [ "https://\\:bar@example.com" ], 2065 | "inputs": [ "https://:bar@example.com" ], 2066 | "exactly_empty_components": [ "username", "port" ], 2067 | "expected_obj": { 2068 | "protocol": "https", 2069 | "password": "bar", 2070 | "hostname": "example.com", 2071 | "pathname": "*" 2072 | }, 2073 | "expected_match": { 2074 | "protocol": { "input": "https", "groups": {} }, 2075 | "password": { "input": "bar", "groups": {} }, 2076 | "hostname": { "input": "example.com", "groups": {} }, 2077 | "pathname": { "input": "/", "groups": { "0": "/" } } 2078 | } 2079 | }, 2080 | { 2081 | "pattern": [ "https://:user::pass@example.com" ], 2082 | "inputs": [ "https://foo:bar@example.com" ], 2083 | "exactly_empty_components": [ "port" ], 2084 | "expected_obj": { 2085 | "protocol": "https", 2086 | "username": ":user", 2087 | "password": ":pass", 2088 | "hostname": "example.com", 2089 | "pathname": "*" 2090 | }, 2091 | "expected_match": { 2092 | "protocol": { "input": "https", "groups": {} }, 2093 | "username": { "input": "foo", "groups": { "user": "foo" } }, 2094 | "password": { "input": "bar", "groups": { "pass": "bar" } }, 2095 | "hostname": { "input": "example.com", "groups": {} }, 2096 | "pathname": { "input": "/", "groups": { "0": "/" } } 2097 | } 2098 | }, 2099 | { 2100 | "pattern": [ "https\\:foo\\:bar@example.com" ], 2101 | "inputs": [ "https:foo:bar@example.com" ], 2102 | "exactly_empty_components": [ "port" ], 2103 | "expected_obj": { 2104 | "protocol": "https", 2105 | "username": "foo", 2106 | "password": "bar", 2107 | "hostname": "example.com", 2108 | "pathname": "*" 2109 | }, 2110 | "expected_match": { 2111 | "protocol": { "input": "https", "groups": {} }, 2112 | "username": { "input": "foo", "groups": {} }, 2113 | "password": { "input": "bar", "groups": {} }, 2114 | "hostname": { "input": "example.com", "groups": {} }, 2115 | "pathname": { "input": "/", "groups": { "0": "/" } } 2116 | } 2117 | }, 2118 | { 2119 | "pattern": [ "data\\:foo\\:bar@example.com" ], 2120 | "inputs": [ "data:foo:bar@example.com" ], 2121 | "exactly_empty_components": [ "hostname", "port" ], 2122 | "expected_obj": { 2123 | "protocol": "data", 2124 | "pathname": "foo\\:bar@example.com" 2125 | }, 2126 | "expected_match": { 2127 | "protocol": { "input": "data", "groups": {} }, 2128 | "pathname": { "input": "foo:bar@example.com", "groups": {} } 2129 | } 2130 | }, 2131 | { 2132 | "pattern": [ "https://foo{\\:}bar@example.com" ], 2133 | "inputs": [ "https://foo:bar@example.com" ], 2134 | "exactly_empty_components": [ "port" ], 2135 | "expected_obj": { 2136 | "protocol": "https", 2137 | "username": "foo%3Abar", 2138 | "hostname": "example.com" 2139 | }, 2140 | "expected_match": null 2141 | }, 2142 | { 2143 | "pattern": [ "data{\\:}channel.html", "https://example.com" ], 2144 | "inputs": [ "https://example.com/data:channel.html" ], 2145 | "exactly_empty_components": [ "port" ], 2146 | "expected_obj": { 2147 | "protocol": "https", 2148 | "hostname": "example.com", 2149 | "pathname": "/data\\:channel.html", 2150 | "search": "*", 2151 | "hash": "*" 2152 | }, 2153 | "expected_match": { 2154 | "protocol": { "input": "https", "groups": {} }, 2155 | "hostname": { "input": "example.com", "groups": {} }, 2156 | "pathname": { "input": "/data:channel.html", "groups": {} } 2157 | } 2158 | }, 2159 | { 2160 | "pattern": [ "http://[\\:\\:1]/" ], 2161 | "inputs": [ "http://[::1]/" ], 2162 | "exactly_empty_components": [ "port" ], 2163 | "expected_obj": { 2164 | "protocol": "http", 2165 | "hostname": "[\\:\\:1]", 2166 | "pathname": "/" 2167 | }, 2168 | "expected_match": { 2169 | "protocol": { "input": "http", "groups": {} }, 2170 | "hostname": { "input": "[::1]", "groups": {} }, 2171 | "pathname": { "input": "/", "groups": {} } 2172 | } 2173 | }, 2174 | { 2175 | "pattern": [ "http://[\\:\\:1]:8080/" ], 2176 | "inputs": [ "http://[::1]:8080/" ], 2177 | "expected_obj": { 2178 | "protocol": "http", 2179 | "hostname": "[\\:\\:1]", 2180 | "port": "8080", 2181 | "pathname": "/" 2182 | }, 2183 | "expected_match": { 2184 | "protocol": { "input": "http", "groups": {} }, 2185 | "hostname": { "input": "[::1]", "groups": {} }, 2186 | "port": { "input": "8080", "groups": {} }, 2187 | "pathname": { "input": "/", "groups": {} } 2188 | } 2189 | }, 2190 | { 2191 | "pattern": [ "http://[\\:\\:a]/" ], 2192 | "inputs": [ "http://[::a]/" ], 2193 | "exactly_empty_components": [ "port" ], 2194 | "expected_obj": { 2195 | "protocol": "http", 2196 | "hostname": "[\\:\\:a]", 2197 | "pathname": "/" 2198 | }, 2199 | "expected_match": { 2200 | "protocol": { "input": "http", "groups": {} }, 2201 | "hostname": { "input": "[::a]", "groups": {} }, 2202 | "pathname": { "input": "/", "groups": {} } 2203 | } 2204 | }, 2205 | { 2206 | "pattern": [ "http://[:address]/" ], 2207 | "inputs": [ "http://[::1]/" ], 2208 | "exactly_empty_components": [ "port" ], 2209 | "expected_obj": { 2210 | "protocol": "http", 2211 | "hostname": "[:address]", 2212 | "pathname": "/" 2213 | }, 2214 | "expected_match": { 2215 | "protocol": { "input": "http", "groups": {} }, 2216 | "hostname": { "input": "[::1]", "groups": { "address": "::1" }}, 2217 | "pathname": { "input": "/", "groups": {} } 2218 | } 2219 | }, 2220 | { 2221 | "pattern": [ "http://[\\:\\:AB\\::num]/" ], 2222 | "inputs": [ "http://[::ab:1]/" ], 2223 | "exactly_empty_components": [ "port" ], 2224 | "expected_obj": { 2225 | "protocol": "http", 2226 | "hostname": "[\\:\\:ab\\::num]", 2227 | "pathname": "/" 2228 | }, 2229 | "expected_match": { 2230 | "protocol": { "input": "http", "groups": {} }, 2231 | "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}, 2232 | "pathname": { "input": "/", "groups": {} } 2233 | } 2234 | }, 2235 | { 2236 | "pattern": [{ "hostname": "[\\:\\:AB\\::num]" }], 2237 | "inputs": [{ "hostname": "[::ab:1]" }], 2238 | "expected_obj": { 2239 | "hostname": "[\\:\\:ab\\::num]" 2240 | }, 2241 | "expected_match": { 2242 | "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} 2243 | } 2244 | }, 2245 | { 2246 | "pattern": [{ "hostname": "[\\:\\:xY\\::num]" }], 2247 | "expected_obj": "error" 2248 | }, 2249 | { 2250 | "pattern": [{ "hostname": "{[\\:\\:ab\\::num]}" }], 2251 | "inputs": [{ "hostname": "[::ab:1]" }], 2252 | "expected_match": { 2253 | "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} 2254 | } 2255 | }, 2256 | { 2257 | "pattern": [{ "hostname": "{[\\:\\:fé\\::num]}" }], 2258 | "expected_obj": "error" 2259 | }, 2260 | { 2261 | "pattern": [{ "hostname": "{[\\:\\::num\\:1]}" }], 2262 | "inputs": [{ "hostname": "[::ab:1]" }], 2263 | "expected_match": { 2264 | "hostname": { "input": "[::ab:1]", "groups": { "num": "ab" }} 2265 | } 2266 | }, 2267 | { 2268 | "pattern": [{ "hostname": "{[\\:\\::num\\:fé]}" }], 2269 | "expected_obj": "error" 2270 | }, 2271 | { 2272 | "pattern": [{ "hostname": "[*\\:1]" }], 2273 | "inputs": [{ "hostname": "[::ab:1]" }], 2274 | "expected_match": { 2275 | "hostname": { "input": "[::ab:1]", "groups": { "0": "::ab" }} 2276 | } 2277 | }, 2278 | { 2279 | "pattern": [{ "hostname": "*\\:1]" }], 2280 | "expected_obj": "error" 2281 | }, 2282 | { 2283 | "pattern": [ "https://foo{{@}}example.com" ], 2284 | "inputs": [ "https://foo@example.com" ], 2285 | "expected_obj": "error" 2286 | }, 2287 | { 2288 | "pattern": [ "https://foo{@example.com" ], 2289 | "inputs": [ "https://foo@example.com" ], 2290 | "expected_obj": "error" 2291 | }, 2292 | { 2293 | "pattern": [ "data\\:text/javascript,let x = 100/:tens?5;" ], 2294 | "inputs": [ "data:text/javascript,let x = 100/5;" ], 2295 | "exactly_empty_components": [ "hostname", "port" ], 2296 | "expected_obj": { 2297 | "protocol": "data", 2298 | "pathname": "text/javascript,let x = 100/:tens?5;" 2299 | }, 2300 | "//": "The `null` below is translated to undefined in the test harness.", 2301 | "expected_match": { 2302 | "protocol": { "input": "data", "groups": {} }, 2303 | "pathname": { "input": "text/javascript,let x = 100/5;", "groups": { "tens": null } } 2304 | } 2305 | }, 2306 | { 2307 | "pattern": [{ "pathname": "/:id/:id" }], 2308 | "expected_obj": "error" 2309 | }, 2310 | { 2311 | "pattern": [{ "pathname": "/foo", "baseURL": "" }], 2312 | "expected_obj": "error" 2313 | }, 2314 | { 2315 | "pattern": [ "/foo", "" ], 2316 | "expected_obj": "error" 2317 | }, 2318 | { 2319 | "pattern": [{ "pathname": "/foo" }, "https://example.com" ], 2320 | "expected_obj": "error" 2321 | }, 2322 | { 2323 | "pattern": [{ "pathname": ":name*" }], 2324 | "inputs": [{ "pathname": "foobar" }], 2325 | "expected_match": { 2326 | "pathname": { "input": "foobar", "groups": { "name": "foobar" }} 2327 | } 2328 | }, 2329 | { 2330 | "pattern": [{ "pathname": ":name+" }], 2331 | "inputs": [{ "pathname": "foobar" }], 2332 | "expected_match": { 2333 | "pathname": { "input": "foobar", "groups": { "name": "foobar" }} 2334 | } 2335 | }, 2336 | { 2337 | "pattern": [{ "pathname": ":name" }], 2338 | "inputs": [{ "pathname": "foobar" }], 2339 | "expected_match": { 2340 | "pathname": { "input": "foobar", "groups": { "name": "foobar" }} 2341 | } 2342 | }, 2343 | { 2344 | "pattern": [{ "protocol": ":name*" }], 2345 | "inputs": [{ "protocol": "foobar" }], 2346 | "expected_match": { 2347 | "protocol": { "input": "foobar", "groups": { "name": "foobar" }} 2348 | } 2349 | }, 2350 | { 2351 | "pattern": [{ "protocol": ":name+" }], 2352 | "inputs": [{ "protocol": "foobar" }], 2353 | "expected_match": { 2354 | "protocol": { "input": "foobar", "groups": { "name": "foobar" }} 2355 | } 2356 | }, 2357 | { 2358 | "pattern": [{ "protocol": ":name" }], 2359 | "inputs": [{ "protocol": "foobar" }], 2360 | "expected_match": { 2361 | "protocol": { "input": "foobar", "groups": { "name": "foobar" }} 2362 | } 2363 | }, 2364 | { 2365 | "pattern": [{ "hostname": "bad hostname" }], 2366 | "expected_obj": "error" 2367 | }, 2368 | { 2369 | "pattern": [{ "hostname": "bad#hostname" }], 2370 | "expected_obj": "error" 2371 | }, 2372 | { 2373 | "pattern": [{ "hostname": "bad%hostname" }], 2374 | "expected_obj": "error" 2375 | }, 2376 | { 2377 | "pattern": [{ "hostname": "bad/hostname" }], 2378 | "expected_obj": "error" 2379 | }, 2380 | { 2381 | "pattern": [{ "hostname": "bad\\:hostname" }], 2382 | "expected_obj": "error" 2383 | }, 2384 | { 2385 | "pattern": [{ "hostname": "badhostname" }], 2390 | "expected_obj": "error" 2391 | }, 2392 | { 2393 | "pattern": [{ "hostname": "bad?hostname" }], 2394 | "expected_obj": "error" 2395 | }, 2396 | { 2397 | "pattern": [{ "hostname": "bad@hostname" }], 2398 | "expected_obj": "error" 2399 | }, 2400 | { 2401 | "pattern": [{ "hostname": "bad[hostname" }], 2402 | "expected_obj": "error" 2403 | }, 2404 | { 2405 | "pattern": [{ "hostname": "bad]hostname" }], 2406 | "expected_obj": "error" 2407 | }, 2408 | { 2409 | "pattern": [{ "hostname": "bad\\\\hostname" }], 2410 | "expected_obj": "error" 2411 | }, 2412 | { 2413 | "pattern": [{ "hostname": "bad^hostname" }], 2414 | "expected_obj": "error" 2415 | }, 2416 | { 2417 | "pattern": [{ "hostname": "bad|hostname" }], 2418 | "expected_obj": "error" 2419 | }, 2420 | { 2421 | "pattern": [{ "hostname": "bad\nhostname" }], 2422 | "expected_obj": "error" 2423 | }, 2424 | { 2425 | "pattern": [{ "hostname": "bad\rhostname" }], 2426 | "expected_obj": "error" 2427 | }, 2428 | { 2429 | "pattern": [{ "hostname": "bad\thostname" }], 2430 | "expected_obj": "error" 2431 | }, 2432 | { 2433 | "pattern": [{}], 2434 | "inputs": ["https://example.com/"], 2435 | "expected_match": { 2436 | "protocol": { "input": "https", "groups": { "0": "https" }}, 2437 | "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, 2438 | "pathname": { "input": "/", "groups": { "0": "/" }} 2439 | } 2440 | }, 2441 | { 2442 | "pattern": [], 2443 | "inputs": ["https://example.com/"], 2444 | "expected_match": { 2445 | "protocol": { "input": "https", "groups": { "0": "https" }}, 2446 | "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, 2447 | "pathname": { "input": "/", "groups": { "0": "/" }} 2448 | } 2449 | }, 2450 | { 2451 | "pattern": [], 2452 | "inputs": [{}], 2453 | "expected_match": {} 2454 | }, 2455 | { 2456 | "pattern": [], 2457 | "inputs": [], 2458 | "expected_match": { "inputs": [{}] } 2459 | }, 2460 | { 2461 | "pattern": [{ "pathname": "(foo)(.*)" }], 2462 | "inputs": [{ "pathname": "foobarbaz" }], 2463 | "expected_match": { 2464 | "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} 2465 | } 2466 | }, 2467 | { 2468 | "pattern": [{ "pathname": "{(foo)bar}(.*)" }], 2469 | "inputs": [{ "pathname": "foobarbaz" }], 2470 | "expected_match": { 2471 | "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "baz" }} 2472 | } 2473 | }, 2474 | { 2475 | "pattern": [{ "pathname": "(foo)?(.*)" }], 2476 | "inputs": [{ "pathname": "foobarbaz" }], 2477 | "expected_obj": { 2478 | "pathname": "(foo)?*" 2479 | }, 2480 | "expected_match": { 2481 | "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} 2482 | } 2483 | }, 2484 | { 2485 | "pattern": [{ "pathname": "{:foo}(.*)" }], 2486 | "inputs": [{ "pathname": "foobarbaz" }], 2487 | "expected_match": { 2488 | "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} 2489 | } 2490 | }, 2491 | { 2492 | "pattern": [{ "pathname": "{:foo}(barbaz)" }], 2493 | "inputs": [{ "pathname": "foobarbaz" }], 2494 | "expected_match": { 2495 | "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "barbaz" }} 2496 | } 2497 | }, 2498 | { 2499 | "pattern": [{ "pathname": "{:foo}{(.*)}" }], 2500 | "inputs": [{ "pathname": "foobarbaz" }], 2501 | "expected_obj": { 2502 | "pathname": "{:foo}(.*)" 2503 | }, 2504 | "expected_match": { 2505 | "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} 2506 | } 2507 | }, 2508 | { 2509 | "pattern": [{ "pathname": "{:foo}{(.*)bar}" }], 2510 | "inputs": [{ "pathname": "foobarbaz" }], 2511 | "expected_obj": { 2512 | "pathname": ":foo{*bar}" 2513 | }, 2514 | "expected_match": null 2515 | }, 2516 | { 2517 | "pattern": [{ "pathname": "{:foo}{bar(.*)}" }], 2518 | "inputs": [{ "pathname": "foobarbaz" }], 2519 | "expected_obj": { 2520 | "pathname": ":foo{bar*}" 2521 | }, 2522 | "expected_match": { 2523 | "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "baz" }} 2524 | } 2525 | }, 2526 | { 2527 | "pattern": [{ "pathname": "{:foo}:bar(.*)" }], 2528 | "inputs": [{ "pathname": "foobarbaz" }], 2529 | "expected_obj": { 2530 | "pathname": ":foo:bar(.*)" 2531 | }, 2532 | "expected_match": { 2533 | "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "bar": "oobarbaz" }} 2534 | } 2535 | }, 2536 | { 2537 | "pattern": [{ "pathname": "{:foo}?(.*)" }], 2538 | "inputs": [{ "pathname": "foobarbaz" }], 2539 | "expected_obj": { 2540 | "pathname": ":foo?*" 2541 | }, 2542 | "expected_match": { 2543 | "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} 2544 | } 2545 | }, 2546 | { 2547 | "pattern": [{ "pathname": "{:foo\\bar}" }], 2548 | "inputs": [{ "pathname": "foobar" }], 2549 | "expected_match": { 2550 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2551 | } 2552 | }, 2553 | { 2554 | "pattern": [{ "pathname": "{:foo\\.bar}" }], 2555 | "inputs": [{ "pathname": "foo.bar" }], 2556 | "expected_obj": { 2557 | "pathname": "{:foo.bar}" 2558 | }, 2559 | "expected_match": { 2560 | "pathname": { "input": "foo.bar", "groups": { "foo": "foo" }} 2561 | } 2562 | }, 2563 | { 2564 | "pattern": [{ "pathname": "{:foo(foo)bar}" }], 2565 | "inputs": [{ "pathname": "foobar" }], 2566 | "expected_match": { 2567 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2568 | } 2569 | }, 2570 | { 2571 | "pattern": [{ "pathname": "{:foo}bar" }], 2572 | "inputs": [{ "pathname": "foobar" }], 2573 | "expected_match": { 2574 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2575 | } 2576 | }, 2577 | { 2578 | "pattern": [{ "pathname": ":foo\\bar" }], 2579 | "inputs": [{ "pathname": "foobar" }], 2580 | "expected_obj": { 2581 | "pathname": "{:foo}bar" 2582 | }, 2583 | "expected_match": { 2584 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2585 | } 2586 | }, 2587 | { 2588 | "pattern": [{ "pathname": ":foo{}(.*)" }], 2589 | "inputs": [{ "pathname": "foobar" }], 2590 | "expected_obj": { 2591 | "pathname": "{:foo}(.*)" 2592 | }, 2593 | "expected_match": { 2594 | "pathname": { "input": "foobar", "groups": { "foo": "f", "0": "oobar" }} 2595 | } 2596 | }, 2597 | { 2598 | "pattern": [{ "pathname": ":foo{}bar" }], 2599 | "inputs": [{ "pathname": "foobar" }], 2600 | "expected_obj": { 2601 | "pathname": "{:foo}bar" 2602 | }, 2603 | "expected_match": { 2604 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2605 | } 2606 | }, 2607 | { 2608 | "pattern": [{ "pathname": ":foo{}?bar" }], 2609 | "inputs": [{ "pathname": "foobar" }], 2610 | "expected_obj": { 2611 | "pathname": "{:foo}bar" 2612 | }, 2613 | "expected_match": { 2614 | "pathname": { "input": "foobar", "groups": { "foo": "foo" }} 2615 | } 2616 | }, 2617 | { 2618 | "pattern": [{ "pathname": "*{}**?" }], 2619 | "inputs": [{ "pathname": "foobar" }], 2620 | "expected_obj": { 2621 | "pathname": "*(.*)?" 2622 | }, 2623 | "//": "The `null` below is translated to undefined in the test harness.", 2624 | "expected_match": { 2625 | "pathname": { "input": "foobar", "groups": { "0": "foobar", "1": null }} 2626 | } 2627 | }, 2628 | { 2629 | "pattern": [{ "pathname": ":foo(baz)(.*)" }], 2630 | "inputs": [{ "pathname": "bazbar" }], 2631 | "expected_match": { 2632 | "pathname": { "input": "bazbar", "groups": { "foo": "baz", "0": "bar" }} 2633 | } 2634 | }, 2635 | { 2636 | "pattern": [{ "pathname": ":foo(baz)bar" }], 2637 | "inputs": [{ "pathname": "bazbar" }], 2638 | "expected_match": { 2639 | "pathname": { "input": "bazbar", "groups": { "foo": "baz" }} 2640 | } 2641 | }, 2642 | { 2643 | "pattern": [{ "pathname": "*/*" }], 2644 | "inputs": [{ "pathname": "foo/bar" }], 2645 | "expected_match": { 2646 | "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} 2647 | } 2648 | }, 2649 | { 2650 | "pattern": [{ "pathname": "*\\/*" }], 2651 | "inputs": [{ "pathname": "foo/bar" }], 2652 | "expected_obj": { 2653 | "pathname": "*/{*}" 2654 | }, 2655 | "expected_match": { 2656 | "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} 2657 | } 2658 | }, 2659 | { 2660 | "pattern": [{ "pathname": "*/{*}" }], 2661 | "inputs": [{ "pathname": "foo/bar" }], 2662 | "expected_match": { 2663 | "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} 2664 | } 2665 | }, 2666 | { 2667 | "pattern": [{ "pathname": "*//*" }], 2668 | "inputs": [{ "pathname": "foo/bar" }], 2669 | "expected_match": null 2670 | }, 2671 | { 2672 | "pattern": [{ "pathname": "/:foo." }], 2673 | "inputs": [{ "pathname": "/bar." }], 2674 | "expected_match": { 2675 | "pathname": { "input": "/bar.", "groups": { "foo": "bar" } } 2676 | } 2677 | }, 2678 | { 2679 | "pattern": [{ "pathname": "/:foo.." }], 2680 | "inputs": [{ "pathname": "/bar.." }], 2681 | "expected_match": { 2682 | "pathname": { "input": "/bar..", "groups": { "foo": "bar" } } 2683 | } 2684 | }, 2685 | { 2686 | "pattern": [{ "pathname": "./foo" }], 2687 | "inputs": [{ "pathname": "./foo" }], 2688 | "expected_match": { 2689 | "pathname": { "input": "./foo", "groups": {}} 2690 | } 2691 | }, 2692 | { 2693 | "pattern": [{ "pathname": "../foo" }], 2694 | "inputs": [{ "pathname": "../foo" }], 2695 | "expected_match": { 2696 | "pathname": { "input": "../foo", "groups": {}} 2697 | } 2698 | }, 2699 | { 2700 | "pattern": [{ "pathname": ":foo./" }], 2701 | "inputs": [{ "pathname": "bar./" }], 2702 | "expected_match": { 2703 | "pathname": { "input": "bar./", "groups": { "foo": "bar" }} 2704 | } 2705 | }, 2706 | { 2707 | "pattern": [{ "pathname": ":foo../" }], 2708 | "inputs": [{ "pathname": "bar../" }], 2709 | "expected_match": { 2710 | "pathname": { "input": "bar../", "groups": { "foo": "bar" }} 2711 | } 2712 | }, 2713 | { 2714 | "pattern": [{ "pathname": "/:foo\\bar" }], 2715 | "inputs": [{ "pathname": "/bazbar" }], 2716 | "expected_obj": { 2717 | "pathname": "{/:foo}bar" 2718 | }, 2719 | "expected_match": { 2720 | "pathname": { "input": "/bazbar", "groups": { "foo": "baz" }} 2721 | } 2722 | }, 2723 | { 2724 | "pattern": [{ "pathname": "/foo/bar" }, { "ignoreCase": true }], 2725 | "inputs": [{ "pathname": "/FOO/BAR" }], 2726 | "expected_match": { 2727 | "pathname": { "input": "/FOO/BAR", "groups": {} } 2728 | } 2729 | }, 2730 | { 2731 | "pattern": [{ "ignoreCase": true }], 2732 | "inputs": [{ "pathname": "/FOO/BAR" }], 2733 | "expected_match": { 2734 | "pathname": { "input": "/FOO/BAR", "groups": { "0": "/FOO/BAR" } } 2735 | } 2736 | }, 2737 | { 2738 | "pattern": [ "https://example.com:8080/foo?bar#baz", 2739 | { "ignoreCase": true }], 2740 | "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", 2741 | "baseURL": "https://example.com:8080" }], 2742 | "expected_obj": { 2743 | "protocol": "https", 2744 | "hostname": "example.com", 2745 | "port": "8080", 2746 | "pathname": "/foo", 2747 | "search": "bar", 2748 | "hash": "baz" 2749 | }, 2750 | "expected_match": { 2751 | "protocol": { "input": "https", "groups": {} }, 2752 | "hostname": { "input": "example.com", "groups": {} }, 2753 | "port": { "input": "8080", "groups": {} }, 2754 | "pathname": { "input": "/FOO", "groups": {} }, 2755 | "search": { "input": "BAR", "groups": {} }, 2756 | "hash": { "input": "BAZ", "groups": {} } 2757 | } 2758 | }, 2759 | { 2760 | "pattern": [ "/foo?bar#baz", "https://example.com:8080", 2761 | { "ignoreCase": true }], 2762 | "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", 2763 | "baseURL": "https://example.com:8080" }], 2764 | "expected_obj": { 2765 | "protocol": "https", 2766 | "hostname": "example.com", 2767 | "port": "8080", 2768 | "pathname": "/foo", 2769 | "search": "bar", 2770 | "hash": "baz" 2771 | }, 2772 | "expected_match": { 2773 | "protocol": { "input": "https", "groups": {} }, 2774 | "hostname": { "input": "example.com", "groups": {} }, 2775 | "port": { "input": "8080", "groups": {} }, 2776 | "pathname": { "input": "/FOO", "groups": {} }, 2777 | "search": { "input": "BAR", "groups": {} }, 2778 | "hash": { "input": "BAZ", "groups": {} } 2779 | } 2780 | }, 2781 | { 2782 | "pattern": [ "/foo?bar#baz", { "ignoreCase": true }, 2783 | "https://example.com:8080" ], 2784 | "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", 2785 | "baseURL": "https://example.com:8080" }], 2786 | "expected_obj": "error" 2787 | }, 2788 | { 2789 | "pattern": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], 2790 | "inputs": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], 2791 | "exactly_empty_components": [ "port" ], 2792 | "expected_obj": { 2793 | "pathname": "/a/\\+/b" 2794 | }, 2795 | "expected_match": { 2796 | "hostname": { "input": "example.com", "groups": {} }, 2797 | "pathname": { "input": "/a/+/b", "groups": {} }, 2798 | "protocol": { "input": "https", "groups": {} }, 2799 | "search": { "input": "foo", "groups": {} } 2800 | } 2801 | }, 2802 | { 2803 | "pattern": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], 2804 | "inputs": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], 2805 | "exactly_empty_components": [ "port" ], 2806 | "expected_obj": { 2807 | "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)" 2808 | }, 2809 | "expected_match": { 2810 | "hostname": { "input": "example.com", "groups": {} }, 2811 | "pathname": { "input": "/", "groups": {} }, 2812 | "protocol": { "input": "https", "groups": {} }, 2813 | "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, 2814 | "hash": { "input": "foo", "groups": {} } 2815 | } 2816 | }, 2817 | { 2818 | "pattern": [ "#foo", "https://example.com/?q=*&v=?&hmm={}&umm=()" ], 2819 | "inputs": [ "https://example.com/?q=*&v=?&hmm={}&umm=()#foo" ], 2820 | "exactly_empty_components": [ "port" ], 2821 | "expected_obj": { 2822 | "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)", 2823 | "hash": "foo" 2824 | }, 2825 | "expected_match": { 2826 | "hostname": { "input": "example.com", "groups": {} }, 2827 | "pathname": { "input": "/", "groups": {} }, 2828 | "protocol": { "input": "https", "groups": {} }, 2829 | "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, 2830 | "hash": { "input": "foo", "groups": {} } 2831 | } 2832 | }, 2833 | { 2834 | "pattern": [{ "pathname": "/([[a-z]--a])" }], 2835 | "inputs": [{ "pathname": "/a" }], 2836 | "expected_match": null 2837 | }, 2838 | { 2839 | "pattern": [{ "pathname": "/([[a-z]--a])" }], 2840 | "inputs": [{ "pathname": "/z" }], 2841 | "expected_match": { 2842 | "pathname": { "input": "/z", "groups": { "0": "z" } } 2843 | } 2844 | }, 2845 | { 2846 | "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], 2847 | "inputs": [{ "pathname": "/0" }], 2848 | "expected_match": { 2849 | "pathname": { "input": "/0", "groups": { "0": "0" } } 2850 | } 2851 | }, 2852 | { 2853 | "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], 2854 | "inputs": [{ "pathname": "/3" }], 2855 | "expected_match": null 2856 | } 2857 | ] 2858 | -------------------------------------------------------------------------------- /tokenizer.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "unicode" 7 | 8 | "golang.org/x/exp/utf8string" 9 | ) 10 | 11 | var TypeError = errors.New("type error") 12 | 13 | // https://wicg.github.io/urlpattern/#tokenizing 14 | type tokenizePolicy bool 15 | 16 | const ( 17 | tokenizePolicyLenient tokenizePolicy = false 18 | tokenizePolicyStrict tokenizePolicy = true 19 | ) 20 | 21 | type tokenizer struct { 22 | input *utf8string.String 23 | policy tokenizePolicy 24 | tokenList []token 25 | index int 26 | nextIndex int 27 | codePoint rune 28 | } 29 | 30 | func tokenize(input string, policy tokenizePolicy) ([]token, error) { 31 | t := tokenizer{ 32 | input: utf8string.NewString(input), 33 | policy: policy, 34 | tokenList: make([]token, 0, len(input)), 35 | } 36 | 37 | len := t.input.RuneCount() 38 | 39 | for t.index < len { 40 | t.seekAndGetNextCodePoint(t.index) 41 | 42 | switch t.codePoint { 43 | case '*': 44 | t.addTokenWithDefaultPositionAndLength(tokenAsterisk) 45 | 46 | case '+': 47 | t.addTokenWithDefaultPositionAndLength(tokenOtherModifier) 48 | 49 | case '?': 50 | t.addTokenWithDefaultPositionAndLength(tokenOtherModifier) 51 | 52 | case '\\': 53 | if t.index == len-1 { 54 | if err := t.processTokenizingError(t.nextIndex, t.index); err == nil { 55 | return nil, err 56 | } 57 | 58 | continue 59 | } 60 | 61 | escapedIndex := t.nextIndex 62 | t.getNextCodePoint() 63 | t.addTokenWithDefaultLength(tokenEscapedChar, t.nextIndex, escapedIndex) 64 | 65 | case '{': 66 | t.addTokenWithDefaultPositionAndLength(tokenOpen) 67 | 68 | case '}': 69 | t.addTokenWithDefaultPositionAndLength(tokenClose) 70 | 71 | case ':': 72 | namePosition := t.nextIndex 73 | nameStart := namePosition 74 | 75 | for namePosition < len { 76 | t.seekAndGetNextCodePoint(namePosition) 77 | 78 | var firstCodePoint bool 79 | if namePosition == nameStart { 80 | firstCodePoint = true 81 | } 82 | 83 | if !isValidNameCodePoint(t.codePoint, firstCodePoint) { 84 | break 85 | } 86 | 87 | namePosition = t.nextIndex 88 | } 89 | 90 | if namePosition <= nameStart { 91 | if err := t.processTokenizingError(nameStart, t.index); err != nil { 92 | return nil, err 93 | } 94 | 95 | continue 96 | } 97 | 98 | t.addTokenWithDefaultLength(tokenName, namePosition, nameStart) 99 | 100 | case '(': 101 | depth := 1 102 | regexpPosition := t.nextIndex 103 | regexpStart := regexpPosition 104 | err := false 105 | 106 | Loop: 107 | for regexpPosition < len { 108 | t.seekAndGetNextCodePoint(regexpPosition) 109 | if !isASCII(t.codePoint) || 110 | (regexpPosition == regexpStart && t.codePoint == '?') { 111 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 112 | return nil, e 113 | } 114 | 115 | err = true 116 | break 117 | } 118 | 119 | switch t.codePoint { 120 | case '\\': 121 | if regexpPosition == len-1 { 122 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 123 | return nil, e 124 | } 125 | 126 | err = true 127 | break Loop 128 | } 129 | 130 | t.getNextCodePoint() 131 | 132 | if !isASCII(t.codePoint) { 133 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 134 | return nil, e 135 | } 136 | 137 | err = true 138 | break Loop 139 | } 140 | 141 | regexpPosition = t.nextIndex 142 | 143 | continue 144 | 145 | case ')': 146 | depth-- 147 | if depth == 0 { 148 | regexpPosition = t.nextIndex 149 | break Loop 150 | } 151 | 152 | case '(': 153 | depth++ 154 | 155 | if regexpPosition == len-1 { 156 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 157 | return nil, e 158 | } 159 | 160 | err = true 161 | break Loop 162 | } 163 | 164 | temporaryPosition := t.nextIndex 165 | t.getNextCodePoint() 166 | 167 | if t.codePoint != '?' { 168 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 169 | return nil, e 170 | } 171 | 172 | err = true 173 | break Loop 174 | } 175 | 176 | t.nextIndex = temporaryPosition 177 | } 178 | 179 | regexpPosition = t.nextIndex 180 | } 181 | 182 | if err { 183 | continue 184 | } 185 | 186 | if depth != 0 { 187 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 188 | return nil, e 189 | } 190 | 191 | continue 192 | } 193 | 194 | regexpLength := regexpPosition - regexpStart - 1 195 | if regexpLength == 0 { 196 | if e := t.processTokenizingError(regexpStart, t.index); e != nil { 197 | return nil, e 198 | } 199 | 200 | continue 201 | } 202 | 203 | t.addToken(tokenRegexp, regexpPosition, regexpStart, regexpLength) 204 | 205 | default: 206 | t.addTokenWithDefaultPositionAndLength(tokenChar) 207 | } 208 | } 209 | 210 | t.addTokenWithDefaultLength(tokenEnd, t.index, t.index) 211 | 212 | return t.tokenList, nil 213 | } 214 | 215 | func (t *tokenizer) getNextCodePoint() { 216 | t.codePoint = t.input.At(t.nextIndex) 217 | t.nextIndex++ 218 | } 219 | 220 | func (t *tokenizer) seekAndGetNextCodePoint(index int) { 221 | t.nextIndex = index 222 | t.getNextCodePoint() 223 | } 224 | 225 | func (t *tokenizer) addToken(tType tokenType, nextPosition, valuePosition, valueLength int) { 226 | t.tokenList = append(t.tokenList, token{ 227 | tType: tType, 228 | index: t.index, 229 | value: t.input.Slice(valuePosition, valuePosition+valueLength), 230 | }) 231 | t.index = nextPosition 232 | } 233 | 234 | func (t *tokenizer) addTokenWithDefaultLength(tType tokenType, nextPosition, valuePosition int) { 235 | t.addToken(tType, nextPosition, valuePosition, nextPosition-valuePosition) 236 | } 237 | 238 | func (t *tokenizer) addTokenWithDefaultPositionAndLength(tType tokenType) { 239 | t.addTokenWithDefaultLength(tType, t.nextIndex, t.index) 240 | } 241 | 242 | func (t *tokenizer) processTokenizingError(nextPosition, valuePosition int) error { 243 | if t.policy == tokenizePolicyStrict { 244 | return fmt.Errorf("%w: %#v", TypeError, t) 245 | } 246 | 247 | t.addTokenWithDefaultLength(tokenInvalidChar, nextPosition, valuePosition) 248 | 249 | return nil 250 | } 251 | 252 | func isValidNameCodePoint(codePoint rune, first bool) bool { 253 | if first { 254 | return isIdentifierStart(codePoint) 255 | } 256 | 257 | return isIdentifierPart(codePoint) 258 | } 259 | 260 | func isIdentifierStart(codePoint rune) bool { 261 | return unicode.In( 262 | codePoint, 263 | unicode.L, 264 | unicode.Nl, 265 | unicode.Other_ID_Start, 266 | ) && !unicode.In( 267 | codePoint, 268 | unicode.Pattern_Syntax, 269 | unicode.Pattern_White_Space, 270 | ) 271 | } 272 | 273 | func isIdentifierPart(codePoint rune) bool { 274 | return unicode.In( 275 | codePoint, 276 | unicode.L, 277 | unicode.Nl, 278 | unicode.Other_ID_Start, 279 | unicode.Mn, 280 | unicode.Mc, 281 | unicode.Nd, 282 | unicode.Pc, 283 | unicode.Other_ID_Continue, 284 | ) && !unicode.In( 285 | codePoint, 286 | unicode.Pattern_Syntax, 287 | unicode.Pattern_White_Space, 288 | ) 289 | } 290 | 291 | func isASCII(codePoint rune) bool { 292 | return codePoint >= 0 && codePoint <= unicode.MaxASCII 293 | } 294 | -------------------------------------------------------------------------------- /tokens.go: -------------------------------------------------------------------------------- 1 | package urlpattern 2 | 3 | // https://wicg.github.io/urlpattern/#tokens 4 | type token struct { 5 | tType tokenType 6 | index int 7 | value string 8 | } 9 | 10 | type tokenType uint8 11 | 12 | const ( 13 | // tokenOpen represents a U+007B ({) code point. 14 | tokenOpen tokenType = iota 15 | // tokenClose represents a U+007D (}) code point. 16 | tokenClose 17 | // tokenRegexp represents a string of the form "()". The regular expression is required to consist of only ASCII code points. 18 | tokenRegexp 19 | // tokenName a string of the form ":". The name value is restricted to code points that are consistent with JavaScript identifiers. 20 | tokenName 21 | // tokenChar represents a valid pattern code point without any special syntactical meaning. 22 | tokenChar 23 | // tokenEscapedChar represents a code point escaped using a backslash like "\". 24 | tokenEscapedChar 25 | // tokenOtherModifier represents a matching group modifier that is either the U+003F (?) or U+002B (+) code points. 26 | tokenOtherModifier 27 | // tokenAsterisk represents a U+002A (*) code point that can be either a wildcard matching group or a matching group modifier. 28 | tokenAsterisk 29 | // tokenEnd represents the end of the pattern string. 30 | tokenEnd 31 | // tokenInvalidChar represents a code point that is invalid in the pattern. This could be because of the code point value itself or due to its location within the pattern relative to other syntactic elements. 32 | tokenInvalidChar 33 | ) 34 | -------------------------------------------------------------------------------- /urlpattern.go: -------------------------------------------------------------------------------- 1 | // Package urlpattern implements the URLPattern web API. 2 | // 3 | // The specification is available at https://urlpattern.spec.whatwg.org/. 4 | package urlpattern 5 | 6 | import ( 7 | "errors" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/nlnwa/whatwg-url/url" 12 | ) 13 | 14 | var ( 15 | NoBaseURLError = errors.New("relative URL and no baseURL provided") 16 | UnexpectedEmptyStringError = errors.New("unexpected empty string") 17 | ) 18 | 19 | // https://url.spec.whatwg.org/#special-scheme 20 | var specialSchemeList = []string{"ftp", "http", "https", "ws", "wss"} 21 | 22 | type URLPatternResult struct { 23 | Inputs []string 24 | InitInputs []*URLPatternInit 25 | 26 | Protocol URLPatternComponentResult 27 | Username URLPatternComponentResult 28 | Password URLPatternComponentResult 29 | Hostname URLPatternComponentResult 30 | Port URLPatternComponentResult 31 | Pathname URLPatternComponentResult 32 | Search URLPatternComponentResult 33 | Hash URLPatternComponentResult 34 | } 35 | 36 | type URLPatternComponentResult struct { 37 | Input string 38 | Groups map[string]string 39 | } 40 | 41 | // https://urlpattern.spec.whatwg.org/#url-pattern-struct 42 | type URLPattern struct { 43 | protocol *component 44 | username *component 45 | password *component 46 | hostname *component 47 | port *component 48 | pathname *component 49 | search *component 50 | hash *component 51 | } 52 | 53 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-protocol 54 | func (u *URLPattern) Protocol() string { 55 | return u.protocol.patternString 56 | } 57 | 58 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-username 59 | func (u *URLPattern) Username() string { 60 | return u.username.patternString 61 | } 62 | 63 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-password 64 | func (u *URLPattern) Password() string { 65 | return u.password.patternString 66 | } 67 | 68 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-hostname 69 | func (u *URLPattern) Hostname() string { 70 | return u.hostname.patternString 71 | } 72 | 73 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-port 74 | func (u *URLPattern) Port() string { 75 | return u.port.patternString 76 | } 77 | 78 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-pathname 79 | func (u *URLPattern) Pathname() string { 80 | return u.pathname.patternString 81 | } 82 | 83 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-search 84 | func (u *URLPattern) Search() string { 85 | return u.search.patternString 86 | } 87 | 88 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-hash 89 | func (u *URLPattern) Hash() string { 90 | return u.hash.patternString 91 | } 92 | 93 | // https://urlpattern.spec.whatwg.org/#component 94 | type component struct { 95 | patternString string 96 | regularExpression *regexp.Regexp 97 | groupNameList []string 98 | hasRegexpGroups bool 99 | } 100 | 101 | // https://urlpattern.spec.whatwg.org/#protocol-component-matches-a-special-scheme 102 | func (c *component) protocolComponentMatchesSpecialScheme() bool { 103 | for _, scheme := range specialSchemeList { 104 | if c.regularExpression.MatchString(scheme) { 105 | return true 106 | } 107 | } 108 | 109 | return false 110 | } 111 | 112 | // https://urlpattern.spec.whatwg.org/#url-pattern-create 113 | func New(input string, baseURL string, options *Options) (*URLPattern, error) { 114 | init, err := parseConstructorString(input) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if baseURL == "" && init.Protocol == nil { 120 | return nil, NoBaseURLError 121 | } 122 | 123 | if baseURL != "" { 124 | init.BaseURL = &baseURL 125 | } 126 | 127 | return init.New(options) 128 | } 129 | 130 | // https://urlpattern.spec.whatwg.org/#url-pattern-create 131 | func (init *URLPatternInit) New(opt *Options) (*URLPattern, error) { 132 | if opt == nil { 133 | opt = &Options{} 134 | } 135 | 136 | processedInit, err := init.process("pattern", nil, nil, nil, nil, nil, nil, nil, nil) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | star := "*" 142 | if processedInit.Protocol == nil { 143 | processedInit.Protocol = &star 144 | } 145 | if processedInit.Username == nil { 146 | processedInit.Username = &star 147 | } 148 | if processedInit.Password == nil { 149 | processedInit.Password = &star 150 | } 151 | if processedInit.Hostname == nil { 152 | processedInit.Hostname = &star 153 | } 154 | if processedInit.Port == nil { 155 | processedInit.Port = &star 156 | } 157 | if processedInit.Pathname == nil { 158 | processedInit.Pathname = &star 159 | } 160 | if processedInit.Search == nil { 161 | processedInit.Search = &star 162 | } 163 | if processedInit.Hash == nil { 164 | processedInit.Hash = &star 165 | } 166 | 167 | var emptyString string 168 | for _, s := range specialSchemeList { 169 | if *processedInit.Protocol == s && *processedInit.Port == DefaultPorts[s] { 170 | processedInit.Port = &emptyString 171 | break 172 | } 173 | } 174 | 175 | defaultOptions := options{} 176 | 177 | urlPattern := &URLPattern{} 178 | urlPattern.protocol, err = compileComponent(*processedInit.Protocol, canonicalizeProtocol, defaultOptions) 179 | if err != nil { 180 | return nil, err 181 | } 182 | urlPattern.username, err = compileComponent(*processedInit.Username, canonicalizeUsername, defaultOptions) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | urlPattern.password, err = compileComponent(*processedInit.Password, canonicalizePassword, defaultOptions) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | // If the result running hostname pattern is an IPv6 address given processedInit["hostname"] is true, then set urlPattern’s hostname component to the result of compiling a component given processedInit["hostname"], canonicalize an IPv6 hostname, and hostname options. 193 | 194 | hostnameOptions := options{delimiterCodePoint: '.'} 195 | if hostnamePatternIsIPv6Address(*processedInit.Hostname) { 196 | urlPattern.hostname, err = compileComponent(*processedInit.Hostname, canonicalizeIPv6Hostname, hostnameOptions) 197 | if err != nil { 198 | return nil, err 199 | } 200 | } else if urlPattern.protocol.protocolComponentMatchesSpecialScheme() || *processedInit.Protocol == "*" { 201 | urlPattern.hostname, err = compileComponent(*processedInit.Hostname, canonicalizeDomainName, hostnameOptions) 202 | if err != nil { 203 | return nil, err 204 | } 205 | } else { 206 | urlPattern.hostname, err = compileComponent(*processedInit.Hostname, func(s string) (string, error) { return canonicalizeHostname(s, "") }, hostnameOptions) 207 | if err != nil { 208 | return nil, err 209 | } 210 | } 211 | 212 | urlPattern.port, err = compileComponent(*processedInit.Port, func(s string) (string, error) { return canonicalizePort(s, "") }, defaultOptions) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | compileOptions := defaultOptions 218 | compileOptions.ignoreCase = opt.IgnoreCase 219 | 220 | pathnameOptions := options{'/', '/', false} 221 | 222 | if urlPattern.protocol.protocolComponentMatchesSpecialScheme() { 223 | pathCompileOptions := pathnameOptions 224 | pathCompileOptions.ignoreCase = opt.IgnoreCase 225 | 226 | urlPattern.pathname, err = compileComponent(*processedInit.Pathname, canonicalizePathname, pathCompileOptions) 227 | if err != nil { 228 | return nil, err 229 | } 230 | } else { 231 | urlPattern.pathname, err = compileComponent(*processedInit.Pathname, canonicalizeOpaquePathname, compileOptions) 232 | if err != nil { 233 | return nil, err 234 | } 235 | } 236 | 237 | urlPattern.search, err = compileComponent(*processedInit.Search, canonicalizeSearch, compileOptions) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | urlPattern.hash, err = compileComponent(*processedInit.Hash, canonicalizeHash, compileOptions) 243 | if err != nil { 244 | return nil, err 245 | } 246 | 247 | return urlPattern, nil 248 | } 249 | 250 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-exec 251 | func (u *URLPattern) ExecInit(input *URLPatternInit) *URLPatternResult { 252 | protocol := "" 253 | username := "" 254 | password := "" 255 | hostname := "" 256 | port := "" 257 | pathname := "" 258 | search := "" 259 | hash := "" 260 | 261 | inputs := []*URLPatternInit{input} 262 | 263 | applyResult, err := input.process("url", &protocol, &username, &password, &hostname, &port, &pathname, &search, &hash) 264 | if err != nil { 265 | return nil 266 | } 267 | 268 | protocol = *applyResult.Protocol 269 | username = *applyResult.Username 270 | password = *applyResult.Password 271 | hostname = *applyResult.Hostname 272 | port = *applyResult.Port 273 | pathname = *applyResult.Pathname 274 | search = *applyResult.Search 275 | hash = *applyResult.Hash 276 | 277 | r := u.match(protocol, username, password, hostname, port, pathname, search, hash) 278 | if r != nil { 279 | r.InitInputs = inputs 280 | } 281 | 282 | return r 283 | } 284 | 285 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-exec 286 | func (u *URLPattern) Exec(input, baseURLString string) *URLPatternResult { 287 | protocol := "" 288 | username := "" 289 | password := "" 290 | hostname := "" 291 | port := "" 292 | pathname := "" 293 | search := "" 294 | hash := "" 295 | 296 | inputs := []string{input} 297 | 298 | var baseURL *url.Url 299 | var err error 300 | 301 | if baseURLString != "" { 302 | baseURL, err = url.Parse(baseURLString) 303 | if err != nil { 304 | return nil 305 | } 306 | 307 | inputs = append(inputs, baseURLString) 308 | } 309 | 310 | ur, err := urlParser.BasicParser(input, baseURL, nil, url.NoState) 311 | if err != nil { 312 | return nil 313 | } 314 | 315 | protocol = ur.Scheme() 316 | username = ur.Username() 317 | password = ur.Password() 318 | hostname = ur.Hostname() 319 | port = ur.Port() 320 | pathname = ur.Pathname() 321 | search = ur.Query() 322 | hash = ur.Fragment() 323 | 324 | r := u.match(protocol, username, password, hostname, port, pathname, search, hash) 325 | if r != nil { 326 | r.Inputs = inputs 327 | } 328 | 329 | return r 330 | } 331 | 332 | // https://urlpattern.spec.whatwg.org/#url-pattern-match 333 | func (u *URLPattern) match(protocol, username, password, hostname, port, pathname, search, hash string) *URLPatternResult { 334 | protocolExecResult := u.protocol.regularExpression.FindStringSubmatch(protocol) 335 | usernameExecResult := u.username.regularExpression.FindStringSubmatch(username) 336 | passwordExecResult := u.password.regularExpression.FindStringSubmatch(password) 337 | hostnameExecResult := u.hostname.regularExpression.FindStringSubmatch(hostname) 338 | portExecResult := u.port.regularExpression.FindStringSubmatch(port) 339 | pathnameExecResult := u.pathname.regularExpression.FindStringSubmatch(pathname) 340 | searchExecResult := u.search.regularExpression.FindStringSubmatch(search) 341 | hashExecResult := u.hash.regularExpression.FindStringSubmatch(hash) 342 | 343 | if protocolExecResult == nil || 344 | usernameExecResult == nil || 345 | passwordExecResult == nil || 346 | hostnameExecResult == nil || 347 | portExecResult == nil || 348 | pathnameExecResult == nil || 349 | searchExecResult == nil || 350 | hashExecResult == nil { 351 | return nil 352 | } 353 | 354 | result := &URLPatternResult{} 355 | result.Protocol = createComponentMatchResult(*u.protocol, protocol, protocolExecResult) 356 | result.Username = createComponentMatchResult(*u.username, username, usernameExecResult) 357 | result.Password = createComponentMatchResult(*u.password, password, passwordExecResult) 358 | result.Hostname = createComponentMatchResult(*u.hostname, hostname, hostnameExecResult) 359 | result.Port = createComponentMatchResult(*u.port, port, portExecResult) 360 | result.Pathname = createComponentMatchResult(*u.pathname, pathname, pathnameExecResult) 361 | result.Search = createComponentMatchResult(*u.search, search, searchExecResult) 362 | result.Hash = createComponentMatchResult(*u.hash, hash, hashExecResult) 363 | 364 | return result 365 | } 366 | 367 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-test 368 | func (u *URLPattern) Test(input, baseURL string) bool { 369 | return u.Exec(input, baseURL) != nil 370 | } 371 | 372 | // https://urlpattern.spec.whatwg.org/#dom-urlpattern-test 373 | func (u *URLPattern) TestInit(input *URLPatternInit) bool { 374 | return u.ExecInit(input) != nil 375 | } 376 | 377 | // https://urlpattern.spec.whatwg.org/#url-pattern-has-regexp-groups 378 | func (u *URLPattern) HasRegexpGroups() bool { 379 | return u.protocol.hasRegexpGroups || 380 | u.username.hasRegexpGroups || 381 | u.password.hasRegexpGroups || 382 | u.hostname.hasRegexpGroups || 383 | u.port.hasRegexpGroups || 384 | u.pathname.hasRegexpGroups || 385 | u.search.hasRegexpGroups || 386 | u.hash.hasRegexpGroups 387 | } 388 | 389 | // https://urlpattern.spec.whatwg.org/#create-a-component-match-result 390 | func createComponentMatchResult(component component, input string, execResult []string) URLPatternComponentResult { 391 | result := URLPatternComponentResult{Input: input} 392 | 393 | if len(execResult)-1 == 0 || (len(execResult) == 2 && execResult[0] == "" && execResult[1] == "") { 394 | return result 395 | } 396 | 397 | result.Groups = make(map[string]string, len(execResult)-1) 398 | for index := 1; index < len(execResult); index++ { 399 | name := component.groupNameList[index-1] 400 | value := execResult[index] 401 | 402 | result.Groups[name] = value 403 | } 404 | 405 | return result 406 | } 407 | 408 | type Options struct { 409 | IgnoreCase bool 410 | } 411 | 412 | // https://urlpattern.spec.whatwg.org/#dictdef-urlpatterninit 413 | type URLPatternInit struct { 414 | Protocol *string 415 | Username *string 416 | Password *string 417 | Hostname *string 418 | Port *string 419 | Pathname *string 420 | Search *string 421 | Hash *string 422 | 423 | BaseURL *string 424 | } 425 | 426 | // https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit 427 | func (init *URLPatternInit) process(iType string, protocol, username, password, hostname, port, pathname, search, hash *string) (*URLPatternInit, error) { 428 | result := &URLPatternInit{protocol, username, password, hostname, port, pathname, search, hash, nil} 429 | 430 | var ( 431 | baseURL *url.Url 432 | err error 433 | ) 434 | if init.BaseURL != nil { 435 | baseURL, err = url.Parse(*init.BaseURL) 436 | if err != nil { 437 | return nil, err 438 | } 439 | 440 | if init.Protocol == nil { 441 | p := processBaseURLString(baseURL.Scheme(), iType) 442 | result.Protocol = &p 443 | } 444 | 445 | // TODO: the end of this block can be simplified, but let's be as close as possible from the standard algorithm for now 446 | 447 | if iType != "pattern" && init.Protocol == nil && init.Hostname == nil && init.Port == nil && init.Username == nil { 448 | u := processBaseURLString(baseURL.Username(), iType) 449 | result.Username = &u 450 | } 451 | 452 | if iType != "pattern" && init.Protocol == nil && init.Hostname == nil && init.Port == nil && init.Username == nil && init.Password == nil { 453 | password := baseURL.Password() 454 | p := processBaseURLString(password, iType) 455 | result.Password = &p 456 | } 457 | 458 | if init.Protocol == nil && init.Hostname == nil { 459 | baseHost := baseURL.Hostname() 460 | h := processBaseURLString(baseHost, iType) 461 | result.Hostname = &h 462 | } 463 | 464 | if init.Protocol == nil && init.Hostname == nil && init.Port == nil { 465 | p := baseURL.Port() 466 | result.Port = &p 467 | } 468 | 469 | if init.Protocol == nil && init.Hostname == nil && init.Port == nil && init.Pathname == nil { 470 | p := processBaseURLString(baseURL.Pathname(), iType) 471 | result.Pathname = &p 472 | } 473 | 474 | if init.Protocol == nil && init.Hostname == nil && init.Port == nil && init.Pathname == nil && init.Search == nil { 475 | s := processBaseURLString(baseURL.Query(), iType) 476 | result.Search = &s 477 | } 478 | 479 | if init.Protocol == nil && init.Hostname == nil && init.Port == nil && init.Pathname == nil && init.Search == nil && init.Hash == nil { 480 | h := processBaseURLString(baseURL.Fragment(), iType) 481 | result.Hash = &h 482 | } 483 | } 484 | 485 | if init.Protocol != nil { 486 | p, err := processProtocolForInit(*init.Protocol, iType) 487 | if err != nil { 488 | return nil, err 489 | } 490 | 491 | result.Protocol = &p 492 | } 493 | 494 | if init.Username != nil { 495 | u, err := processUsernameForInit(*init.Username, iType) 496 | if err != nil { 497 | return nil, err 498 | } 499 | 500 | result.Username = &u 501 | } 502 | 503 | if init.Password != nil { 504 | p, err := processPasswordForInit(*init.Password, iType) 505 | if err != nil { 506 | return nil, err 507 | } 508 | 509 | result.Password = &p 510 | } 511 | 512 | var proto string 513 | if result.Protocol == nil { 514 | proto = "" 515 | } else { 516 | proto = *result.Protocol 517 | } 518 | 519 | if init.Hostname != nil { 520 | h, err := processHostnameForInit(*init.Hostname, proto, iType) 521 | if err != nil { 522 | return nil, err 523 | } 524 | 525 | result.Hostname = &h 526 | } 527 | 528 | if init.Port != nil { 529 | p, err := processPortForInit(*init.Port, proto, iType) 530 | if err != nil { 531 | return nil, err 532 | } 533 | 534 | result.Port = &p 535 | } 536 | 537 | if init.Pathname != nil { 538 | result.Pathname = init.Pathname 539 | 540 | // TODO: according to the spec, we should check that he path is opaque, but it's illogical and breaks the tests 541 | if baseURL != nil && !baseURL.OpaquePath() && !isAbsolutePathname(*result.Pathname, iType) { 542 | baseURLPath := processBaseURLString(baseURL.Pathname(), iType) 543 | 544 | slashIndex := strings.LastIndex(baseURLPath, "/") 545 | if slashIndex != -1 { 546 | newPathname := baseURLPath[0:slashIndex+1] + *result.Pathname 547 | result.Pathname = &newPathname 548 | } 549 | } 550 | 551 | p, err := processPathnameForInit(*result.Pathname, proto, iType) 552 | if err != nil { 553 | return nil, err 554 | } 555 | 556 | result.Pathname = &p 557 | } 558 | 559 | if init.Search != nil { 560 | s, err := processSearchForInit(*init.Search, iType) 561 | if err != nil { 562 | return nil, err 563 | } 564 | 565 | result.Search = &s 566 | } 567 | 568 | if init.Hash != nil { 569 | h, err := processHashForInit(*init.Hash, iType) 570 | if err != nil { 571 | return nil, err 572 | } 573 | 574 | result.Hash = &h 575 | } 576 | 577 | return result, nil 578 | } 579 | 580 | // https://urlpattern.spec.whatwg.org/#process-a-base-url-string 581 | func processBaseURLString(input, uType string) string { 582 | if uType != "pattern" { 583 | return input 584 | } 585 | 586 | return escapePatternString(input) 587 | } 588 | 589 | // https://urlpattern.spec.whatwg.org/#process-protocol-for-init 590 | func processProtocolForInit(value, pType string) (string, error) { 591 | strippedValue := strings.TrimSuffix(value, ":") 592 | 593 | if pType == "pattern" { 594 | return strippedValue, nil 595 | } 596 | 597 | return canonicalizeProtocol(strippedValue) 598 | } 599 | 600 | // https://urlpattern.spec.whatwg.org/#process-username-for-init 601 | func processUsernameForInit(value, uType string) (string, error) { 602 | if uType == "pattern" { 603 | return value, nil 604 | } 605 | 606 | return canonicalizeUsername(value) 607 | } 608 | 609 | // https://urlpattern.spec.whatwg.org/#process-password-for-init 610 | func processPasswordForInit(value, uType string) (string, error) { 611 | if uType == "pattern" { 612 | return value, nil 613 | } 614 | 615 | return canonicalizePassword(value) 616 | } 617 | 618 | // https://urlpattern.spec.whatwg.org/#process-hostname-for-init 619 | func processHostnameForInit(value, protocolValue, uType string) (string, error) { 620 | if uType == "pattern" { 621 | return value, nil 622 | } 623 | 624 | if protocolValue == "" { 625 | return canonicalizeDomainName(value) 626 | } 627 | 628 | for _, s := range specialSchemeList { 629 | if protocolValue == s { 630 | return canonicalizeDomainName(value) 631 | } 632 | } 633 | 634 | return canonicalizeHostname(value, protocolValue) 635 | } 636 | 637 | // https://urlpattern.spec.whatwg.org/#process-port-for-init 638 | func processPortForInit(portValue, protocolValue, pType string) (string, error) { 639 | if pType == "pattern" { 640 | return portValue, nil 641 | } 642 | 643 | return canonicalizePort(portValue, protocolValue) 644 | } 645 | 646 | // https://urlpattern.spec.whatwg.org/#process-pathname-for-init 647 | func processPathnameForInit(pathnameValue, protocolValue, ptype string) (string, error) { 648 | if ptype == "pattern" { 649 | return pathnameValue, nil 650 | } 651 | 652 | if protocolValue == "" { 653 | return canonicalizePathname(pathnameValue) 654 | } 655 | 656 | for _, ss := range specialSchemeList { 657 | if protocolValue == ss { 658 | return canonicalizePathname(pathnameValue) 659 | } 660 | } 661 | 662 | return canonicalizeOpaquePathname(pathnameValue) 663 | } 664 | 665 | // https://urlpattern.spec.whatwg.org/#process-search-for-init 666 | func processSearchForInit(value, sType string) (string, error) { 667 | strippedValue := strings.TrimPrefix(value, "?") 668 | 669 | if sType == "pattern" { 670 | return strippedValue, nil 671 | } 672 | 673 | return canonicalizeSearch(strippedValue) 674 | } 675 | 676 | // https://urlpattern.spec.whatwg.org/#process-hash-for-init 677 | func processHashForInit(value, hType string) (string, error) { 678 | strippedValue := strings.TrimPrefix(value, "#") 679 | 680 | if hType == "pattern" { 681 | return strippedValue, nil 682 | } 683 | 684 | return canonicalizeHash(value) 685 | } 686 | 687 | // https://urlpattern.spec.whatwg.org/#is-an-absolute-pathname 688 | func isAbsolutePathname(input, pType string) bool { 689 | if input == "" { 690 | return false 691 | } 692 | 693 | if input[0] == '/' { 694 | return true 695 | } 696 | 697 | if pType == "url" { 698 | return false 699 | } 700 | 701 | return strings.HasPrefix(input, "\\/") || strings.HasPrefix(input, "{/") 702 | } 703 | 704 | // https://urlpattern.spec.whatwg.org/#hostname-pattern-is-an-ipv6-address 705 | func hostnamePatternIsIPv6Address(input string) bool { 706 | if len(input) < 2 { 707 | return false 708 | } 709 | 710 | i := []rune(input) 711 | 712 | if i[0] == '[' { 713 | return true 714 | } 715 | if i[0] == '{' && i[1] == '[' { 716 | return true 717 | } 718 | if i[0] == '\\' && i[1] == '[' { 719 | return true 720 | } 721 | 722 | return false 723 | } 724 | -------------------------------------------------------------------------------- /urlpattern_test.go: -------------------------------------------------------------------------------- 1 | package urlpattern_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/dunglas/go-urlpattern" 13 | "github.com/nlnwa/whatwg-url/url" 14 | ) 15 | 16 | // Port of https://github.com/web-platform-tests/wpt/blob/d3e55612911b00cb53271476de610e75a8603ae7/urlpattern/resources/urlpatterntests.js 17 | 18 | //go:generate curl https://raw.githubusercontent.com/web-platform-tests/wpt/master/urlpattern/resources/urlpatterntestdata.json -o testdata/urlpatterntestdata.json 19 | 20 | type Entry struct { 21 | Pattern []interface{} `json:"pattern"` 22 | Inputs []interface{} `json:"inputs"` 23 | ExactlyEmptyComponents []string `json:"exactly_empty_components"` 24 | ExpectedObj interface{} `json:"expected_obj"` 25 | ExpectedMatch interface{} `json:"expected_match"` 26 | } 27 | 28 | func TestURLPattern(t *testing.T) { 29 | content, err := os.ReadFile("testdata/urlpatterntestdata.json") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | var data []Entry 35 | if err := json.Unmarshal(content, &data); err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | for i, entry := range data { 40 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 41 | pattern, err := newPattern(t, &entry) 42 | 43 | if e, _ := entry.ExpectedObj.(string); e == "error" { 44 | if err == nil { 45 | t.Logf("want error for %#v", entry.Pattern) 46 | t.FailNow() 47 | } 48 | 49 | return 50 | } 51 | 52 | if err != nil { 53 | t.Logf("unexpected error: %s (%#v)", err, entry) 54 | t.FailNow() 55 | } 56 | 57 | assertExpectedObject(t, entry, pattern) 58 | 59 | if e, _ := entry.ExpectedMatch.(string); e == "error" { 60 | _, err := callTest(pattern, entry) 61 | if err == nil { 62 | t.Logf("want error when running Test for %#v", entry) 63 | t.FailNow() 64 | } 65 | _, err = callExec(pattern, entry) 66 | if err == nil { 67 | t.Logf("want error when running Test for %#v", entry) 68 | t.FailNow() 69 | } 70 | 71 | return 72 | } 73 | 74 | testResult, err := callTest(pattern, entry) 75 | if err != nil { 76 | if len(entry.Inputs) == 1 { 77 | if i, ok := entry.Inputs[0].(map[string]interface{}); ok { 78 | if p, _ := i["protocol"].(string); p == "café" { 79 | t.Skip("TODO: check why this fails, probably a bug in the test suite") 80 | } 81 | } 82 | } 83 | 84 | t.Logf("unexpected error when running Test: %s (%#v)", err, entry) 85 | t.FailNow() 86 | } 87 | 88 | expectedTestResult := entry.ExpectedMatch != nil 89 | 90 | if testResult != expectedTestResult { 91 | if len(entry.Pattern) > 0 { 92 | e, _ := entry.Pattern[0].(map[string]interface{}) 93 | if pa := e["pathname"]; pa != nil { 94 | p := pa.(string) 95 | if strings.Contains(p, "[") && (strings.Contains(p, "--") || strings.Contains(p, "&&")) { 96 | t.Skip("Advanced unicode features aren't supported by Go") 97 | } 98 | } 99 | } 100 | 101 | t.Logf("Test must return %v; got %v (%#v)", expectedTestResult, testResult, entry) 102 | t.FailNow() 103 | } 104 | 105 | execResult, err := callExec(pattern, entry) 106 | if err != nil { 107 | t.Logf("unexpected error when running Test: %s (%#v)", err, entry) 108 | t.FailNow() 109 | } 110 | 111 | if entry.ExpectedMatch == nil { 112 | if execResult != nil { 113 | t.Logf("Match must return nil, go %#v (%#v)", execResult, entry) 114 | t.Fail() 115 | } 116 | 117 | return 118 | } 119 | 120 | expectedObj := entry.ExpectedMatch.(map[string]interface{}) 121 | if _, ok := expectedObj["inputs"]; !ok { 122 | expectedObj["inputs"] = entry.Inputs 123 | } 124 | 125 | if er := newExpectedResult(entry); !reflect.DeepEqual(er, execResult) { 126 | t.Logf("want %#v; got %#v (%#v)", er, execResult, entry) 127 | t.Fail() 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func newPattern(t *testing.T, entry *Entry) (*urlpattern.URLPattern, error) { 134 | var baseURL string 135 | options := &urlpattern.Options{} 136 | 137 | switch len(entry.Pattern) { 138 | case 0: 139 | i := &urlpattern.URLPatternInit{} 140 | return i.New(options) 141 | 142 | case 2: 143 | switch entry.Pattern[1].(type) { 144 | case map[string]interface{}: 145 | options.IgnoreCase = true 146 | 147 | case string: 148 | baseURL = entry.Pattern[1].(string) 149 | 150 | default: 151 | return nil, errors.New("invalid constructor parameter #1") 152 | } 153 | 154 | case 3: 155 | options.IgnoreCase = true 156 | 157 | bu, ok := entry.Pattern[1].(string) 158 | if !ok { 159 | return nil, errors.New("invalid constructor parameter #2") 160 | } 161 | 162 | baseURL = bu 163 | } 164 | 165 | switch entry.Pattern[0].(type) { 166 | case string: 167 | return urlpattern.New(entry.Pattern[0].(string), baseURL, options) 168 | 169 | case map[string]interface{}: 170 | if baseURL != "" { 171 | return nil, errors.New("Invalid second argument baseURL provided with a URLPatternInit input. Use the URLPatternInit.baseURL property instead.") 172 | } 173 | 174 | m := entry.Pattern[0].(map[string]interface{}) 175 | u := initFromObj(m) 176 | 177 | return u.New(options) 178 | } 179 | 180 | t.Fatalf("invalid entry pattern %#v", entry.Pattern) 181 | 182 | return nil, nil 183 | } 184 | 185 | func newExpectedResult(e Entry) *urlpattern.URLPatternResult { 186 | 187 | expectedResult := urlpattern.URLPatternResult{} 188 | for k, v := range e.ExpectedMatch.(map[string]interface{}) { 189 | if k == "inputs" { 190 | for _, initInput := range v.([]interface{}) { 191 | if ip, ok := initInput.(map[string]interface{}); ok { 192 | expectedResult.InitInputs = append(expectedResult.InitInputs, initFromObj(ip)) 193 | } else { 194 | expectedResult.Inputs = append(expectedResult.Inputs, initInput.(string)) 195 | } 196 | } 197 | 198 | continue 199 | } 200 | mv := v.(map[string]interface{}) 201 | component := urlpattern.URLPatternComponentResult{} 202 | component.Input = mv["input"].(string) 203 | len := len(mv["groups"].(map[string]interface{})) 204 | 205 | if len > 0 { 206 | component.Groups = make(map[string]string, len) 207 | 208 | for k, v := range mv["groups"].(map[string]interface{}) { 209 | if v == nil { 210 | // TODO: this should probably be nil, but it's currently not implemented 211 | component.Groups[k] = "" 212 | continue 213 | } 214 | 215 | component.Groups[k] = v.(string) 216 | } 217 | } 218 | 219 | switch k { 220 | case "protocol": 221 | expectedResult.Protocol = component 222 | 223 | case "username": 224 | expectedResult.Username = component 225 | 226 | case "password": 227 | expectedResult.Password = component 228 | 229 | case "hostname": 230 | expectedResult.Hostname = component 231 | 232 | case "port": 233 | expectedResult.Port = component 234 | 235 | case "pathname": 236 | expectedResult.Pathname = component 237 | 238 | case "search": 239 | expectedResult.Search = component 240 | 241 | case "hash": 242 | expectedResult.Hash = component 243 | } 244 | } 245 | 246 | return &expectedResult 247 | } 248 | 249 | func stringOrNil(v interface{}) *string { 250 | if v == nil { 251 | return nil 252 | } 253 | 254 | s := v.(string) 255 | 256 | return &s 257 | } 258 | 259 | func callTest(pattern *urlpattern.URLPattern, entry Entry) (bool, error) { 260 | if len(entry.Inputs) == 0 { 261 | return pattern.TestInit(&urlpattern.URLPatternInit{}), nil 262 | } 263 | 264 | if u, ok := entry.Inputs[0].(string); ok { 265 | var baseURL string 266 | if len(entry.Inputs) > 1 { 267 | baseURL = entry.Inputs[1].(string) 268 | } 269 | 270 | return pattern.Test(u, baseURL), nil 271 | } 272 | 273 | if len(entry.Inputs) > 1 { 274 | return false, errors.New("invalid constructor parameter #1") 275 | } 276 | 277 | return pattern.TestInit(initFromObj(entry.Inputs[0].(map[string]interface{}))), nil 278 | } 279 | 280 | func callExec(pattern *urlpattern.URLPattern, entry Entry) (*urlpattern.URLPatternResult, error) { 281 | if len(entry.Inputs) == 0 { 282 | return pattern.ExecInit(&urlpattern.URLPatternInit{}), nil 283 | } 284 | 285 | if u, ok := entry.Inputs[0].(string); ok { 286 | var baseURL string 287 | if len(entry.Inputs) > 1 { 288 | baseURL = entry.Inputs[1].(string) 289 | } 290 | 291 | return pattern.Exec(u, baseURL), nil 292 | } 293 | 294 | if len(entry.Inputs) > 1 { 295 | return nil, errors.New("invalid constructor parameter #1") 296 | } 297 | 298 | return pattern.ExecInit(initFromObj(entry.Inputs[0].(map[string]interface{}))), nil 299 | } 300 | 301 | func initFromObj(m map[string]interface{}) *urlpattern.URLPatternInit { 302 | return &urlpattern.URLPatternInit{ 303 | Protocol: stringOrNil(m["protocol"]), 304 | Username: stringOrNil(m["username"]), 305 | Password: stringOrNil(m["password"]), 306 | Hostname: stringOrNil(m["hostname"]), 307 | Port: stringOrNil(m["port"]), 308 | Pathname: stringOrNil(m["pathname"]), 309 | Search: stringOrNil(m["search"]), 310 | Hash: stringOrNil(m["hash"]), 311 | BaseURL: stringOrNil(m["baseURL"]), 312 | } 313 | } 314 | 315 | var earlierComponents = map[string][]string{ 316 | "hostname": {"protocol"}, 317 | "port": {"protocol", "hostname"}, 318 | "pathname": {"protocol", "hostname", "port"}, 319 | "search": {"protocol", "hostname", "port", "pathname"}, 320 | "hash": {"protocol", "hostname", "port", "pathname", "search"}, 321 | } 322 | 323 | func buildExpected(entry Entry, component string) *string { 324 | if entry.ExpectedObj == nil { 325 | for _, c := range entry.ExactlyEmptyComponents { 326 | if c == component { 327 | es := "" 328 | return &es 329 | } 330 | } 331 | 332 | if len(entry.Pattern) > 0 { 333 | star := "*" 334 | 335 | p, ok := entry.Pattern[0].(map[string]interface{}) 336 | if ok { 337 | if p[component] != nil { 338 | v := p[component].(string) 339 | 340 | return &v 341 | } 342 | 343 | for _, e := range earlierComponents[component] { 344 | if _, ok := p[e]; ok { 345 | return &star 346 | } 347 | } 348 | 349 | var baseURL *url.Url 350 | if bu, ok := p["baseURL"]; ok { 351 | baseURL, _ = url.Parse(bu.(string)) 352 | } else if len(entry.Pattern) > 1 { 353 | if bu, ok := entry.Pattern[1].(string); ok { 354 | baseURL, _ = url.Parse(bu) 355 | } 356 | } 357 | 358 | if baseURL != nil && component != "username" && component != "password" { 359 | var baseValue string 360 | switch component { 361 | case "protocol": 362 | baseValue = baseURL.Protocol() 363 | baseValue = baseValue[:len(baseValue)-1] 364 | 365 | case "hostname": 366 | baseValue = baseURL.Hostname() 367 | 368 | case "port": 369 | baseValue = baseURL.Port() 370 | 371 | case "pathname": 372 | baseValue = baseURL.Pathname() 373 | 374 | case "search": 375 | baseValue = baseURL.Search()[1:] 376 | 377 | case "hash": 378 | baseValue = baseURL.Hash()[1:] 379 | } 380 | 381 | return &baseValue 382 | } 383 | 384 | return &star 385 | } 386 | 387 | } 388 | 389 | return nil 390 | } 391 | 392 | o := entry.ExpectedObj.(map[string]interface{}) 393 | e, ok := o[component] 394 | if !ok { 395 | return nil 396 | } 397 | 398 | expected := e.(string) 399 | 400 | return &expected 401 | } 402 | 403 | func assertExpectedObject(t *testing.T, entry Entry, pattern *urlpattern.URLPattern) { 404 | assertExpectedObjectProp(t, "protocol", entry, pattern.Protocol()) 405 | assertExpectedObjectProp(t, "username", entry, pattern.Username()) 406 | assertExpectedObjectProp(t, "password", entry, pattern.Password()) 407 | assertExpectedObjectProp(t, "hostname", entry, pattern.Hostname()) 408 | assertExpectedObjectProp(t, "port", entry, pattern.Port()) 409 | assertExpectedObjectProp(t, "pathname", entry, pattern.Pathname()) 410 | assertExpectedObjectProp(t, "search", entry, pattern.Search()) 411 | assertExpectedObjectProp(t, "hash", entry, pattern.Hash()) 412 | 413 | } 414 | 415 | func assertExpectedObjectProp(t *testing.T, key string, entry Entry, value string) { 416 | expected := buildExpected(entry, key) 417 | if expected == nil { 418 | return 419 | } 420 | 421 | if *expected != value { 422 | t.Logf("%s: want %q, got %q (%#v)", key, *expected, value, entry.Pattern) 423 | t.FailNow() 424 | } 425 | } 426 | 427 | func Example() { 428 | pattern, err := urlpattern.New("/books/:id", "https://example.com", nil) 429 | if err != nil { 430 | panic(err) 431 | } 432 | 433 | fmt.Printf("%t\n", pattern.Test("https://example.com/books/123", "")) 434 | fmt.Printf("%t\n", pattern.Test("https://example.com/authors/123", "")) 435 | 436 | fmt.Printf("%v", pattern.Exec("123", "https://example.com/books/").Pathname.Groups) 437 | 438 | // Output: true 439 | // false 440 | // map[id:123] 441 | } 442 | --------------------------------------------------------------------------------