diff --git a/.appends/.github/labels.yml b/.appends/.github/labels.yml new file mode 100644 index 000000000..2bef75771 --- /dev/null +++ b/.appends/.github/labels.yml @@ -0,0 +1,8 @@ +# ----------------------------------------------------------------------------------------- # +# These are the repository-specific labels that augment the Exercise-wide labels defined in # +# https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # +# ----------------------------------------------------------------------------------------- # + +- name: "bug?" + description: "" + color: "eb6420" diff --git a/.gha.gofmt.sh b/.gha.gofmt.sh new file mode 100755 index 000000000..2902ab797 --- /dev/null +++ b/.gha.gofmt.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd "$(dirname $0)" +if [ -n "$(go fmt ./...)" ]; then + echo "Go code is not formatted, run 'go fmt github.com/exercism/cli/...'" >&2 + exit 1 +fi diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..73ac4bb57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,4 @@ + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5c6cb5943 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 + +updates: + # Keep dependencies for GitHub Actions up-to-date + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + labels: + - 'x:size/small' diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000..cd989c70f --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,177 @@ +# --------------------------------------------------------------- # +# This is an auto-generated file - Do not manually edit this file # +# --------------------------------------------------------------- # + +# This file is automatically generated by concatenating two files: +# +# 1. The Exercism-wide labels: defined in https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml +# 2. The repository-specific labels: defined in the `.appends/.github/labels.yml` file within this repository. +# +# If any of these two files change, a pull request is automatically created containing a re-generated version of this file. +# Consequently, to change repository-specific labels you should update the `.appends/.github/labels.yml` file and _not_ this file. +# +# When the pull request has been merged, the GitHub labels will be automatically updated by the "Sync labels" workflow. +# This typically takes 5-10 minutes. + +# --------------------------------------------------------------------- # +# These are the Exercism-wide labels which are shared across all repos. # +# --------------------------------------------------------------------- # + +# The following Exercism-wide labels are used to show "tasks" on the website, which will point users to things they can contribute to. + +# The `x:action/` labels describe what sort of work the contributor will be engaged in when working on the issue +- name: "x:action/create" + description: "Work on something from scratch" + color: "ffffff" + +- name: "x:action/fix" + description: "Fix an issue" + color: "ffffff" + +- name: "x:action/improve" + description: "Improve existing functionality/content" + color: "ffffff" + +- name: "x:action/proofread" + description: "Proofread text" + color: "ffffff" + +- name: "x:action/sync" + description: "Sync content with its latest version" + color: "ffffff" + +# The `x:knowledge/` labels describe how much Exercism knowledge is required by the contributor +- name: "x:knowledge/none" + description: "No existing Exercism knowledge required" + color: "ffffff" + +- name: "x:knowledge/elementary" + description: "Little Exercism knowledge required" + color: "ffffff" + +- name: "x:knowledge/intermediate" + description: "Quite a bit of Exercism knowledge required" + color: "ffffff" + +- name: "x:knowledge/advanced" + description: "Comprehensive Exercism knowledge required" + color: "ffffff" + +# The `x:module/` labels indicate what part of Exercism the contributor will be working on +- name: "x:module/analyzer" + description: "Work on Analyzers" + color: "ffffff" + +- name: "x:module/concept" + description: "Work on Concepts" + color: "ffffff" + +- name: "x:module/concept-exercise" + description: "Work on Concept Exercises" + color: "ffffff" + +- name: "x:module/generator" + description: "Work on Exercise generators" + color: "ffffff" + +- name: "x:module/practice-exercise" + description: "Work on Practice Exercises" + color: "ffffff" + +- name: "x:module/representer" + description: "Work on Representers" + color: "ffffff" + +- name: "x:module/test-runner" + description: "Work on Test Runners" + color: "ffffff" + +# The `x:rep/` labels describe the amount of reputation to award +# +# For more information on reputation and how these labels should be used, +# check out https://exercism.org/docs/using/product/reputation +- name: "x:rep/tiny" + description: "Tiny amount of reputation" + color: "ffffff" + +- name: "x:rep/small" + description: "Small amount of reputation" + color: "ffffff" + +- name: "x:rep/medium" + description: "Medium amount of reputation" + color: "ffffff" + +- name: "x:rep/large" + description: "Large amount of reputation" + color: "ffffff" + +- name: "x:rep/massive" + description: "Massive amount of reputation" + color: "ffffff" + +# The `x:size/` labels describe the expected amount of work for a contributor +- name: "x:size/tiny" + description: "Tiny amount of work" + color: "ffffff" + +- name: "x:size/small" + description: "Small amount of work" + color: "ffffff" + +- name: "x:size/medium" + description: "Medium amount of work" + color: "ffffff" + +- name: "x:size/large" + description: "Large amount of work" + color: "ffffff" + +- name: "x:size/massive" + description: "Massive amount of work" + color: "ffffff" + +# The `x:status/` label indicates if there is already someone working on the issue +- name: "x:status/claimed" + description: "Someone is working on this issue" + color: "ffffff" + +# The `x:type/` labels describe what type of work the contributor will be engaged in +- name: "x:type/ci" + description: "Work on Continuous Integration (e.g. GitHub Actions workflows)" + color: "ffffff" + +- name: "x:type/coding" + description: "Write code that is not student-facing content (e.g. test-runners, generators, but not exercises)" + color: "ffffff" + +- name: "x:type/content" + description: "Work on content (e.g. exercises, concepts)" + color: "ffffff" + +- name: "x:type/docker" + description: "Work on Dockerfiles" + color: "ffffff" + +- name: "x:type/docs" + description: "Work on Documentation" + color: "ffffff" + +# This Exercism-wide label is added to all automatically created pull requests that help migrate/prepare a track for Exercism v3 +- name: "v3-migration 🤖" + description: "Preparing for Exercism v3" + color: "e99695" + +# This Exercism-wide label can be used to bulk-close issues in preparation for pausing community contributions +- name: "paused" + description: "Work paused until further notice" + color: "e4e669" + +# ----------------------------------------------------------------------------------------- # +# These are the repository-specific labels that augment the Exercise-wide labels defined in # +# https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # +# ----------------------------------------------------------------------------------------- # + +- name: "bug?" + description: "" + color: "eb6420" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..05aa6a748 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + tests: + name: Go ${{ matrix.go-version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + go-version: + - '1.20.x' + - '1.21.x' + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 + with: + go-version: ${{ matrix.go-version }} + + - name: Run Tests + run: | + go test -cover ./... + shell: bash + + formatting: + name: Go Format + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + + - name: Check formatting + run: ./.gha.gofmt.sh diff --git a/.github/workflows/pause-community-contributions.yml b/.github/workflows/pause-community-contributions.yml new file mode 100644 index 000000000..46f0c60b4 --- /dev/null +++ b/.github/workflows/pause-community-contributions.yml @@ -0,0 +1,22 @@ +name: Pause Community Contributions + +on: + issues: + types: + - opened + pull_request_target: + types: + - opened + +permissions: + issues: write + pull-requests: write + +jobs: + pause: + if: github.repository_owner == 'exercism' # Stops this job from running on forks + uses: exercism/github-actions/.github/workflows/community-contributions.yml@main + with: + forum_category: support + secrets: + github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..599f23bdb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: release + +on: + push: + tags: + - 'v*.*.*' # semver release tags + - 'v*.*.*-*' # pre-release tags for testing + +permissions: + contents: write # needed by goreleaser/goreleaser-action for publishing release artifacts + +jobs: + goreleaser: + runs-on: ubuntu-22.04 + steps: + + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: '1.20.x' + + - name: Import GPG Key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + + - name: Cut Release + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + with: + version: latest + args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 000000000..e7b99e504 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,19 @@ +name: Tools + +on: + push: + branches: + - main + paths: + - .github/labels.yml + - .github/workflows/sync-labels.yml + workflow_dispatch: + schedule: + - cron: 0 0 1 * * # First day of each month + +permissions: + issues: write + +jobs: + sync-labels: + uses: exercism/github-actions/.github/workflows/labels.yml@main diff --git a/.gitignore b/.gitignore index e849b2269..c7963cd6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ # Folders _obj _test +vendor/ +dist/ # Architecture specific extensions/prefixes *.[568vq] @@ -24,6 +26,8 @@ _testmain.go out/ release/ go-exercism +testercism # Intellij -/.idea \ No newline at end of file +/.idea + diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 000000000..86f2100cc --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,131 @@ +# You can find the GoReleaser documentation at http://goreleaser.com +version: 2 +project_name: exercism + +env: + - CGO_ENABLED=0 +builds: + - id: release-build + main: ./exercism/main.go + mod_timestamp: "{{ .CommitTimestamp }}" + flags: + - -trimpath # removes file system paths from compiled executable + ldflags: + - "-s -w" # strip debug symbols and DWARF debugging info + goos: + - darwin + - linux + - windows + - freebsd + - openbsd + goarch: + - amd64 + - 386 + - arm + - arm64 + - ppc64 + goarm: + - 5 + - 6 + ignore: + - goos: openbsd + goarch: arm + - goos: freebsd + goarch: arm + - id: installer-build + main: ./exercism/main.go + mod_timestamp: "{{ .CommitTimestamp }}" + flags: + - -trimpath # removes file system paths from compiled executable + ldflags: + - "-s -w" # strip debug symbols and DWARF debugging info + goos: + - windows + goarch: + - amd64 + - 386 + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +archives: + - id: release-archives + ids: + - release-build + name_template: >- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{- .Arch }}{{ end }} + {{- if .Arm }}v{{- .Arm }}{{ end }} + format_overrides: + - goos: windows + formats: ["zip"] + files: + - shell/** + - LICENSE + - README.md + - id: installer-archives + ids: + - installer-build + name_template: >- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else }}{{- .Arch }}{{ end }} + {{- if .Arm }}v{{- .Arm }}{{ end }} + format_overrides: + - goos: windows + formats: ["zip"] + files: + - shell/** + - LICENSE + - README.md + +checksum: + name_template: "{{ .ProjectName }}_checksums.txt" + ids: + - release-archives + - installer-archives + +signs: + - artifacts: checksum + args: + [ + "--batch", + "-u", + "{{ .Env.GPG_FINGERPRINT }}", + "--output", + "${signature}", + "--detach-sign", + "${artifact}", + ] + +release: + # Repo in which the release will be created. + # Default is extracted from the origin remote URL. + github: + owner: exercism + name: cli + + # If set to true, will not auto-publish the release. + # Default is false. + draft: true + + # If set to auto, will mark the release as not ready for production + # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 + # If set to true, will mark the release as not ready for production. + # Default is false. + prerelease: auto + + # You can change the name of the GitHub release. + # Default is `{{.Tag}}` + name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" diff --git a/.release/header.md b/.release/header.md new file mode 100644 index 000000000..c648e2d8b --- /dev/null +++ b/.release/header.md @@ -0,0 +1,3 @@ +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough + +--- diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e21905d4b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: go - -sudo: false - -go: - - 1.6 - - tip - -install: true - -script: - - go test $(go list ./... | grep -v /vendor/) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7055540a7..9de216f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,331 +2,608 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ----------------- +--- ## Next Release -* **Your contribution here** -* [#301](https://github.com/exercism/cli/pull/301) Return error message for unknown track status - [@neslom] -* [#299](https://github.com/exercism/cli/pull/299) List output uses track ID and problem from list - [@Tonkpils] +- **Your contribution here** + +## v3.5.8 (2025-09-24) + +- [#1215](https://github.com/exercism/cli/pull/1215) Update the token URL to point to the API settings page [@isaacg] +- [#1210](https://github.com/exercism/cli/pull/1210) Use mode 0700 for the config dir, not 0755; other users should not have access to the config [@isaacg] +- [#1205](https://github.com/exercism/cli/pull/1205) goreleaser: update deprecated archives.builds to new archives.ids [@isaacg] +- [#1213](https://github.com/exercism/cli/pull/1213) Include empty files in downloads [@isaacg] +- [#1206](https://github.com/exercism/cli/pull/1206) Drop team-specific logic from the CLI [@isaacg] + +## v3.5.7 (2025-07-14) + +- [#1202](https://github.com/exercism/cli/pull/1202) Add error decoding support for content type parameters such as charset - [@isaacg] +- [#1201](https://github.com/exercism/cli/pull/1201) Fix goreleaser deprecations - [@erikschierboom] + +## v3.5.6 (2025-07-06) + +- [#1199](https://github.com/exercism/cli/pull/1199) Support for Futhark in exercism test - [@erikschierboom] +- [#1198](https://github.com/exercism/cli/pull/1198) Show a "try again after" message when a response sets a Retry-After header - [@isaacg] +- [#1196](https://github.com/exercism/cli/pull/1196) Check HTTP response content type before trying to decode it as JSON - [@isaacg] + +## v3.5.5 (2025-05-30) + +- [#1192](https://github.com/exercism/cli/pull/1192) Change Idris test command to use slug - [@keiravillekode] + +## v3.5.4 (2024-11-15) + +- [#1183](https://github.com/exercism/cli/pull/1183) Add support for Uiua track to `exercism test` - [@vaeng] + +## v3.5.3 (2024-11-03) + +- [#1178](https://github.com/exercism/cli/pull/1178) Add arm64-assembly test configuration [@keiravillekode] +- [#1177](https://github.com/exercism/cli/pull/1177) refactored exercism.io links to exercism.org [@ladokp] +- [#1165](https://github.com/exercism/cli/pull/1165) Add support for the YAMLScript language [@ingydotnet] + +## v3.5.2 (2024-10-09) + +- [#1174](https://github.com/exercism/cli/pull/1174) Fix an issue with `exercism completion bash` where the command name is not present in the completion output. - [@petrem] +- [#1172](https://github.com/exercism/cli/pull/1172) Fix `exercism test` command for Batch track - [@bnandras] + +## v3.5.1 (2024-08-28) + +- [#1162](https://github.com/exercism/cli/pull/1162) Add support for Roc to `exercism test` - [@ageron] + +## v3.5.0 (2024-08-22) + +- [#1157](https://github.com/exercism/cli/pull/1157) Add support for Batch to `exercism test` - [@GroophyLifefor] +- [#1159](https://github.com/exercism/cli/pull/1159) Fix duplicated `t` alias - + [@muzimuzhi] + +## v3.4.2 (2024-08-20) + +- [#1156](https://github.com/exercism/cli/pull/1156) Add `test` command to Shell completions - + [@muzimuzhi] + +## v3.4.1 (2024-08-15) + +- [#1152](https://github.com/exercism/cli/pull/1152) Add support for Idris to `exercism test` - + [@isberg] +- [#1151](https://github.com/exercism/cli/pull/1151) Add support for Cairo to `exercism test` - [@isberg] +- [#1147](https://github.com/exercism/cli/pull/1147) Add support for Arturo to `exercism test` - [@erikschierboom] + +## v3.4.0 (2024-05-09) + +- [#1126](https://github.com/exercism/cli/pull/1126) Update `exercism test` to use Gradle wrapper to test Java exercise - [@sanderploegsma] +- [#1139](https://github.com/exercism/cli/pull/1139) Add support for Pyret to `exercism test` +- [#1136](https://github.com/exercism/cli/pull/1136) Add support for J to `exercism test` - [@enascimento178] +- [#1070](https://github.com/exercism/cli/pull/1070) `exercism open` does not require specifying the directory (defaults to current directory) - [@halfdan] +- [#1122](https://github.com/exercism/cli/pull/1122) Troubleshoot command suggest to open forum post instead of GitHub issue - [@glennj] +- [#1065](https://github.com/exercism/cli/pull/1065) Update help text for `exercism submit` to indicate specifying files is optional - [@andrerfcsantos] +- [#1140](https://github.com/exercism/cli/pull/1140) Fix release notes link + +## v3.3.0 (2024-02-15) + +- [#1128](https://github.com/exercism/cli/pull/1128) Fix `exercism test` command not working for the `8th` and `emacs-lisp` tracks - [@glennj] +- [#1125](https://github.com/exercism/cli/pull/1125) Simplify root command description +- [#1124](https://github.com/exercism/cli/pull/1124) Use correct domain for FAQ link [@tomasnorre] + +## v3.2.0 (2023-07-28) + +- [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] +- [#1073](https://github.com/exercism/cli/pull/1073) Add `arm64` build for each OS + +## v3.1.0 (2022-10-04) + +- [#979](https://github.com/exercism/cli/pull/979) Protect existing solutions from being overwritten by 'download' - [@harugo] +- [#981](https://github.com/exercism/cli/pull/981) Check if authorisation header is set before attempting to extract token - [@harugo] +- [#1044](https://github.com/exercism/cli/pull/1044) Submit without specifying files - [@andrerfcsantos] + +## v3.0.13 (2019-10-23) + +- [#866](https://github.com/exercism/cli/pull/866) The API token outputted during verbose will now be masked by default - [@Jrank2013] +- [#873](https://github.com/exercism/cli/pull/873) Make all errors in cmd package checked - [@avegner] +- [#871](https://github.com/exercism/cli/pull/871) Error message returned if the track is locked - [@Jrank2013] +- [#886](https://github.com/exercism/cli/pull/886) Added GoReleaser config, updated docs, made archive naming adjustments - [@ekingery] + +## v3.0.12 (2019-07-07) + +- [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] +- [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] +- [#806](https://github.com/exercism/cli/pull/806) Make Zsh shell tab completions work on $fpath - [@QuLogic] +- [#797](https://github.com/exercism/cli/pull/797) Fix panic when submit command is not given args - [@jdsutherland] +- [#828](https://github.com/exercism/cli/pull/828) Remove duplicate files before submitting - [@larson004] +- [#793](https://github.com/exercism/cli/pull/793) Submit handles non 2xx responses - [@jdsutherland] + +## v3.0.11 (2018-11-18) + +- [#752](https://github.com/exercism/cli/pull/752) Improve error message on upgrade command - [@farisj] +- [#759](https://github.com/exercism/cli/pull/759) Update shell tab completion for bash and zsh - [@nywilken] +- [#762](https://github.com/exercism/cli/pull/762) Improve usage documentation - [@Smarticles101] +- [#766](https://github.com/exercism/cli/pull/766) Tweak messaging to work for teams edition - [@kytrinyx] + +## v3.0.10 (2018-10-03) + +- official release of v3.0.10-alpha.1 - [@nywilken] + +## v3.0.10-alpha.1 (2018-09-21) + +- [#739](https://github.com/exercism/cli/pull/739) update maxFileSize error to include filename - [@nywilken] +- [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] +- [#738](https://github.com/exercism/cli/pull/738) Add missing contributor URLs to CHANGELOG - [@nywilken] +- [#737](https://github.com/exercism/cli/pull/737) Remove unused solutions type - [@jdsutherland] +- [#729](https://github.com/exercism/cli/pull/729) Update Oh My Zsh instructions - [@katrinleinweber] +- [#725](https://github.com/exercism/cli/pull/725) Do not allow submission of enormous files - [@sfairchild] +- [#724](https://github.com/exercism/cli/pull/724) Update submit error message when submitting a directory - [@sfairchild] +- [#723](https://github.com/exercism/cli/pull/720) Move .solution.json to hidden subdirectory - [@jdsutherland] + +## v3.0.9 (2018-08-29) + +- [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] +- [#721](https://github.com/exercism/cli/pull/721) Handle windows filepaths that accidentally got submitted to the server - [@kytrinyx] +- [#722](https://github.com/exercism/cli/pull/722) Handle exercise directories with numeric suffixes - [@kytrinyx] + +## v3.0.8 (2018-08-22) + +- [#713](https://github.com/exercism/cli/pull/713) Fix broken support for uuid flag on download command - [@nywilken] + +## v3.0.7 (2018-08-21) + +- [#705](https://github.com/exercism/cli/pull/705) Fix confusion about path and filepath - [@kytrinyx] +- [#650](https://github.com/exercism/cli/pull/650) Fix encoding problem in filenames - [@williandrade] + +## v3.0.6 (2018-07-17) + +- [#652](https://github.com/exercism/cli/pull/652) Add support for teams feature - [@kytrinyx] +- [#683](https://github.com/exercism/cli/pull/683) Fix typo in welcome message - [@glebedel] +- [#675](https://github.com/exercism/cli/pull/675) Improve output of troubleshoot command when CLI is unconfigured - [@kytrinyx] +- [#679](https://github.com/exercism/cli/pull/679) Improve error message for failed /ping on configure - [@kytrinyx] +- [#669](https://github.com/exercism/cli/pull/669) Add debug as alias for troubleshoot - [@kytrinyx] +- [#647](https://github.com/exercism/cli/pull/647) Ensure welcome message has full link to settings page - [@kytrinyx] +- [#667](https://github.com/exercism/cli/pull/667) Improve bash completion script - [@cookrn] + +## v3.0.5 (2018-07-17) + +- [#646](https://github.com/exercism/cli/pull/646) Fix issue with upgrading on Windows - [@nywilken] + +## v3.0.4 (2018-07-15) + +- [#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] + +## v3.0.3 (2018-07-14) + +- [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] +- [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] +- [#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] +- [#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] +- [#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] +- [#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] +- [#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] + +## v3.0.2 (2018-07-13) + +- [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] + +## v3.0.1 (2018-07-13) + +- [#619](https://github.com/exercism/cli/pull/619) Improve error message for successful configuration - [@kytrinyx] + +## v3.0.0 (2018-07-13) + +This is a complete rewrite from the ground up to work against the new https://exercism.org site. + +## v2.4.1 (2017-07-01) + +- [#385](https://github.com/exercism/cli/pull/385) Fix broken upgrades for Windows - [@Tonkpils] + +## v2.4.0 (2017-03-24) + +- [#344](https://github.com/exercism/cli/pull/344) Make the CLI config paths more XDG friendly - [@narqo] +- [#346](https://github.com/exercism/cli/pull/346) Fallback to UTF-8 if encoding is uncertain - [@petertseng] +- [#350](https://github.com/exercism/cli/pull/350) Add ARMv8 binaries to CLI releases - [@Tonkpils] +- [#352](https://github.com/exercism/cli/pull/352) Fix case sensitivity on slug and track ID - [@Tonkpils] +- [#353](https://github.com/exercism/cli/pull/353) Print confirmation when fetching --all - [@neslom] +- [#356](https://github.com/exercism/cli/pull/356) Resolve symlinks before attempting to read files - [@lcowell] +- [#358](https://github.com/exercism/cli/pull/358) Redact API key from debug output - [@Tonkpils] +- [#359](https://github.com/exercism/cli/pull/359) Add short flag `-m` for submit comment flag - [@jgsqware] +- [#366](https://github.com/exercism/cli/pull/366) Allow obfuscation on configure command - [@dmmulroy] +- [#367](https://github.com/exercism/cli/pull/367) Use supplied confirmation text from API on submit - [@nilbus] + +## v2.3.0 (2016-08-07) + +- [#339](https://github.com/exercism/cli/pull/339) Don't run status command if API key is missing - [@ests] +- [#336](https://github.com/exercism/cli/pull/336) Add '--all' flag to fetch command - [@neslom] +- [#333](https://github.com/exercism/cli/pull/333) Update references of codegangsta/cli -> urfave/cli - [@manusajith], [@blackerby] +- [#331](https://github.com/exercism/cli/pull/331) Improve usage/help text of submit command - [@manusajith] + +## v2.2.6 (2016-05-30) + +- [#306](https://github.com/exercism/cli/pull/306) Don't use Fatal to print usage - [@broady] +- [#307](https://github.com/exercism/cli/pull/307) Pass API key when fetching individual exercises - [@kytrinyx] +- [#312](https://github.com/exercism/cli/pull/312) Add missing newline on usage string - [@jppunnett] +- [#318](https://github.com/exercism/cli/pull/318) Show activity stream URL after submitting - [@lcowell] +- [4710640](https://github.com/exercism/cli/commit/4710640751c7a01deb1b5bf8a9a65b611b078c05) - [@lcowell] +- Update codegangsta/cli dependency - [@manusajith], [@lcowell] +- [#320](https://github.com/exercism/cli/pull/320) Add missing newlines to usage strings - [@hjljo] +- [#328](https://github.com/exercism/cli/pull/328) Append solution URL path consistently - [@Tonkpils] + +## v2.2.5 (2016-04-02) + +- [#284](https://github.com/exercism/cli/pull/284) Update release instructions - [@kytrinyx] +- [#285](https://github.com/exercism/cli/pull/285) Create a copy/pastable release text - [@kytrinyx] +- [#289](https://github.com/exercism/cli/pull/289) Fix a typo in the usage statement - [@AlexWheeler] +- [#290](https://github.com/exercism/cli/pull/290) Fix upgrade command for Linux systems - [@jbaiter] +- [#292](https://github.com/exercism/cli/pull/292) Vendor dependencies - [@Tonkpils] +- [#293](https://github.com/exercism/cli/pull/293) Remove extraneous/distracting details from README - [@Tonkpils] +- [#294](https://github.com/exercism/cli/pull/294) Improve usage statement: alphabetize commands - [@beanieboi] +- [#297](https://github.com/exercism/cli/pull/297) Improve debug output when API key is unconfigured - [@mrageh] +- [#299](https://github.com/exercism/cli/pull/299) List output uses track ID and problem from list - [@Tonkpils] +- [#301](https://github.com/exercism/cli/pull/301) Return error message for unknown track status - [@neslom] +- [#302](https://github.com/exercism/cli/pull/302) Add helpful error message when user tries to submit a directory - [@alebaffa] ## v2.2.4 (2016-01-28) -* [#270](https://github.com/exercism/cli/pull/270) Allow commenting on submission with --comment - [@Tonkpils] -* [#271](https://github.com/exercism/cli/pull/271) Increase timeout to 20 seconds - [@Tonkpils] -* [#273](https://github.com/exercism/cli/pull/273) Guard against submitting spec files and README - [@daveyarwood] -* [#278](https://github.com/exercism/cli/pull/278) Create files with 0644 mode, create missing directories for downloaded solutions - [@petertseng] -* [#281](https://github.com/exercism/cli/pull/281) Create missing directories for downloaded problems - [@petertseng] -* [#282](https://github.com/exercism/cli/pull/282) Remove random encouragement after submitting - [@kytrinyx] -* [#283](https://github.com/exercism/cli/pull/283) Print current configuration after calling configure command - [@kytrinyx] +- [#270](https://github.com/exercism/cli/pull/270) Allow commenting on submission with --comment - [@Tonkpils] +- [#271](https://github.com/exercism/cli/pull/271) Increase timeout to 20 seconds - [@Tonkpils] +- [#273](https://github.com/exercism/cli/pull/273) Guard against submitting spec files and README - [@daveyarwood] +- [#278](https://github.com/exercism/cli/pull/278) Create files with 0644 mode, create missing directories for downloaded solutions - [@petertseng] +- [#281](https://github.com/exercism/cli/pull/281) Create missing directories for downloaded problems - [@petertseng] +- [#282](https://github.com/exercism/cli/pull/282) Remove random encouragement after submitting - [@kytrinyx] +- [#283](https://github.com/exercism/cli/pull/283) Print current configuration after calling configure command - [@kytrinyx] ## v2.2.3 (2015-12-27) -* [#264](https://github.com/exercism/cli/pull/264) Fix version flag to use --version and --v - [@Tonkpils] + +- [#264](https://github.com/exercism/cli/pull/264) Fix version flag to use --version and --v - [@Tonkpils] ## v2.2.2 (2015-12-26) -* [#212](https://github.com/exercism/cli/pull/212) extract path related code from config - [@lcowell] -* [#215](https://github.com/exercism/cli/pull/215) use $XDG_CONFIG_HOME if available - [@lcowell] -* [#248](https://github.com/exercism/cli/pull/248) [#253](https://github.com/exercism/cli/pull/253) add debugging output - [@lcowell] -* [#256](https://github.com/exercism/cli/pull/256) clean up build scripts - [@lcowell] -* [#258](https://github.com/exercism/cli/pull/258) reduce filesystem noise on fetch [@devonestes] -* [#261](https://github.com/exercism/cli/pull/261) improve error message when track and exercise can't be identified on submit - [@anxiousmodernman] -* [#262](https://github.com/exercism/cli/pull/262) encourage iterating to improve after first submission on an exercise - [@eToThePiIPower] +- [#212](https://github.com/exercism/cli/pull/212) extract path related code from config - [@lcowell] +- [#215](https://github.com/exercism/cli/pull/215) use $XDG_CONFIG_HOME if available - [@lcowell] +- [#248](https://github.com/exercism/cli/pull/248) [#253](https://github.com/exercism/cli/pull/253) add debugging output - [@lcowell] +- [#256](https://github.com/exercism/cli/pull/256) clean up build scripts - [@lcowell] +- [#258](https://github.com/exercism/cli/pull/258) reduce filesystem noise on fetch [@devonestes] +- [#261](https://github.com/exercism/cli/pull/261) improve error message when track and exercise can't be identified on submit - [@anxiousmodernman] +- [#262](https://github.com/exercism/cli/pull/262) encourage iterating to improve after first submission on an exercise - [@eToThePiIPower] ## v2.2.1 (2015-08-11) -* [#200](https://github.com/exercism/cli/pull/200): Add guard to unsubmit command - [@kytrinyx] -* [#204](https://github.com/exercism/cli/pull/204): Improve upgrade failure messages and increase timeout - [@Tonkpils] -* [#206](https://github.com/exercism/cli/pull/207): Fix verbose flag and removed short `-v` - [@zabawaba99] -* [#208](https://github.com/exercism/cli/pull/208): avoid ambiguous or unresolvable exercism paths - [@lcowell] +- [#200](https://github.com/exercism/cli/pull/200): Add guard to unsubmit command - [@kytrinyx] +- [#204](https://github.com/exercism/cli/pull/204): Improve upgrade failure messages and increase timeout - [@Tonkpils] +- [#206](https://github.com/exercism/cli/pull/207): Fix verbose flag and removed short `-v` - [@zabawaba99] +- [#208](https://github.com/exercism/cli/pull/208): avoid ambiguous or unresolvable exercism paths - [@lcowell] ## v2.2.0 (2015-06-27) -* [b3c3d6f](https://github.com/exercism/cli/commit/b3c3d6fe54c622fc0ee07fdd221c8e8e5b73c8cd): Improve error message on Internal Server Error - [@Tonkpils] -* [#196](https://github.com/exercism/cli/pull/196): Add upgrade command - [@Tonkpils] -* [#194](https://github.com/exercism/cli/pull/194): Fix home expansion on configure update - [@Tonkpils] -* [523c5bd](https://github.com/exercism/cli/commit/523c5bdec5ef857f07b39de738a764589660cd5a): Document release process - [@kytrinyx] +- [b3c3d6f](https://github.com/exercism/cli/commit/b3c3d6fe54c622fc0ee07fdd221c8e8e5b73c8cd): Improve error message on Internal Server Error - [@Tonkpils] +- [#196](https://github.com/exercism/cli/pull/196): Add upgrade command - [@Tonkpils] +- [#194](https://github.com/exercism/cli/pull/194): Fix home expansion on configure update - [@Tonkpils] +- [523c5bd](https://github.com/exercism/cli/commit/523c5bdec5ef857f07b39de738a764589660cd5a): Document release process - [@kytrinyx] ## v2.1.1 (2015-05-13) -* [#192](https://github.com/exercism/cli/pull/192): Loosen up restrictions on --test flag for submissions - [@Tonkpils] -* [#190](https://github.com/exercism/cli/pull/190): Fix bug in home directory expansion for Windows - [@Tonkpils] +- [#192](https://github.com/exercism/cli/pull/192): Loosen up restrictions on --test flag for submissions - [@Tonkpils] +- [#190](https://github.com/exercism/cli/pull/190): Fix bug in home directory expansion for Windows - [@Tonkpils] ## v2.1.0 (2015-05-08) -* [1a2fd1b](https://github.com/exercism/cli/commit/1a2fd1bfb2dba358611a7c3266f935cccaf924b5): Handle config as either directory or file - [@lcowell] -* [#177](https://github.com/exercism/cli/pull/177): Improve JSON error handling and reporting - [@Tonkpils] -* [#178](https://github.com/exercism/cli/pull/178): Add support for $XDG_CONFIG_HOME - [@lcowell] -* [#184](https://github.com/exercism/cli/pull/184): Handle different file encodings in submissions - [@ambroff] -* [#179](https://github.com/exercism/cli/pull/179): Pretty print the JSON config - [@Tonkpils] -* [#181](https://github.com/exercism/cli/pull/181): Fix path issue when downloading problems - [@Tonkpils] -* [#186](https://github.com/exercism/cli/pull/186): Allow people to specify a target directory for the demo - [@Tonkpils] -* [#189](https://github.com/exercism/cli/pull/189): Implement `--test` flag to allow submitting a test file in the solution - [@pminten] +- [1a2fd1b](https://github.com/exercism/cli/commit/1a2fd1bfb2dba358611a7c3266f935cccaf924b5): Handle config as either directory or file - [@lcowell] +- [#177](https://github.com/exercism/cli/pull/177): Improve JSON error handling and reporting - [@Tonkpils] +- [#178](https://github.com/exercism/cli/pull/178): Add support for $XDG_CONFIG_HOME - [@lcowell] +- [#184](https://github.com/exercism/cli/pull/184): Handle different file encodings in submissions - [@ambroff] +- [#179](https://github.com/exercism/cli/pull/179): Pretty print the JSON config - [@Tonkpils] +- [#181](https://github.com/exercism/cli/pull/181): Fix path issue when downloading problems - [@Tonkpils] +- [#186](https://github.com/exercism/cli/pull/186): Allow people to specify a target directory for the demo - [@Tonkpils] +- [#189](https://github.com/exercism/cli/pull/189): Implement `--test` flag to allow submitting a test file in the solution - [@pminten] ## v2.0.2 (2015-04-01) -* [#174](https://github.com/exercism/cli/issues/174): Fix panic during fetch - [@kytrinyx] -* Refactor handling of ENV vars - [@lcowell] +- [#174](https://github.com/exercism/cli/issues/174): Fix panic during fetch - [@kytrinyx] +- Refactor handling of ENV vars - [@lcowell] ## v2.0.1 (2015-03-25) -* [#167](https://github.com/exercism/cli/pull/167): Fixes misspelling of exercism list command - [@queuebit] -* Tweak output from `fetch` so that languages are scannable. -* [#35](https://github.com/exercism/cli/issues/35): Add support for submitting multiple-file solutions -* [#171](https://github.com/exercism/cli/pull/171): Implement `skip` command to bypass individual exercises - [@Tonkpils] +- [#167](https://github.com/exercism/cli/pull/167): Fixes misspelling of exercism list command - [@queuebit] +- Tweak output from `fetch` so that languages are scannable. +- [#35](https://github.com/exercism/cli/issues/35): Add support for submitting multiple-file solutions +- [#171](https://github.com/exercism/cli/pull/171): Implement `skip` command to bypass individual exercises - [@Tonkpils] ## v2.0.0 (2015-03-05) Added: -* [#154](https://github.com/exercism/cli/pull/154): Add 'list' command to list available exercises for a language - [@lcowell] -* [3551884](https://github.com/exercism/cli/commit/3551884e9f38d6e563b99dae7b28a18d4525455d): Add host connectivity status to debug output. - [@lcowell] -* [#162](https://github.com/exercism/cli/pull/162): Allow users to open the browser from the terminal. - [@zabawaba99] +- [#154](https://github.com/exercism/cli/pull/154): Add 'list' command to list available exercises for a language - [@lcowell] +- [3551884](https://github.com/exercism/cli/commit/3551884e9f38d6e563b99dae7b28a18d4525455d): Add host connectivity status to debug output. - [@lcowell] +- [#162](https://github.com/exercism/cli/pull/162): Allow users to open the browser from the terminal. - [@zabawaba99] Removed: -* Stop supporting legacy config files (`~/.exercism.go`) -* Deleted deprecated login/logout commands -* Deleted deprecated key names in config +- Stop supporting legacy config files (`~/.exercism.go`) +- Deleted deprecated login/logout commands +- Deleted deprecated key names in config Fixed: -* [#151](https://github.com/exercism/cli/pull/151): Expand '~' in config path to home directory - [@lcowell] -* [#155](https://github.com/exercism/cli/pull/155): Display problems not yet submitted on fetch API - [@Tonkpils] -* [f999e69](https://github.com/exercism/cli/commit/f999e69e5290cec6c5c9933aecc6fddfad8cf019): Disambiguate debug and verbose flags. - [@lcowell] -* Report 'new' at the bottom after fetching, it's going to be more relevant than 'unchanged', which includes all the languages they don't care about. +- [#151](https://github.com/exercism/cli/pull/151): Expand '~' in config path to home directory - [@lcowell] +- [#155](https://github.com/exercism/cli/pull/155): Display problems not yet submitted on fetch API - [@Tonkpils] +- [f999e69](https://github.com/exercism/cli/commit/f999e69e5290cec6c5c9933aecc6fddfad8cf019): Disambiguate debug and verbose flags. - [@lcowell] +- Report 'new' at the bottom after fetching, it's going to be more relevant than 'unchanged', which includes all the languages they don't care about. Tweaked: -* Set environment variable in build script -* [#153](https://github.com/exercism/cli/pull/153): Refactored configuration package - [@kytrinyx] -* [#157](https://github.com/exercism/cli/pull/157): Refactored API package - [@Tonkpils] +- Set environment variable in build script +- [#153](https://github.com/exercism/cli/pull/153): Refactored configuration package - [@kytrinyx] +- [#157](https://github.com/exercism/cli/pull/157): Refactored API package - [@Tonkpils] ## v1.9.2 (2015-01-11) -* [exercism.io#2155](https://github.com/exercism/exercism.io/issues/2155): Fixed problem with passed in config file being ignored. -* Added first version of changelog +- [exercism#2155](https://github.com/exercism/exercism/issues/2155): Fixed problem with passed in config file being ignored. +- Added first version of changelog ## v1.9.1 (2015-01-10) -* [#147](https://github.com/exercism/cli/pull/147): added `--api` option to exercism configure - [@morphatic] +- [#147](https://github.com/exercism/cli/pull/147): added `--api` option to exercism configure - [@morphatic] ## v1.9.0 (2014-11-27) -* [#143](https://github.com/exercism/cli/pull/143): added command for downloading a specific solution - [@harimp] -* [#142](https://github.com/exercism/cli/pull/142): fixed command name to be `exercism` rather than `cli` on `go get` - [@Tonkpils] +- [#143](https://github.com/exercism/cli/pull/143): added command for downloading a specific solution - [@harimp] +- [#142](https://github.com/exercism/cli/pull/142): fixed command name to be `exercism` rather than `cli` on `go get` - [@Tonkpils] ## v1.8.2 (2014-10-24) -* [9cbd069](https://github.com/exercism/cli/commit/9cbd06916cc05bbb165e8c2cb00d5e03cb4dbb99): Made path comparison case insensitive +- [9cbd069](https://github.com/exercism/cli/commit/9cbd06916cc05bbb165e8c2cb00d5e03cb4dbb99): Made path comparison case insensitive ## v1.8.1 (2014-10-23) -* [0ccc7a4](https://github.com/exercism/cli/commit/0ccc7a479940d2d7bb5e12eab41c91105519f135): Implemented debug flag on submit command +- [0ccc7a4](https://github.com/exercism/cli/commit/0ccc7a479940d2d7bb5e12eab41c91105519f135): Implemented debug flag on submit command ## v1.8.0 (2014-10-15) -* [#138](https://github.com/exercism/cli/pull/138): Added conversion to line endings for submissions on Windows - [@rprouse] -* [#116](https://github.com/exercism/cli/issues/116): Added support for setting name of config file in an environment variable -* [47d6fd4](https://github.com/exercism/cli/commit/47d6fd407fd0410f5c81d60172e01e8624608f53): Added a `track` command to list the problems in a given language -* [#126](https://github.com/exercism/cli/issues/126): Added explanation in `submit` response about fetching the next problems -* [#133](https://github.com/exercism/cli/pull/133): Changed config command to create the exercism directory, rather than waiting until the first time problems are fetched - [@Tonkpils] +- [#138](https://github.com/exercism/cli/pull/138): Added conversion to line endings for submissions on Windows - [@rprouse] +- [#116](https://github.com/exercism/cli/issues/116): Added support for setting name of config file in an environment variable +- [47d6fd4](https://github.com/exercism/cli/commit/47d6fd407fd0410f5c81d60172e01e8624608f53): Added a `track` command to list the problems in a given language +- [#126](https://github.com/exercism/cli/issues/126): Added explanation in `submit` response about fetching the next problems +- [#133](https://github.com/exercism/cli/pull/133): Changed config command to create the exercism directory, rather than waiting until the first time problems are fetched - [@Tonkpils] ## v1.7.5 (2014-10-5) -* [88cf1a1fbc884545dfc10e98535f667e4a43e693](https://github.com/exercism/cli/commit/88cf1a1fbc884545dfc10e98535f667e4a43e693): Added ARMv6 to build -* [12672c4](https://github.com/exercism/cli/commit/12672c4f695cfe3891f96467619a3615e6d57c34): Added an error message when people submit a file that is not within the exercism directory tree -* [#128](https://github.com/exercism/cli/pull/128): Made paths os-agnostic in tests - [@ccnp123] +- [88cf1a1fbc884545dfc10e98535f667e4a43e693](https://github.com/exercism/cli/commit/88cf1a1fbc884545dfc10e98535f667e4a43e693): Added ARMv6 to build +- [12672c4](https://github.com/exercism/cli/commit/12672c4f695cfe3891f96467619a3615e6d57c34): Added an error message when people submit a file that is not within the exercism directory tree +- [#128](https://github.com/exercism/cli/pull/128): Made paths os-agnostic in tests - [@ccnp123] ## v1.7.4 (2014-09-27) -* [4ca3e97](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Fixed incorrect HOME directory on Windows -* [8bd1a25](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Added ARMv5 to build -* [#117](https://github.com/exercism/cli/pull/117): Archive windows binaries using zip rather than tar and gzip - [@LegalizeAdulthood] +- [4ca3e97](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Fixed incorrect HOME directory on Windows +- [8bd1a25](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Added ARMv5 to build +- [#117](https://github.com/exercism/cli/pull/117): Archive windows binaries using zip rather than tar and gzip - [@LegalizeAdulthood] ## v1.7.3 (2014-09-26) -* [8bec393](https://github.com/exercism/cli/commit/8bec39387094680990af7cf438ada1780cf87129): Fixed submit so it can handle symlinks +- [8bec393](https://github.com/exercism/cli/commit/8bec39387094680990af7cf438ada1780cf87129): Fixed submit so it can handle symlinks ## v1.7.2 (2014-09-24) -* [#111](https://github.com/exercism/cli/pull/111): Don't clobber existing config values when adding more - [@jish] +- [#111](https://github.com/exercism/cli/pull/111): Don't clobber existing config values when adding more - [@jish] ## v1.7.1 (2014-09-19) -* Completely reorganized the code, separating each command into a separate handler -* [17fc164](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Fixed paths to be platform-independent -* [8b174e2](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Made the output of demo command more helpful -* [8b174e2](https://github.com/exercism/cli/commit/8b174e2fd8c7a545ea5c47c998ac10c5a7ab371f): Deleted the 'current' command +- Completely reorganized the code, separating each command into a separate handler +- [17fc164](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Fixed paths to be platform-independent +- [8b174e2](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Made the output of demo command more helpful +- [8b174e2](https://github.com/exercism/cli/commit/8b174e2fd8c7a545ea5c47c998ac10c5a7ab371f): Deleted the 'current' command ## v1.7.0 (2014-08-28) -* [ac6dbfd](https://github.com/exercism/cli/commit/ac6dbfd81a86e7a9a5a9b68521b0226c40d8e813): Added os and architecture to the user agent -* [5d58fd1](https://github.com/exercism/cli/commit/5d58fd14b9db84fb752b3bf6112123cd6f04c532): Fixed bug in detecting user's home directory -* [#100](https://github.com/exercism/cli/pull/100): Added 'debug' command, which supersedes the 'info' command - [@Tonkpils] -* Extracted a couple of commands into separate handlers -* [6ec5876](https://github.com/exercism/cli/commit/6ec5876bde0b02206cacbe685bb8aedcbdba25d4): Added a hack to rename old config files to the new default name -* [bb7d0d6](https://github.com/exercism/cli/commit/bb7d0d6151a950c92590dc771ec3ff5fdd1c83b0): Rename 'home' command to 'info' -* [#95](https://github.com/exercism/cli/issues/95): Added 'home' command -* Deprecate login/logout commands -* [1a39134](https://github.com/exercism/cli/commit/1a391342da93aa32ae398f1500a3981aa65b9f41): Changed demo to write exercises to the default exercism problems directory -* [07cc334](https://github.com/exercism/cli/commit/07cc334739465b21d6eb5d973e16e1c88f67758e): Deleted the whoami command, we weren't using github usernames for anything -* [#97](https://github.com/exercism/cli/pull/97): Changed default exercism directory to ~/exercism - [@lcowell] -* [#94](https://github.com/exercism/cli/pull/94): Updated language detection to handle C++ - [@LegalizeAdulthood] -* [#92](https://github.com/exercism/cli/pull/92): Renamed config json file to .exercism.json instead of .exercism.go - [@lcowell] -* [f55653f](https://github.com/exercism/cli/commit/f55653f35863914086a54375afb0898e142c1638): Deleted go vet from travis build temporarily until the codebase can be cleaned up -* [#91](https://github.com/exercism/cli/pull/91): Replaced temp file usage with encode/decode - [@lcowell] -* [#90](https://github.com/exercism/cli/pull/90): Added sanitization to config values to trim whitespace before writing it - [@lcowell] -* Did a fair amount of cleanup to make code a bit more idiomatic -* [#86](https://github.com/exercism/cli/pull/86): Triggered interactive login command for commands that require auth - [@Tonkpils] +- [ac6dbfd](https://github.com/exercism/cli/commit/ac6dbfd81a86e7a9a5a9b68521b0226c40d8e813): Added os and architecture to the user agent +- [5d58fd1](https://github.com/exercism/cli/commit/5d58fd14b9db84fb752b3bf6112123cd6f04c532): Fixed bug in detecting user's home directory +- [#100](https://github.com/exercism/cli/pull/100): Added 'debug' command, which supersedes the 'info' command - [@Tonkpils] +- Extracted a couple of commands into separate handlers +- [6ec5876](https://github.com/exercism/cli/commit/6ec5876bde0b02206cacbe685bb8aedcbdba25d4): Added a hack to rename old config files to the new default name +- [bb7d0d6](https://github.com/exercism/cli/commit/bb7d0d6151a950c92590dc771ec3ff5fdd1c83b0): Rename 'home' command to 'info' +- [#95](https://github.com/exercism/cli/issues/95): Added 'home' command +- Deprecate login/logout commands +- [1a39134](https://github.com/exercism/cli/commit/1a391342da93aa32ae398f1500a3981aa65b9f41): Changed demo to write exercises to the default exercism problems directory +- [07cc334](https://github.com/exercism/cli/commit/07cc334739465b21d6eb5d973e16e1c88f67758e): Deleted the whoami command, we weren't using github usernames for anything +- [#97](https://github.com/exercism/cli/pull/97): Changed default exercism directory to ~/exercism - [@lcowell] +- [#94](https://github.com/exercism/cli/pull/94): Updated language detection to handle C++ - [@LegalizeAdulthood] +- [#92](https://github.com/exercism/cli/pull/92): Renamed config json file to .exercism.json instead of .exercism.go - [@lcowell] +- [f55653f](https://github.com/exercism/cli/commit/f55653f35863914086a54375afb0898e142c1638): Deleted go vet from travis build temporarily until the codebase can be cleaned up +- [#91](https://github.com/exercism/cli/pull/91): Replaced temp file usage with encode/decode - [@lcowell] +- [#90](https://github.com/exercism/cli/pull/90): Added sanitization to config values to trim whitespace before writing it - [@lcowell] +- Did a fair amount of cleanup to make code a bit more idiomatic +- [#86](https://github.com/exercism/cli/pull/86): Triggered interactive login command for commands that require auth - [@Tonkpils] ## v1.6.2 (2014-06-02) -* [a5b7a55](https://github.com/exercism/cli/commit/a5b7a55f52c23ac5ce2c6bd1826ea7767aea38c4): Update login prompt +- [a5b7a55](https://github.com/exercism/cli/commit/a5b7a55f52c23ac5ce2c6bd1826ea7767aea38c4): Update login prompt ## v1.6.1 (2014-05-16) -* [#84](https://github.com/exercism/cli/pull/84): Change hard-coded filepath so that it will work on any platform - [@simonjefford] +- [#84](https://github.com/exercism/cli/pull/84): Change hard-coded filepath so that it will work on any platform - [@simonjefford] ## v1.6.0 (2014-05-10) -* [#82](https://github.com/exercism/cli/pull/82): Fixed typo in tests - [@srt32] -* [aa7446d](https://github.com/exercism/cli/commit/aa7446d598fc894ef329756555c48ef358baf676): Clarified output to user after they fetch -* [#79](https://github.com/exercism/cli/pull/79): Updated development instructions to fix permissions problem - [@andrewsardone] -* [#78](https://github.com/exercism/cli/pull/78): Deleted deprecated action `peek` - [@djquan] -* [#74](https://github.com/exercism/cli/pull/74): Implemented new option on `fetch` to get a single language - [@Tonkpils] -* [#75](https://github.com/exercism/cli/pull/75): Improved feedback to user after logging in - [@Tonkpils] -* [#72](https://github.com/exercism/cli/pull/72): Optimized use of temp file - [@Dparker1990] -* [#70](https://github.com/exercism/cli/pull/70): Fixed a panic - [@Tonkpils] -* [#68](https://github.com/exercism/cli/pull/68): Fixed how user input is read so that it doesn't stop at the first space - [@Tonkpils] +- [#82](https://github.com/exercism/cli/pull/82): Fixed typo in tests - [@srt32] +- [aa7446d](https://github.com/exercism/cli/commit/aa7446d598fc894ef329756555c48ef358baf676): Clarified output to user after they fetch +- [#79](https://github.com/exercism/cli/pull/79): Updated development instructions to fix permissions problem - [@andrewsardone] +- [#78](https://github.com/exercism/cli/pull/78): Deleted deprecated action `peek` - [@djquan] +- [#74](https://github.com/exercism/cli/pull/74): Implemented new option on `fetch` to get a single language - [@Tonkpils] +- [#75](https://github.com/exercism/cli/pull/75): Improved feedback to user after logging in - [@Tonkpils] +- [#72](https://github.com/exercism/cli/pull/72): Optimized use of temp file - [@Dparker1990] +- [#70](https://github.com/exercism/cli/pull/70): Fixed a panic - [@Tonkpils] +- [#68](https://github.com/exercism/cli/pull/68): Fixed how user input is read so that it doesn't stop at the first space - [@Tonkpils] ## v1.5.1 (2014-03-14) -* [5b672ee](https://github.com/exercism/cli/commit/5b672ee7bf26859c41de9eed83396b7454286063 ): Provided a visual mark next to new problems that get fetched +- [5b672ee](https://github.com/exercism/cli/commit/5b672ee7bf26859c41de9eed83396b7454286063): Provided a visual mark next to new problems that get fetched ## v1.5.0 (2014-02-28) -* [#63](https://github.com/exercism/cli/pull/63): Implemeted `fetch` for a single language - [@Tonkpils] -* [#62](https://github.com/exercism/cli/pull/62): Expose error message from API to user on `fetch` - [@Tonkpils] -* [#59](https://github.com/exercism/cli/pull/59): Added global flag to pass the path to the config file instead of relying on default - [@isbadawi] -* [#57](https://github.com/exercism/cli/pull/57): Added description to the restore command - [@rcode5] -* [#56](https://github.com/exercism/cli/pull/56): Updated developer instructions in README based on real-life experience - [@rcode5] +- [#63](https://github.com/exercism/cli/pull/63): Implemeted `fetch` for a single language - [@Tonkpils] +- [#62](https://github.com/exercism/cli/pull/62): Expose error message from API to user on `fetch` - [@Tonkpils] +- [#59](https://github.com/exercism/cli/pull/59): Added global flag to pass the path to the config file instead of relying on default - [@isbadawi] +- [#57](https://github.com/exercism/cli/pull/57): Added description to the restore command - [@rcode5] +- [#56](https://github.com/exercism/cli/pull/56): Updated developer instructions in README based on real-life experience - [@rcode5] ## v1.4.0 (2014-01-13) -* [#47](https://github.com/exercism/cli/pull/47): Added 'restore' command to download all of a user's existing solutions with their corresponding problems - [@ebautistabar] -* Numerous small fixes and cleanup to code and documentation - [@dpritchett], [@TrevorBramble], [@elimisteve] +- [#47](https://github.com/exercism/cli/pull/47): Added 'restore' command to download all of a user's existing solutions with their corresponding problems - [@ebautistabar] +- Numerous small fixes and cleanup to code and documentation - [@dpritchett], [@TrevorBramble], [@elimisteve] ## v1.3.2 (2013-12-14) -* [f8dd974](https://github.com/exercism/cli/commit/f8dd9748078b1b191629eae385aaeda8af94305b): Fixed content-type header when posting to API -* Fixed user-agent string +- [f8dd974](https://github.com/exercism/cli/commit/f8dd9748078b1b191629eae385aaeda8af94305b): Fixed content-type header when posting to API +- Fixed user-agent string ## v1.3.1 (2013-12-01) -* [exercism.io#1039](https://github.com/exercism/exercism.io/issues/1039): Stopped clobbering existing files on fetch +- [exercism#1039](https://github.com/exercism/exercism/issues/1039): Stopped clobbering existing files on fetch ## v1.3.0 (2013-11-16) -* [7f39ee4](https://github.com/exercism/cli/commit/7f39ee4802752925466bc2715790dc965026b09d): Allow users to specify a particular problem when fetching. +- [7f39ee4](https://github.com/exercism/cli/commit/7f39ee4802752925466bc2715790dc965026b09d): Allow users to specify a particular problem when fetching. ## v1.2.3 (2013-11-13) -* [exercism.io#998](https://github.com/exercism/exercism.io/issues/998): Fix problem with writing an empty config file under certain circumstances. +- [exercism#998](https://github.com/exercism/exercism/issues/998): Fix problem with writing an empty config file under certain circumstances. ## v1.2.2 (2013-11-12) -* [#28](https://github.com/exercism/cli/issues/28): Create exercism directory immediately upon logging in. -* Upgrade to newer version of [codegangsta/cli](https://github.com/codegansta/cli) library, which returns an error from the main Run() function. +- [#28](https://github.com/exercism/cli/issues/28): Create exercism directory immediately upon logging in. +- Upgrade to newer version of [codegangsta/cli](https://github.com/codegansta/cli) library, which returns an error from the main Run() function. ## v1.2.1 (2013-11-09) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add support for nested directories under the language track directory allowing us to create idiomatic scala, clojure, and other exercises. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add support for nested directories under the language track directory allowing us to create idiomatic scala, clojure, and other exercises. ## v1.2.0 (2013-11-07) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Consume the new hash of filename => content that the problem API returns. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Consume the new hash of filename => content that the problem API returns. ## v1.1.1 (2013-10-20) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add output when fetching to tell the user where the files where created. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add output when fetching to tell the user where the files where created. ## v1.1.0 (2013-10-24) -* Refactor to extract config package -* Delete stray binary **TODO** we might rewrite history on this one, see [#102](https://github.com/exercism/xgo/issues/102). -* [#22](https://github.com/exercism/cli/pull/22): Display submission url after submitting solution - [@Tonkpils] -* [#21](https://github.com/exercism/cli/pull/21): Add unsubmit command - [@Tonkpils] -* [#20](https://github.com/exercism/cli/pull/20): Add current command - [@Tonkpils] -* Inline refactoring experiment, various cleanup +- Refactor to extract config package +- Delete stray binary **TODO** we might rewrite history on this one, see [#102](https://github.com/exercism/xgo/issues/102). +- [#22](https://github.com/exercism/cli/pull/22): Display submission url after submitting solution - [@Tonkpils] +- [#21](https://github.com/exercism/cli/pull/21): Add unsubmit command - [@Tonkpils] +- [#20](https://github.com/exercism/cli/pull/20): Add current command - [@Tonkpils] +- Inline refactoring experiment, various cleanup ## v1.0.1 (2013-09-27) -* [#11](https://github.com/exercism/cli/pull/11): Don't require authentication for demo - [@msgehard] -* [#14](https://github.com/exercism/cli/pull/14): Print out fetched assignments - [@Tonkpils] -* [#16](https://github.com/exercism/cli/pull/16): Fix broken submit for relative path names - [@nf] -* Create a separate demo directory if there's no configured exercism directory +- [#11](https://github.com/exercism/cli/pull/11): Don't require authentication for demo - [@msgehard] +- [#14](https://github.com/exercism/cli/pull/14): Print out fetched assignments - [@Tonkpils] +- [#16](https://github.com/exercism/cli/pull/16): Fix broken submit for relative path names - [@nf] +- Create a separate demo directory if there's no configured exercism directory ## v1.0.0 (2013-09-22) -* [#7](https://github.com/exercism/cli/pull/7): Recognize haskell test files -* [#5](https://github.com/exercism/cli/pull/5): Fix typo - [@simonjefford] -* [#1](https://github.com/exercism/cli/pull/1): Output the location of the config file - [@msgehard] -* Recognize more language test files - [@msgehard] +- [#7](https://github.com/exercism/cli/pull/7): Recognize haskell test files +- [#5](https://github.com/exercism/cli/pull/5): Fix typo - [@simonjefford] +- [#1](https://github.com/exercism/cli/pull/1): Output the location of the config file - [@msgehard] +- Recognize more language test files - [@msgehard] ## v0.0.27.beta (2013-08-25) All changes by [@msgehard] -* Clean up homedir -* Add dev instructions to README +- Clean up homedir +- Add dev instructions to README ## v0.0.26.beta (2013-08-24) All changes by [@msgehard] -* Ensure that ruby gem's config file doesn't get clobbered -* Add cross-compilation -* Set proper User-Agent so server doesn't blow up. -* Implement `submit` -* Implement `demo` -* Implement `peek` -* Expand ~ in config -* Implement `fetch` -* Implement `current` -* Implement `whoami` -* Implement login and logout -* Build on Travis - +- Ensure that ruby gem's config file doesn't get clobbered +- Add cross-compilation +- Set proper User-Agent so server doesn't blow up. +- Implement `submit` +- Implement `demo` +- Implement `peek` +- Expand ~ in config +- Implement `fetch` +- Implement `current` +- Implement `whoami` +- Implement login and logout +- Build on Travis + +[@alexwheeler]: https://github.com/AlexWheeler +[@andrerfcsantos]: https://github.com/andrerfcsantos +[@avegner]: https://github.com/avegner +[@dparker1990]: https://github.com/Dparker1990 +[@john-goff]: https://github.com/John-Goff +[@legalizeadulthood]: https://github.com/LegalizeAdulthood +[@qulogic]: https://github.com/QuLogic +[@smarticles101]: https://github.com/Smarticles101 +[@tonkpils]: https://github.com/Tonkpils +[@trevorbramble]: https://github.com/TrevorBramble +[@alebaffa]: https://github.com/alebaffa [@ambroff]: https://github.com/ambroff [@andrewsardone]: https://github.com/andrewsardone [@anxiousmodernman]: https://github.com/anxiousmodernman +[@beanieboi]: https://github.com/beanieboi +[@blackerby]: https://github.com/blackerby +[@broady]: https://github.com/broady [@ccnp123]: https://github.com/ccnp123 -[@Dparker1990]: https://github.com/Dparker1990 +[@cookrn]: https://github.com/cookrn [@daveyarwood]: https://github.com/daveyarwood [@devonestes]: https://github.com/devonestes [@djquan]: https://github.com/djquan +[@dmmulroy]: https://github.com/dmmulroy [@dpritchett]: https://github.com/dpritchett -[@eToThePiIPower]: https://github.com/eToThePiIPower +[@etothepiipower]: https://github.com/eToThePiIPower [@ebautistabar]: https://github.com/ebautistabar +[@ekingery]: https://github.com/ekingery [@elimisteve]: https://github.com/elimisteve +[@erikschierboom]: https://github.com/erikschierboom +[@ests]: https://github.com/ests +[@farisj]: https://github.com/farisj +[@glebedel]: https://github.com/glebedel [@harimp]: https://github.com/harimp +[@harugo]: https://github.com/harugo +[@hjljo]: https://github.com/hjljo +[@isaacg]: https://github.com/isaacg [@isbadawi]: https://github.com/isbadawi +[@jbaiter]: https://github.com/jbaiter +[@jdsutherland]: https://github.com/jdsutherland +[@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish +[@jrank2013]: https://github.com/Jrank2013 +[@jppunnett]: https://github.com/jppunnett +[@katrinleinweber]: https://github.com/katrinleinweber [@kytrinyx]: https://github.com/kytrinyx -[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood +[@larson004]: https://github.com/larson004 [@lcowell]: https://github.com/lcowell -[@neslom]: https://github.com/neslom -[@msgehard]: https://github.com/msgehard +[@manusajith]: https://github.com/manusajith [@morphatic]: https://github.com/morphatic +[@mrageh]: https://github.com/mrageh +[@msgehard]: https://github.com/msgehard +[@narqo]: https://github.com/narqo +[@neslom]: https://github.com/neslom [@nf]: https://github.com/nf +[@nilbus]: https://github.com/nilbus +[@nywilken]: https://github.com/nywilken [@petertseng]: https://github.com/petertseng [@pminten]: https://github.com/pminten [@queuebit]: https://github.com/queuebit [@rcode5]: https://github.com/rcode5 [@rprouse]: https://github.com/rprouse +[@sfairchild]: https://github.com/sfairchild [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 -[@Tonkpils]: https://github.com/Tonkpils -[@TrevorBramble]: https://github.com/TrevorBramble +[@xavdid]: https://github.com/xavdid +[@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 +[@groophylifefor]: https://github.com/GroophyLifefor +[@muzimuzhi]: https://github.com/muzimuzhi +[@isberg]: https://github.com/isberg +[@erikschierboom]: https://github.com/erikschierboom +[@sanderploegsma]: https://github.com/sanderploegsma +[@enascimento178]: https://github.com/enascimento178 +[@halfdan]: https://github.com/halfdan +[@glennj]: https://github.com/glennj +[@tomasnorre]: https://github.com/tomasnorre +[@ageron]: https://github.com/ageron +[@petrem]: https://github.com/petrem +[@bnandras]: https://github.com/bnandras +[@vaeng]: https://github.com/vaeng +[@keiravillekode]: https://github.com/keiravillekode diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..3f7813de1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,93 @@ +# Code of Conduct + +## Introduction + +Exercism is a platform centered around empathetic conversation. +We have a low tolerance for communication that makes anyone feel unwelcome, unsupported, insulted or discriminated against. + +## Seen or experienced something uncomfortable? + +If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email [abuse@exercism.org](mailto:abuse@exercism.org?subject=%5BCoC%5D) and include \[CoC\] in the subject line. +We will follow up with you as a priority. + +## Enforcement + +We actively monitor for Code of Conduct (CoC) violations and take any reports of violations extremely seriously. +We have banned contributors, mentors and users due to violations. + +After we receive a report of a CoC violation, we view that person's conversation history on Exercism and related communication channels and attempt to understand whether someone has deliberately broken the CoC, or accidentally crossed a line. +We generally reach out to the person who has been reported to discuss any concerns we have and warn them that repeated violations will result in a ban. +Sometimes we decide that no violation has occurred and that no action is required and sometimes we will also ban people on a first offense. +We strive to be fair, but will err on the side of protecting the culture of our community. + +Exercism's leadership reserve the right to take whatever action they feel appropriate with regards to CoC violations. + +## The simple version + +- Be empathetic +- Be welcoming +- Be kind +- Be honest +- Be supportive +- Be polite + +## The details + +Exercism should be a safe place for everybody regardless of + +- Gender, gender identity or gender expression +- Sexual orientation +- Disability +- Physical appearance (including but not limited to body size) +- Race +- Age +- Religion +- Anything else you can think of + +As someone who is part of this community, you agree that: + +- We are collectively and individually committed to safety and inclusivity +- We have zero tolerance for abuse, harassment, or discrimination +- We respect people’s boundaries and identities +- We refrain from using language that can be considered offensive or oppressive (systemically or otherwise), eg. sexist, racist, homophobic, transphobic, ableist, classist, etc. + - this includes (but is not limited to) various slurs. +- We avoid using offensive topics as a form of humor + +We actively work towards: + +- Being a safe community +- Cultivating a network of support & encouragement for each other +- Encouraging responsible and varied forms of expression + +We condemn: + +- Stalking, doxxing, or publishing private information +- Violence, threats of violence or violent language +- Anything that compromises people’s safety +- Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature +- The use of unwelcome, suggestive, derogatory or inappropriate nicknames or terms +- Disrespect towards others (jokes, innuendo, dismissive attitudes) and towards differences of opinion +- Intimidation or harassment (online or in-person). + Please read the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md) for how we interpret harassment +- Inappropriate attention or contact +- Not understanding the differences between constructive criticism and disparagement + +These things are NOT OK. + +Be aware of how your actions affect others. +If it makes someone uncomfortable, stop. + +If you say something that is found offensive, and you are called out on it, try to: + +- Listen without interruption +- Believe what the person is saying & do not attempt to disqualify what they have to say +- Ask for tips / help with avoiding making the offense in the future +- Apologize and ask forgiveness + +## History + +This policy was initially adopted from the Front-end London Slack community and has been modified since. +A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md). + +_This policy is a "living" document, and subject to refinement and expansion in the future. +This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Discord, Forum, Twitter, email) and any other Exercism entity or event._ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1515a4fcb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing Guide + +First, thank you! :tada: +Exercism would be impossible without people like you being willing to spend time and effort making things better. + +## Documentation +* [Exercism Documentation Repository](https://github.com/exercism/docs) + +## Dependencies + +You'll need Go version 1.20 or higher. Follow the directions on http://golang.org/doc/install + +## Development + +This project uses Go's [`modules` dependency management](https://github.com/golang/go/wiki/Modules) system. + +To contribute [fork this repo on the GitHub webpage][fork] and clone your fork. +Make your desired changes and submit a pull request. +Please provide tests for the changes where possible. + +Please note that if your development directory is located inside the `GOPATH`, you need to set the `GO111MODULE=on` environment variable. + +## Running the Tests + +To run the tests locally + +``` +go test ./... +``` + +## Manual Testing against Exercism + +To test your changes while doing everyday Exercism work you +can build using the following instructions. Any name may be used for the +binary (e.g. `testercism`) - by using a name other than `exercism` you +can have different profiles under `~/.config` and avoid possibly +damaging your real Exercism submissions, or test different tokens, etc. + +On Unices: + +- `cd /path/to/the/development/directory/cli && go build -o testercism ./exercism/main.go` +- `./testercism -h` + +On Windows: + +- `cd /d \path\to\the\development\directory\cli` +- `go build -o testercism.exe exercism\main.go` +- `testercism.exe —h` + +### Releasing a new CLI version +Consult the [release documentation](RELEASE.md). diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json deleted file mode 100644 index ee998ecfc..000000000 --- a/Godeps/Godeps.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "ImportPath": "github.com/exercism/cli", - "GoVersion": "go1.6", - "Packages": [ - "./..." - ], - "Deps": [ - { - "ImportPath": "github.com/codegangsta/cli", - "Comment": "1.2.0-187-gc31a797", - "Rev": "c31a7975863e7810c92e2e288a9ab074f9a88f29" - }, - { - "ImportPath": "github.com/kardianos/osext", - "Rev": "29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc" - }, - { - "ImportPath": "github.com/stretchr/testify/assert", - "Comment": "v1.0-51-gf3960ab", - "Rev": "f3960ab1f9664ecc4e27c78af27cc9063d745a43" - }, - { - "ImportPath": "golang.org/x/net/html", - "Rev": "5d0a0f8cd486626821d2ba44d471ab1c9271d38f" - }, - { - "ImportPath": "golang.org/x/text/encoding", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - }, - { - "ImportPath": "golang.org/x/text/internal/tag", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - }, - { - "ImportPath": "golang.org/x/text/internal/utf8internal", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - }, - { - "ImportPath": "golang.org/x/text/language", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - }, - { - "ImportPath": "golang.org/x/text/runes", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - }, - { - "ImportPath": "golang.org/x/text/transform", - "Rev": "cf4986612c83df6c55578ba198316d1684a9a287" - } - ] -} diff --git a/Godeps/Readme b/Godeps/Readme deleted file mode 100644 index 4cdaa53d5..000000000 --- a/Godeps/Readme +++ /dev/null @@ -1,5 +0,0 @@ -This directory tree is generated automatically by godep. - -Please do not edit. - -See https://github.com/tools/godep for more information. diff --git a/LICENSE b/LICENSE index 449be2455..986629c33 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Mike Gehard, Katrina Owen +Copyright (c) 2013 Mike Gehard, Exercism Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index afd797448..8b0fa92c6 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,19 @@ -[![Build Status](https://travis-ci.org/exercism/cli.png?branch=master)](https://travis-ci.org/exercism/cli) -[![Supporting 24 Pull Requests](https://img.shields.io/badge/Supporting-24%20Pull%20Requests-red.svg?style=flat)](http://24pullrequests.com) +# Exercism Command-line Interface (CLI) -# Exercism Command-Line Client +[![CI](https://github.com/exercism/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/exercism/cli/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/exercism/cli)](https://goreportcard.com/report/github.com/exercism/cli) -The CLI provides a way to do the problems on -[exercism.io](http://exercism.io). +The CLI is the link between the [Exercism][exercism] website and your local work environment. It lets you download exercises and submit your solution to the site. -**Important**: If you're looking for instructions on how to install the CLI. Please read [Installing the CLI](http://exercism.io/cli) +This CLI ships as a binary with no additional runtime requirements. -This CLI ships as a binary with no additional runtime requirements. This means -that if you're doing the Haskell problems on exercism you don't need a working -Python or Ruby environment simply to fetch and submit exercises. +## Installing the CLI -## Dependencies +Instructions can be found at [exercism/cli/releases](https://github.com/exercism/cli/releases) -Go version 1.6 or higher - -## Installing Go - -Follow the directions on http://golang.org/doc/install - -## Development - -1. fork this repo -1. `go get github.com/exercism/cli/exercism` -1. `cd $GOPATH/src/github.com/exercism/cli` -1. `git remote set-url origin https://github.com//cli` -1. `go get -t ./...` -1. Make the change. -1. Submit a pull request. - -Please provide tests for the changes where possible. - -At the moment the CLI commands are not tested, so if you're adding a new -command don't worry too hard about tests. - -## Building - -To build the binary for your platform run - -``` -go install github.com/exercism/cli/exercism -``` - -or - -``` -go build -o out/exercism exercism/main.go -``` - -The resulting binary can be found in `out/exercism` (Linux, Mac OS X) or `out/exercism.exe` (Windows). - -In order to cross-compile for all platforms, run `bin/build-all`. The binaries -will be built into the `release` directory. - -## Domain Concepts - -- **Language** is the name of a programming language. E.g. C++ or Objective-C or JavaScript. -- **Track ID** is a normalized, url-safe identifier for a language track. E.g. `cpp` or `objective-c` or `javascript`. -- **Problem** is an exercism exercise. -- **Problem Slug** is a normalized, url-safe identifier for a problem. -- **Iteration** is a solution that a user has written for a particular problem in a particular language track. A user may have several iterations for the same problem. +## Contributing +If you wish to help improve the CLI, please see the [Contributing guide][contributing]. +[exercism]: http://exercism.org +[contributing]: /CONTRIBUTING.md diff --git a/RELEASE.md b/RELEASE.md index 2453980f0..17182f601 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,119 +1,54 @@ # Cutting a CLI Release -## Bootstrap Cross-Compilation for Go - -**This only has to be done once.** - -Change directory to the go source. Then run the bootstrap command for -each operating system and architecture. - -```plain -$ cd `which go`/../../src -$ sudo GCO_ENABLED=0 GOOS=windows GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=darwin GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=windows GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=darwin GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 ./make.bash --no-clean -``` +The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the release process. -## Update the Changelog +## Requirements -Make sure all the recent changes are reflected in the "next release" section -of the Changelog. Make this a separate commit from bumping the version. +1. [Install GoReleaser](https://goreleaser.com/install/) +1. [Setup GitHub token](https://goreleaser.com/scm/github/) +1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/customization/sign/) ## Bump the version -Edit the `Version` constant in `exercism/main.go`, and edit the Changelog. - -All the changes in the "next release" section should be moved to a new section -that describes the version number, and gives it a date. +1. Create a branch for the new version +1. Bump the `Version` constant in `cmd/version.go` +1. Update the `CHANGELOG.md` file to include a section for the new version and its changes. + Hint: you can view changes using the compare view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main. +1. Commit the updated files +1. Create a PR -The "next release" section should contain only "Your contribution here". +_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v3.5.9`._ -_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`. +## Cut a release -## Generate the Binaries +Once the version bump PR has been merged, run the following command to cut a release: -```plain -$ rm release/* -$ CGO_ENABLED=0 bin/build-all +```shell +GPG_FINGERPRINT="" ./bin/release.sh ``` ## Cut Release on GitHub -Go to [the exercism/cli "new release" page](https://github.com/exercism/cli/releases/new). - -Describe the release, select a specific commit to target, name the version `v{VERSION}`, where -VERSION matches the value of the `Version` constant. - -Upload all the binaries from `release/*`. - -Paste the release text and describe the new changes: - -``` -### Exercism Command-Line Interface (CLI) - -Exercism takes place in two places: the discussions happen on the website, and you work on exercises locally. The CLI bridges the gap, allowing you to fetch exercises and submit solutions to the site. - -This is a stand-alone binary, which means that you don't need to install any particular language or environment in order to use it. - -To install, download the archive that matches your operating system and architecture, unpack the archive, and put the binary somewhere on your path. - -You will need to configure the CLI with your [Exercism API Key](http://exercism.io/account/key) before submitting. - -For more detailed instructions, see the [CLI page on Exercism](http://exercism.io/cli). - -#### Recent Changes +Once the `./bin/release.sh` command finishes, the [release workflow](https://github.com/exercism/cli/actions/workflows/release.yml) will automatically run. +This workflow will create a draft release at https://github.com/exercism/cli/releases/tag/vX.Y.Z. +Once created, go that page to update the release description to: -* ABC... -* XYZ... ``` +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough +--- -## Update Homebrew - -This is helpful for the (many) Mac OS X users. - -First, get a copy of the latest tarball of the source code: - -``` -cd ~/tmp && wget https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz -``` - -Get the SHA256 of the tarball: - -``` -shasum -a 256 vX.Y.Z.tar.gz -``` - -Update the formula: - -``` -cd $(brew --repository) -git checkout master -git remote add YOUR_USERNAME git@github.com:YOUR_USERNAME/homebrew.git -brew update -git checkout -b exercism-vX.Y.Z -brew edit exercism -# update sha256 and tarball url -brew audit exercism -brew install exercism -brew test exercism -git commit -m "exercism X.Y.Z" -git push --set-upstream YOUR_USERNAME exercism-vX.Y.Z +[modify the generated release-notes to describe changes in this release] ``` -Then go to https://github.com/Homebrew/homebrew and create pull request. +Lastly, test and then publish the draft. -Note that they really don't want any verbose commit messages or PR descriptions when all you're doing is bumping a version. +## Homebrew -For more information see their [contribution guidelines](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/How-To-Open-a-Homebrew-Pull-Request-(and-get-it-merged).md#how-to-open-a-homebrew-pull-request-and-get-it-merged). +Homebrew will automatically bump the version, no manual action is required. -## Update the Docs Site +## Update the docs site If there are any significant changes, we should describe them on -[cli.exercism.io](http://cli.exercism.io/). +[exercism.org/cli](https://exercism.org/cli). -The codebase lives at [exercism/cli-www](https://github.com/exercism/cli-www). +The codebase lives at [exercism/website-copy](https://github.com/exercism/website-copy) in `pages/cli.md`. diff --git a/api/api.go b/api/api.go index d4ac1595c..25b5e9d14 100644 --- a/api/api.go +++ b/api/api.go @@ -2,282 +2,50 @@ package api import ( "bytes" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" -) + "os" -var ( - // ErrUnknownTrack represents an error returned when the track requested does not exist. - ErrUnknownTrack = errors.New("no such track") + "golang.org/x/net/html/charset" + "golang.org/x/text/transform" ) -// PayloadError represents an error message from the API. -type PayloadError struct { - Error string `json:"error"` -} - -// PayloadProblems represents a response containing problems. -type PayloadProblems struct { - Problems []*Problem - PayloadError -} - -// PayloadSubmission represents metadata about a successful submission. -type PayloadSubmission struct { - *Submission - PayloadError -} - -// SubmissionInfo contains state information about a submission. -type SubmissionInfo struct { - Slug string `json:"slug"` - State string `json:"state"` -} - -// Fetch retrieves problems from the API. -// Most problems consist of a README, some sort of test suite, and -// any supporting files (header files, test data, boilerplate, skeleton -// files, etc). -func (c *Client) Fetch(args []string) ([]*Problem, error) { - var url string - switch len(args) { - case 0: - url = fmt.Sprintf("%s/v2/exercises?key=%s", c.XAPIHost, c.APIKey) - case 1: - url = fmt.Sprintf("%s/v2/exercises/%s?key=%s", c.XAPIHost, args[0], c.APIKey) - case 2: - url = fmt.Sprintf("%s/v2/exercises/%s/%s", c.XAPIHost, args[0], args[1]) - default: - return nil, fmt.Errorf("Usage: exercism fetch\n or: exercism fetch TRACK_ID\n or: exercism fetch TRACK_ID PROBLEM") - } - - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - payload := &PayloadProblems{} - res, err := c.Do(req, payload) - if err != nil { - return nil, err - } - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to fetch problems (HTTP: %d) - %s", res.StatusCode, payload.Error) - } - - return payload.Problems, nil -} - -// Restore fetches the latest revision of a solution and writes it to disk. -func (c *Client) Restore() ([]*Problem, error) { - url := fmt.Sprintf("%s/v2/exercises/restore?key=%s", c.XAPIHost, c.APIKey) - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - payload := &PayloadProblems{} - res, err := c.Do(req, payload) - if err != nil { - return nil, err - } - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to fetch problems (HTTP: %d) - %s", res.StatusCode, payload.Error) - } - - return payload.Problems, nil -} - -// Submissions gets a list of submitted exercises and their current state. -func (c *Client) Submissions() (map[string][]SubmissionInfo, error) { - url := fmt.Sprintf("%s/api/v1/exercises?key=%s", c.APIHost, c.APIKey) - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - var payload map[string][]SubmissionInfo - if _, err := c.Do(req, &payload); err != nil { - return nil, err - } - - return payload, nil -} - -// SubmissionURL gets the url of the latest iteration on the given language track id and problem slug. -func (c *Client) SubmissionURL(trackID, slug string) (*Submission, error) { - url := fmt.Sprintf("%s/api/v1/submissions/%s/%s?key=%s", c.APIHost, trackID, slug, c.APIKey) - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - var payload Submission - if _, err := c.Do(req, &payload); err != nil { - return nil, err - } - - return &payload, nil -} - -// Download fetches a solution by submission key and writes it to disk. -func (c *Client) Download(submissionID string) (*Submission, error) { - url := fmt.Sprintf("%s/api/v1/submissions/%s", c.APIHost, submissionID) - - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - payload := &PayloadSubmission{} - res, err := c.Do(req, payload) - if err != nil { - return nil, err - } - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to fetch Submission (HTTP: %d) - %s", res.StatusCode, payload.Error) - } - - return payload.Submission, err -} - -// Submit posts an iteration to the API. -func (c *Client) Submit(iter *Iteration) (*Submission, error) { - url := fmt.Sprintf("%s/api/v1/user/assignments", c.APIHost) - payload, err := json.Marshal(iter) - if err != nil { - return nil, err - } - - req, err := c.NewRequest("POST", url, bytes.NewReader(payload)) - if err != nil { - return nil, err - } - - ps := &PayloadSubmission{} - res, err := c.Do(req, ps) - if err != nil { - return nil, fmt.Errorf("unable to submit solution - %s", err) - } - - if res.StatusCode != http.StatusCreated { - return nil, fmt.Errorf("unable to submit (HTTP: %d) - %s", res.StatusCode, ps.Error) - } - - return ps.Submission, nil -} - -// List available problems for a language track. -func (c *Client) List(trackID string) ([]string, error) { - url := fmt.Sprintf("%s/tracks/%s", c.XAPIHost, trackID) +const ( + mimeType = "text/plain" +) - req, err := c.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } +var ( + utf8BOM = []byte{0xef, 0xbb, 0xbf} +) - res, err := c.Do(req, nil) +func readFileAsUTF8String(filename string) (*string, error) { + b, err := os.ReadFile(filename) if err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, ErrUnknownTrack - } - - var payload struct { - Track Track - } - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - return nil, err - } - problems := make([]string, len(payload.Track.Problems)) - prefix := trackID + "/" - - for n, p := range payload.Track.Problems { - problems[n] = strings.TrimPrefix(p, prefix) - } - - return problems, nil -} - -// Tracks gets the current list of active and inactive language tracks. -func (c *Client) Tracks() ([]*Track, error) { - url := fmt.Sprintf("%s/tracks", c.XAPIHost) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return []*Track{}, err - } - - var payload struct { - Tracks []*Track - } - if _, err := c.Do(req, &payload); err != nil { - return []*Track{}, err - } - - return payload.Tracks, nil -} - -// Skip marks the exercise in the given language track as skipped. -func (c *Client) Skip(trackID, slug string) error { - url := fmt.Sprintf("%s/api/v1/iterations/%s/%s/skip?key=%s", c.APIHost, trackID, slug, c.APIKey) - - req, err := http.NewRequest("POST", url, nil) - if err != nil { - return err - } - - res, err := c.Do(req, nil) - if err != nil { - return err - } - - defer res.Body.Close() - if res.StatusCode == http.StatusNoContent { - return nil - } - - var pe PayloadError - if err := json.NewDecoder(res.Body).Decode(&pe); err != nil { - return err - } - - return errors.New(pe.Error) -} - -// Status sends a request to exercism to fetch the user's -// completion status for the given language track. -func (c *Client) Status(trackID string) (*StatusInfo, error) { - url := fmt.Sprintf("%s/api/v1/tracks/%s/status?key=%s", c.APIHost, trackID, c.APIKey) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err + encoding, _, certain := charset.DetermineEncoding(b, mimeType) + if !certain { + // We don't want to use an uncertain encoding. + // In particular, doing that may mangle UTF-8 files + // that have only ASCII in their first 1024 bytes. + // See https://github.com/exercism/cli/issues/309. + // So if we're unsure, use UTF-8 (no transformation). + s := string(b) + return &s, nil } - - res, err := c.Do(req, nil) + decoder := encoding.NewDecoder() + decodedBytes, _, err := transform.Bytes(decoder, b) if err != nil { return nil, err } - defer res.Body.Close() - - if res.StatusCode == http.StatusNotFound { - return nil, ErrUnknownTrack - } - - var si StatusInfo - if err := json.NewDecoder(res.Body).Decode(&si); err != nil { - return nil, err - } + // Drop the UTF-8 BOM that may have been added. This isn't necessary, and + // it's going to be written into another UTF-8 buffer anyway once it's JSON + // serialized. + // + // The standard recommends omitting the BOM. See + // http://www.unicode.org/versions/Unicode5.0.0/ch02.pdf + decodedBytes = bytes.TrimPrefix(decodedBytes, utf8BOM) - return &si, nil + s := string(decodedBytes) + return &s, nil } diff --git a/api/api_test.go b/api/api_test.go deleted file mode 100644 index b9b9386e7..000000000 --- a/api/api_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/exercism/cli/config" - "github.com/stretchr/testify/assert" -) - -func respondWithFixture(w http.ResponseWriter, name string) error { - f, err := os.Open("../fixtures/" + name) - if err != nil { - return err - } - - io.Copy(w, f) - f.Close() - - return nil -} -func TestFetchAllProblem(t *testing.T) { - APIKey := "mykey" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - allProblemsAPI := fmt.Sprintf("/v2/exercises?key=%s", APIKey) - assert.Equal(t, allProblemsAPI, req.RequestURI) - - if err := respondWithFixture(w, "problems.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey}) - - problems, err := client.Fetch([]string{}) - assert.NoError(t, err) - - assert.Equal(t, len(problems), 3) -} - -func TestFetchATrack(t *testing.T) { - var ( - APIKey = "mykey" - trackID = "go" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - trackProblemsAPI := fmt.Sprintf("/v2/exercises/%s?key=%s", trackID, APIKey) - assert.Equal(t, trackProblemsAPI, req.RequestURI) - - if err := respondWithFixture(w, "problems.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey}) - - _, err := client.Fetch([]string{trackID}) - assert.NoError(t, err) -} - -func TestFetchASpecificProblem(t *testing.T) { - var ( - APIKey = "mykey" - trackID = "go" - slug = "leap" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - trackProblemsAPI := fmt.Sprintf("/v2/exercises/%s/%s", trackID, slug) - assert.Equal(t, trackProblemsAPI, req.RequestURI) - - if err := respondWithFixture(w, "problems.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey}) - - _, err := client.Fetch([]string{trackID, slug}) - assert.NoError(t, err) -} - -func TestSkipProblem(t *testing.T) { - var ( - APIKey = "mykey" - trackID = "go" - slug = "leap" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - skipAPI := fmt.Sprintf("/api/v1/iterations/%s/%s/skip?key=%s", trackID, slug, APIKey) - assert.Equal(t, skipAPI, req.RequestURI) - - w.WriteHeader(http.StatusNoContent) - })) - defer ts.Close() - - client := NewClient(&config.Config{API: ts.URL, APIKey: APIKey}) - - err := client.Skip(trackID, slug) - assert.NoError(t, err) -} - -func TestSkipProblemErrorResponse(t *testing.T) { - var ( - APIKey = "mykey" - trackID = "go" - slug = "leap" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - skipAPI := fmt.Sprintf("/api/v1/iterations/%s/%s/skip?key=%s", trackID, slug, APIKey) - assert.Equal(t, skipAPI, req.RequestURI) - - w.Write([]byte(`{"error":"exercise skipped"}`)) - })) - defer ts.Close() - - client := NewClient(&config.Config{API: ts.URL, APIKey: APIKey}) - - err := client.Skip(trackID, slug) - assert.Error(t, err) -} - -func TestGetSubmission(t *testing.T) { - var ( - APIKey = "mykey" - trackID = "go" - slug = "leap" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - trackProblemsAPI := fmt.Sprintf("/api/v1/submissions/%s/%s?key=%s", trackID, slug, APIKey) - assert.Equal(t, trackProblemsAPI, req.RequestURI) - - if err := respondWithFixture(w, "submission.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{API: ts.URL, APIKey: APIKey}) - _, err := client.SubmissionURL(trackID, slug) - assert.NoError(t, err) -} - -func TestSubmitAssignment(t *testing.T) { - submissionComment := "hello world!" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusCreated) - - var body map[string]string - if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - t.Fatal(err) - } - - comment, ok := body["comment"] - if ok && comment != submissionComment { - t.Fatal("comment found and was empty") - } - - if err := respondWithFixture(w, "submit.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{API: ts.URL}) - iter := &Iteration{} // it doesn't matter, we're testing that we can read the fixture - sub, err := client.Submit(iter) - assert.NoError(t, err) - - assert.Equal(t, sub.Language, "ruby") - - // Test sending comment - iter.Comment = submissionComment - _, err = client.Submit(iter) - assert.NoError(t, err) -} - -func TestListTrack(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // check that we correctly built the URI path - assert.Equal(t, "/tracks/clojure", req.RequestURI) - - if err := respondWithFixture(w, "tracks.json"); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - client := NewClient(&config.Config{XAPI: ts.URL}) - - problems, err := client.List("clojure") - assert.NoError(t, err) - - assert.Equal(t, len(problems), 34) - assert.Equal(t, problems[0], "bob") -} - -func TestListUnknownTrack(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - http.NotFound(w, req) - })) - defer ts.Close() - - client := NewClient(&config.Config{XAPI: ts.URL}) - - _, err := client.List("rubbbby") - assert.Equal(t, err, ErrUnknownTrack) -} - -func TestStatusUnknownTrack(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - http.NotFound(w, req) - })) - defer ts.Close() - - client := NewClient(&config.Config{API: ts.URL}) - - _, err := client.Status("rubbbby") - assert.Equal(t, err, ErrUnknownTrack) -} diff --git a/api/client.go b/api/client.go index eb1bddd07..a65cd3235 100644 --- a/api/client.go +++ b/api/client.go @@ -1,92 +1,110 @@ package api import ( - "bytes" - "encoding/json" "fmt" "io" "net/http" - "strings" + "time" - "github.com/exercism/cli/config" "github.com/exercism/cli/debug" ) -const ( - urlTrackerAPI = "https://github.com/exercism/exercism.io/issues" - urlTrackerXAPI = "https://github.com/exercism/x-api/issues" -) - var ( // UserAgent lets the API know where the call is being made from. - // It's set from main() so that we have access to the version. - UserAgent string + // It's overridden from the root command so that we can set the version. + UserAgent = "github.com/exercism/cli" + + // TimeoutInSeconds is the timeout the default HTTP client will use. + TimeoutInSeconds = 60 + // HTTPClient is the client used to make HTTP calls in the cli package. + HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} ) -// Client contains the necessary information to contact the Exercism APIs. +// Client is an http client that is configured for Exercism. type Client struct { - client *http.Client - APIHost string - XAPIHost string - APIKey string + *http.Client + ContentType string + Token string + APIBaseURL string } -// NewClient returns an Exercism API Client. -func NewClient(c *config.Config) *Client { +// NewClient returns an Exercism API client. +func NewClient(token, baseURL string) (*Client, error) { return &Client{ - client: http.DefaultClient, - APIHost: c.API, - XAPIHost: c.XAPI, - APIKey: c.APIKey, - } + Client: HTTPClient, + Token: token, + APIBaseURL: baseURL, + }, nil } // NewRequest returns an http.Request with information for the Exercism API. func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) { + if c.Client == nil { + c.Client = HTTPClient + } + req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) - req.Header.Set("Content-Type", "application/json") + if c.ContentType == "" { + req.Header.Set("Content-Type", "application/json") + } else { + req.Header.Set("Content-Type", c.ContentType) + } + if c.Token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token)) + } return req, nil } // Do performs an http.Request and optionally parses the response body into the given interface. -func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { - debug.Println("Request", req.Method, req.URL) +func (c *Client) Do(req *http.Request) (*http.Response, error) { + debug.DumpRequest(req) - res, err := c.client.Do(req) + res, err := c.Client.Do(req) if err != nil { return nil, err } - debug.Printf("Response StatusCode=%d\n", res.StatusCode) - - switch res.StatusCode { - case http.StatusNoContent: - return res, nil - case http.StatusInternalServerError: - url := urlTrackerAPI - if strings.Contains(req.URL.Host, "x.exercism.io") { - url = urlTrackerXAPI - } - return nil, fmt.Errorf("an internal server error was received.\nPlease file a bug report with the contents of 'exercism debug' at: %s ", url) - default: - if v != nil { - defer res.Body.Close() - - var bodyCopy bytes.Buffer - body := io.TeeReader(res.Body, &bodyCopy) - - err := json.NewDecoder(body).Decode(v) - debug.Printf("Response Body\n%s\n\n", bodyCopy.String()) - if err != nil { - return nil, fmt.Errorf("error parsing API response - %s", err) - } - } - } + debug.DumpResponse(res) return res, nil } + +// TokenIsValid calls the API to determine whether the token is valid. +func (c *Client) TokenIsValid() (bool, error) { + url := fmt.Sprintf("%s/validate_token", c.APIBaseURL) + req, err := c.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + resp, err := c.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK, nil +} + +// IsPingable calls the API /ping to determine whether the API can be reached. +func (c *Client) IsPingable() error { + url := fmt.Sprintf("%s/ping", c.APIBaseURL) + req, err := c.NewRequest("GET", url, nil) + if err != nil { + return err + } + resp, err := c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API returned %s", resp.Status) + } + return nil +} diff --git a/api/client_test.go b/api/client_test.go index a7992543b..efb1c3c14 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -1,57 +1,81 @@ package api import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" - "github.com/exercism/cli/config" "github.com/stretchr/testify/assert" ) -var ( - mux *http.ServeMux - server *httptest.Server - client *Client - conf = &config.Config{APIKey: "apikey", API: "localhost", XAPI: "xlocalhost"} -) - func TestNewRequestSetsDefaultHeaders(t *testing.T) { - UserAgent = "Test" - client = NewClient(conf) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `ok`) + })) + defer ts.Close() - req, err := client.NewRequest("GET", client.APIHost, nil) - assert.NoError(t, err) + UserAgent = "BogusAgent" - assert.Equal(t, UserAgent, req.Header.Get("User-Agent")) - assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + testCases := []struct { + desc string + client *Client + auth string + contentType string + }{ + { + desc: "User defaults", + client: &Client{}, + auth: "", + contentType: "application/json", + }, + { + desc: "Override defaults", + client: &Client{ + Token: "abc123", + APIBaseURL: "http://example.com", + ContentType: "bogus", + }, + auth: "Bearer abc123", + contentType: "bogus", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + req, err := tc.client.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + assert.Equal(t, "BogusAgent", req.Header.Get("User-Agent")) + assert.Equal(t, tc.contentType, req.Header.Get("Content-Type")) + assert.Equal(t, tc.auth, req.Header.Get("Authorization")) + }) + } } func TestDo(t *testing.T) { - UserAgent = "Exercism Test v1" - mux = http.NewServeMux() - server = httptest.NewServer(mux) - defer server.Close() - url := server.URL - conf = &config.Config{APIKey: "apikey", API: url, XAPI: url} - client = NewClient(conf) - - type test struct { - T string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + fmt.Fprint(w, `{"hello": "world"}`) + })) + defer ts.Close() + + type payload struct { + Hello string `json:"hello"` } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "GET", r.Method) - assert.Equal(t, UserAgent, r.Header.Get("User-Agent")) + client := &Client{} - fmt.Fprint(w, `{"T":"world"}`) - }) + req, err := client.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) - req, _ := client.NewRequest("GET", client.APIHost+"/", nil) + res, err := client.Do(req) + assert.NoError(t, err) - var body test - _, err := client.Do(req, &body) + var body payload + err = json.NewDecoder(res.Body).Decode(&body) assert.NoError(t, err) - assert.Equal(t, test{T: "world"}, body) + + assert.Equal(t, "world", body.Hello) } diff --git a/api/iteration.go b/api/iteration.go deleted file mode 100644 index 974f98860..000000000 --- a/api/iteration.go +++ /dev/null @@ -1,186 +0,0 @@ -package api - -import ( - "bytes" - "errors" - "io/ioutil" - "os" - "path/filepath" - "strings" - "text/template" - - "golang.org/x/net/html/charset" - "golang.org/x/text/transform" -) - -const ( - mimeType = "text/plain" -) - -var ( - errNoFiles = errors.New("no files submitted") - utf8BOM = []byte{0xef, 0xbb, 0xbf} -) - -var msgSubmitCalledFromWrongDir = `Unable to identify track and file. - -It seems like you've tried to submit a solution file located outside of your -configured exercises directory. - -Current directory: {{ .Current }} -Configured directory: {{ .Configured }} - -Try re-running "exercism fetch". Then move your solution file to the correct -exercise directory for the problem you're working on. It should be somewhere -inside {{ .Configured }} - -For example, to submit the JavaScript "hello-world.js" problem, run -"exercism submit hello-world.js" from this directory: - -{{ .Configured }}{{ .Separator }}javascript{{ .Separator }}hello-world - -You can see where exercism is looking for your files with "exercism debug". - -` - -var msgGenericPathError = `Bad path to exercise file. - -You're trying to submit a solution file from inside your exercises directory, -but it looks like the directory structure is something that exercism doesn't -recognize as a valid file path. - -First, make a copy of your solution file and save it outside of -{{ .Configured }} - -Then, run "exercism fetch". Move your solution file back to the correct -exercise directory for the problem you're working on. It should be somewhere -inside {{ .Configured }} - -If you are having trouble, you can file a GitHub issue at (https://github.com/exercism/exercism.io/issues) - -` - -// Iteration represents a version of a particular exercise. -// This gets submitted to the API. -type Iteration struct { - Key string `json:"key"` - Code string `json:"code"` - Dir string `json:"dir"` - TrackID string `json:"language"` - Problem string `json:"problem"` - Solution map[string]string `json:"solution"` - Comment string `json:"comment,omitempty"` -} - -// NewIteration prepares an iteration of a problem in a track for submission to the API. -// It takes a dir (from the global config) and a list of files which it will read from disk. -// All paths are assumed to be absolute paths with symlinks resolved. -func NewIteration(dir string, filenames []string) (*Iteration, error) { - if len(filenames) == 0 { - return nil, errNoFiles - } - - iter := &Iteration{ - Dir: dir, - Solution: map[string]string{}, - } - - // All the files should be within the exercism path. - for _, filename := range filenames { - if !iter.isValidFilepath(filename) { - // User has run exercism submit in the wrong directory. - return nil, newIterationError(msgSubmitCalledFromWrongDir, iter.Dir) - } - } - - // Identify the language track and problem slug. - path := filenames[0][len(dir):] - - segments := strings.Split(path, string(filepath.Separator)) - if len(segments) < 4 { - // Submit was called from inside exercism directory, but the path - // is still bad. Has the user modified their path in some way? - return nil, newIterationError(msgGenericPathError, iter.Dir) - } - iter.TrackID = segments[1] - iter.Problem = segments[2] - - for _, filename := range filenames { - fileContents, err := readFileAsUTF8String(filename) - if err != nil { - return nil, err - } - - path := filename[len(iter.RelativePath()):] - iter.Solution[path] = *fileContents - } - return iter, nil -} - -// RelativePath returns the iteration's relative path. -func (iter *Iteration) RelativePath() string { - return filepath.Join(iter.Dir, iter.TrackID, iter.Problem) + string(filepath.Separator) -} - -// isValidFilepath checks a files's absolute filepath and returns true if it is -// within the configured exercise directory. -func (iter *Iteration) isValidFilepath(path string) bool { - if iter == nil { - return false - } - return strings.HasPrefix(strings.ToLower(path), strings.ToLower(iter.Dir)) -} - -func readFileAsUTF8String(filename string) (*string, error) { - b, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - encoding, _, _ := charset.DetermineEncoding(b, mimeType) - decoder := encoding.NewDecoder() - decodedBytes, _, err := transform.Bytes(decoder, b) - if err != nil { - return nil, err - } - - // Drop the UTF-8 BOM that may have been added. This isn't necessary, and - // it's going to be written into another UTF-8 buffer anyway once it's JSON - // serialized. - // - // The standard recommends omitting the BOM. See - // http://www.unicode.org/versions/Unicode5.0.0/ch02.pdf - decodedBytes = bytes.TrimPrefix(decodedBytes, utf8BOM) - - s := string(decodedBytes) - return &s, nil -} - -// newIterationError executes an error message template to create a detailed -// message for the end user. An error type is returned. -func newIterationError(msgTemplate, configured string) error { - buffer := bytes.NewBufferString("") - t, err := template.New("iterErr").Parse(msgTemplate) - if err != nil { - return err - } - - current, err := os.Getwd() - if err != nil { - return err - } - - var pathData = struct { - Current string - Configured string - Separator string - }{ - current, - configured, - string(filepath.Separator), - } - - t.Execute(buffer, pathData) - msg := buffer.String() - return errors.New(msg) -} diff --git a/api/iteration_test.go b/api/iteration_test.go deleted file mode 100644 index 59c4504c4..000000000 --- a/api/iteration_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package api - -import ( - "path/filepath" - "runtime" - "strings" - "testing" - "unicode/utf8" -) - -func TestNewIteration(t *testing.T) { - _, path, _, _ := runtime.Caller(0) - dir := filepath.Join(path, "..", "..", "fixtures", "iteration") - - files := []string{ - filepath.Join(dir, "python", "leap", "one.py"), - filepath.Join(dir, "python", "leap", "two.py"), - filepath.Join(dir, "python", "leap", "lib", "three.py"), - filepath.Join(dir, "python", "leap", "utf16le.py"), - filepath.Join(dir, "python", "leap", "utf16be.py"), - } - - iter, err := NewIteration(dir, files) - if err != nil { - t.Fatal(err) - } - - if iter.TrackID != "python" { - t.Errorf("Expected language to be python, was %s", iter.TrackID) - } - if iter.Problem != "leap" { - t.Errorf("Expected problem to be leap, was %s", iter.Problem) - } - - if len(iter.Solution) != 5 { - t.Fatalf("Expected solution to have 3 files, had %d", len(iter.Solution)) - } - - expected := map[string]string{ - "one.py": "# one", - "two.py": "# two", - filepath.Join("lib", "three.py"): "# three", - "utf16le.py": "# utf16le", - "utf16be.py": "# utf16be", - } - - for filename, code := range expected { - if !utf8.ValidString(iter.Solution[filename]) { - t.Errorf("Iteration content is not valid UTF-8 data: %s", iter.Solution[filename]) - } - - if !strings.HasPrefix(iter.Solution[filename], code) { - t.Errorf("Expected %s to contain `%s', had `%s'", filename, code, iter.Solution[filename]) - } - } -} - -func TestIterationValidFile(t *testing.T) { - testCases := []struct { - file string - ok bool - }{ - { - file: "/Users/me/exercism/ruby/bob/totally/fine/deep/path/src/bob.rb", - ok: true, - }, - { - file: "/Users/me/exercism/ruby/bob/bob.rb", - ok: true, - }, - { - file: "/users/me/exercism/ruby/bob/bob.rb", - ok: true, - }, - { - file: "/Users/me/bob.rb", - ok: false, - }, - { - file: "/tmp/bob.rb", - ok: false, - }, - } - - for _, tt := range testCases { - iter := &Iteration{ - Dir: "/Users/me/exercism", - } - ok := iter.isValidFilepath(tt.file) - if ok && !tt.ok { - t.Errorf("Expected %s to be invalid.", tt.file) - } - } -} diff --git a/api/problem.go b/api/problem.go deleted file mode 100644 index 9ba002f66..000000000 --- a/api/problem.go +++ /dev/null @@ -1,18 +0,0 @@ -package api - -import "fmt" - -// Problem represents a specific problem in a given language track. -type Problem struct { - ID string `json:"id"` - TrackID string `json:"track_id"` - Language string `json:"language"` - Slug string `json:"slug"` - Name string `json:"name"` - Files map[string]string `json:"files"` - Submitted bool -} - -func (p *Problem) String() string { - return fmt.Sprintf("%s (%s)", p.Language, p.Name) -} diff --git a/api/status_info.go b/api/status_info.go deleted file mode 100644 index e158474e1..000000000 --- a/api/status_info.go +++ /dev/null @@ -1,64 +0,0 @@ -package api - -import ( - "fmt" - "log" - "strings" - "time" -) - -const dateFormat = "January 2, 2006" - -// StatusInfo contains information about a user's status on a particular language track. -type StatusInfo struct { - TrackID string `json:"track_id"` - Recent *Recent - FetchedProblems *Slugs `json:"fetched"` - SkippedProblems *Slugs `json:"skipped"` -} - -// Recent contains information about the user's most recently submitted exercise on a particular language track. -type Recent struct { - Problem string `json:"problem"` - SubmittedAt string `json:"submitted_at"` -} - -// Slugs is a collection of slugs, all of which are the names of exercises. -type Slugs []string - -func (r *Recent) String() string { - submittedAt, err := time.Parse(time.RFC3339Nano, r.SubmittedAt) - if err != nil { - log.Fatal(err) - } - - return fmt.Sprintf(" - %s (submitted on %s)", r.Problem, submittedAt.Format(dateFormat)) -} - -func (s *StatusInfo) String() string { - if len(*s.FetchedProblems) == 0 && s.Recent.Problem == "" { - return fmt.Sprintf("\nYou have yet to begin the %s track!\n", s.TrackID) - } - - msg := ` -Your status on the %s track: - -Most recently submitted exercise: -%s - -Exercises fetched but not submitted: -%s - -Exercises skipped: -%s -` - - return fmt.Sprintf(msg, s.TrackID, s.Recent, s.FetchedProblems, s.SkippedProblems) -} - -func (s Slugs) String() string { - for i, problem := range s { - s[i] = fmt.Sprintf(" - %s", problem) - } - return strings.Join(s, "\n") -} diff --git a/api/submission.go b/api/submission.go deleted file mode 100644 index 199c9b4ec..000000000 --- a/api/submission.go +++ /dev/null @@ -1,14 +0,0 @@ -package api - -// Submission is an iteration that has been submitted to the API. -type Submission struct { - URL string `json:"url"` - TrackID string `json:"track_id"` - Language string `json:"language"` - Slug string `json:"slug"` - Name string `json:"name"` - Username string `json:"username"` - ProblemFiles map[string]string `json:"problem_files"` - SolutionFiles map[string]string `json:"solution_files"` - Iteration int `json:"iteration"` -} diff --git a/api/track.go b/api/track.go deleted file mode 100644 index b9323f66c..000000000 --- a/api/track.go +++ /dev/null @@ -1,20 +0,0 @@ -package api - -import "fmt" - -// Track is a collection of problems in a given language. -type Track struct { - ID string `json:"id"` - Language string `json:"language"` - Active bool `json:"active"` - Problems []string `json:"problems"` -} - -// Len lists the number of problems a track has. -func (t *Track) Len() int { - return len(t.Problems) -} - -func (t *Track) String() string { - return fmt.Sprintf("%s (%s)", t.Language, t.ID) -} diff --git a/bin/build-all b/bin/build-all deleted file mode 100755 index b4be75e99..000000000 --- a/bin/build-all +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash - -set -e -x - -echo "Creating release dir..." -mkdir -p release - -# variables as defined by "go tool nm" -OSVAR=github.com/exercism/cli/cmd.BuildOS -ARCHVAR=github.com/exercism/cli/cmd.BuildARCH -ARMVAR=github.com/exercism/cli/cmd.BuildARM - -createRelease() { - os=$1 - arch=$2 - arm=$3 - - if [ "$os" = darwin ] - then - osname='mac' - else - osname=$os - fi - if [ "$arch" = amd64 ] - then - osarch=64bit - else - osarch=32bit - fi - - ldflags="-X $OSVAR $os -X $ARCHVAR $arch" - if [ "$arm" ] - then - osarch=arm-v$arm - ldflags="$ldflags -X $ARMVAR $arm" - fi - - binname=exercism - if [ "$osname" = windows ] - then - binname="$binname.exe" - fi - - relname="../release/exercism-$osname-$osarch" - echo "Creating $os/$arch binary..." - - if [ "$arm" ] - then - GOOS=$os GOARCH=$arch GOARM=$arm go build -ldflags "$ldflags" -o "out/$binname" exercism/main.go - else - GOOS=$os GOARCH=$arch go build -ldflags "$ldflags" -o "out/$binname" exercism/main.go - fi - - cd out - - if [ "$osname" = windows ] - then - zip "$relname.zip" "$binname" - else - tar cvzf "$relname.tgz" "$binname" - fi - cd .. -} - -# Mac Releases -createRelease darwin 386 -createRelease darwin amd64 - -# Linux Releases -createRelease linux 386 -createRelease linux amd64 - -# ARM Releases -createRelease linux arm 5 -createRelease linux arm 6 -createRelease linux arm 7 - -# Windows Releases -createRelease windows 386 -createRelease windows amd64 diff --git a/bin/build.sh b/bin/build.sh new file mode 100755 index 000000000..f2ed29c5d --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go build -o testercism ./exercism/main.go diff --git a/bin/format.sh b/bin/format.sh new file mode 100755 index 000000000..25c19b2e3 --- /dev/null +++ b/bin/format.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go fmt ./... diff --git a/bin/release.sh b/bin/release.sh new file mode 100755 index 000000000..05f228455 --- /dev/null +++ b/bin/release.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GPG_FINGERPRINT}" ]]; then + echo "GPG_FINGERPRINT environment variable is not set" + exit 1 +fi + +echo "Syncing repo with latest main..." +git checkout main +git pull + +VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' cmd/version.go) +TAG_NAME="v${VERSION}" + +echo "Verify release can be built..." +goreleaser --skip=publish --snapshot --clean + +echo "Pushing tag..." +git tag -a "${TAG_NAME}" -m "Release ${TAG_NAME}" +git push origin "${TAG_NAME}" + +echo "Tag pushed" +echo "The release CI workflow will automatically create a draft release." +echo "Once created, edit the release notes and publish it." diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 000000000..062c46972 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go test ./... diff --git a/browser/open.go b/browser/open.go new file mode 100644 index 000000000..48221326c --- /dev/null +++ b/browser/open.go @@ -0,0 +1,32 @@ +package browser + +import ( + "os/exec" + "runtime" + "strings" +) + +// Open opens a browser to the given URL. +// The terminal's open command is operating system dependent. +func Open(url string) error { + // Escape characters are not allowed by cmd/bash. + switch runtime.GOOS { + case "windows": + url = strings.Replace(url, "&", `^&`, -1) + default: + url = strings.Replace(url, "&", `\&`, -1) + } + + // The command to open the browser is OS-dependent. + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "freebsd", "linux", "netbsd", "openbsd": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + } + + return cmd.Run() +} diff --git a/cli/asset.go b/cli/asset.go new file mode 100644 index 000000000..10bbab051 --- /dev/null +++ b/cli/asset.go @@ -0,0 +1,37 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "net/http" +) + +// Asset is a build for a particular system, uploaded to a GitHub release. +type Asset struct { + ID int `json:"id"` + Name string `json:"name"` + ContentType string `json:"content_type"` +} + +func (a *Asset) download() (*bytes.Reader, error) { + downloadURL := fmt.Sprintf("%s/assets/%d", ReleaseURL, a.ID) + req, err := http.NewRequest("GET", downloadURL, nil) + if err != nil { + return nil, err + } + // https://developer.github.com/v3/repos/releases/#get-a-single-release-asset + req.Header.Set("Accept", "application/octet-stream") + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + bs, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + return bytes.NewReader(bs), nil +} diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 000000000..fe113ec5b --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,211 @@ +package cli + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "runtime" + "strings" + "time" + + "github.com/blang/semver" + "github.com/exercism/cli/debug" + update "github.com/inconshreveable/go-update" +) + +var ( + // BuildOS is the operating system (GOOS) used during the build process. + BuildOS string + // BuildARM is the ARM version (GOARM) used during the build process. + BuildARM string + // BuildARCH is the architecture (GOARCH) used during the build process. + BuildARCH string +) + +var ( + osMap = map[string]string{ + "darwin": "darwin", + "freebsd": "freebsd", + "linux": "linux", + "openbsd": "openbsd", + "windows": "windows", + } + + archMap = map[string]string{ + "386": "i386", + "amd64": "x86_64", + "arm": "arm", + "ppc64": "ppc64", + } +) + +var ( + // TimeoutInSeconds is the timeout the default HTTP client will use. + TimeoutInSeconds = 60 + // HTTPClient is the client used to make HTTP calls in the cli package. + HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} + // ReleaseURL is the endpoint that provides information about cli releases. + ReleaseURL = "https://api.github.com/repos/exercism/cli/releases" +) + +// Updater is a simple upgradable file interface. +type Updater interface { + IsUpToDate() (bool, error) + Upgrade() error +} + +// CLI is information about the CLI itself. +type CLI struct { + Version string + LatestRelease *Release +} + +// New creates a CLI, setting it to a particular version. +func New(version string) *CLI { + return &CLI{ + Version: version, + } +} + +// IsUpToDate compares the current version to that of the latest release. +func (c *CLI) IsUpToDate() (bool, error) { + if c.LatestRelease == nil { + if err := c.fetchLatestRelease(); err != nil { + return false, err + } + } + + rv, err := semver.Make(c.LatestRelease.Version()) + if err != nil { + return false, fmt.Errorf("unable to parse latest version (%s): %s", c.LatestRelease.Version(), err) + } + cv, err := semver.Make(c.Version) + if err != nil { + return false, fmt.Errorf("unable to parse current version (%s): %s", c.Version, err) + } + + return cv.GTE(rv), nil +} + +// Upgrade allows the user to upgrade to the latest version of the CLI. +func (c *CLI) Upgrade() error { + var ( + OS = osMap[runtime.GOOS] + ARCH = archMap[runtime.GOARCH] + ) + + if OS == "" || ARCH == "" { + return fmt.Errorf("unable to upgrade: OS %s ARCH %s", OS, ARCH) + } + + buildName := fmt.Sprintf("%s-%s", OS, ARCH) + if BuildARCH == "arm" { + if BuildARM == "" { + return fmt.Errorf("unable to upgrade: arm version not found") + } + buildName = fmt.Sprintf("%s-v%s", buildName, BuildARM) + } + + var downloadRC *bytes.Reader + for _, a := range c.LatestRelease.Assets { + if strings.Contains(a.Name, buildName) { + debug.Printf("Downloading %s\n", a.Name) + var err error + downloadRC, err = a.download() + if err != nil { + return fmt.Errorf("error downloading executable: %s", err) + } + break + } + } + if downloadRC == nil { + return fmt.Errorf("no executable found for %s/%s%s", BuildOS, BuildARCH, BuildARM) + } + + bin, err := extractBinary(downloadRC, OS) + if err != nil { + return err + } + defer bin.Close() + + return update.Apply(bin, update.Options{}) +} + +func (c *CLI) fetchLatestRelease() error { + latestReleaseURL := fmt.Sprintf("%s/%s", ReleaseURL, "latest") + resp, err := HTTPClient.Get(latestReleaseURL) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode > 399 { + msg := "failed to get the latest release\n" + for k, v := range resp.Header { + msg += fmt.Sprintf("\n %s:\n %s", k, v) + } + return fmt.Errorf(msg) + } + + var rel Release + if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil { + return err + } + c.LatestRelease = &rel + return nil +} + +func extractBinary(source *bytes.Reader, platform string) (binary io.ReadCloser, err error) { + if platform == "windows" { + zr, err := zip.NewReader(source, int64(source.Len())) + if err != nil { + return nil, err + } + + for _, f := range zr.File { + info := f.FileInfo() + if info.IsDir() || !strings.HasSuffix(f.Name, ".exe") { + continue + } + return f.Open() + } + } else { + gr, err := gzip.NewReader(source) + if err != nil { + return nil, err + } + defer gr.Close() + + tr := tar.NewReader(gr) + for { + _, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + tmpfile, err := os.CreateTemp("", "temp-exercism") + if err != nil { + return nil, err + } + + if _, err = io.Copy(tmpfile, tr); err != nil { + return nil, err + } + if _, err := tmpfile.Seek(0, 0); err != nil { + return nil, err + } + + binary = tmpfile + } + } + + return binary, nil +} diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 000000000..c9e71e37c --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,80 @@ +package cli + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsUpToDate(t *testing.T) { + testCases := []struct { + desc string + cliVersion string + releaseTag string + ok bool + }{ + { + desc: "It returns false for versions less than release.", + cliVersion: "1.0.0", + releaseTag: "v1.0.1", + ok: false, + }, + { + desc: "It returns false for pre-release versions of release.", + cliVersion: "1.0.1-alpha.1", + releaseTag: "v1.0.1", + ok: false, + }, + { + desc: "It returns true for versions equal to release.", + cliVersion: "2.0.1", + releaseTag: "v2.0.1", + ok: true, + }, + { + desc: "It returns true for versions greater than release.", + cliVersion: "2.0.2", + releaseTag: "v2.0.1", + ok: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + c := &CLI{ + Version: tc.cliVersion, + LatestRelease: &Release{TagName: tc.releaseTag}, + } + + ok, err := c.IsUpToDate() + assert.NoError(t, err) + assert.Equal(t, tc.ok, ok, tc.cliVersion) + }) + } +} + +func TestIsUpToDateWithoutRelease(t *testing.T) { + fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Checking for the latest release should call latestReleaseURL endpoint. + // if the code below fails to return the proper response then the URL generation logic in pkg cli has changed. + if r.URL.Path != "/latest" { + fmt.Fprintln(w, "") + } + fmt.Fprintln(w, `{"tag_name": "v2.0.0"}`) + }) + ts := httptest.NewServer(fakeEndpoint) + defer ts.Close() + ReleaseURL = ts.URL + + c := &CLI{ + Version: "1.0.0", + } + + ok, err := c.IsUpToDate() + assert.NoError(t, err) + assert.False(t, ok) + assert.NotNil(t, c.LatestRelease) +} diff --git a/cli/release.go b/cli/release.go new file mode 100644 index 000000000..973057f4e --- /dev/null +++ b/cli/release.go @@ -0,0 +1,15 @@ +package cli + +import "strings" + +// Release is a specific build of the CLI, released on GitHub. +type Release struct { + Location string `json:"html_url"` + TagName string `json:"tag_name"` + Assets []Asset `json:"assets"` +} + +// Version is the CLI version that is built for the release. +func (r *Release) Version() string { + return strings.TrimPrefix(r.TagName, "v") +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 62dcb0adb..ac28286a7 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,29 +1,129 @@ package cmd import ( - "path/filepath" + "encoding/json" + "errors" + "fmt" + "net/http" "regexp" + "strconv" "strings" + + "io" + + "github.com/exercism/cli/config" + "github.com/spf13/viper" ) -const ( - msgPleaseAuthenticate = "You must be authenticated. Run `exercism configure --key=YOUR_API_KEY`." +var ( + // BinaryName is the name of the app. + // By default this is exercism, but people + // are free to name this however they want. + // The usage examples and help strings should reflect + // the actual name of the binary. + BinaryName string + // Out is used to write to information. + Out io.Writer + // Err is used to write errors. + Err io.Writer + // jsonContentTypeRe is used to match Content-Type which contains JSON. + jsonContentTypeRe = regexp.MustCompile(`^application/([[:alpha:]]+\+)?json($|;)`) ) -func isTest(path string) bool { - ext := filepath.Ext(path) - if ext == ".t" { - return true - } +const msgWelcomePleaseConfigure = ` + + Welcome to Exercism! + + To get started, you need to configure the tool with your API token. + Find your token at + + %s + + Then run the configure command: + + %s configure --token=YOUR_TOKEN + +` + +// Running configure without any arguments will attempt to +// set the default workspace. If the default workspace directory +// risks clobbering an existing directory, it will print an +// error message that explains how to proceed. +const msgRerunConfigure = ` + + Please re-run the configure command to define where + to download the exercises. - file := filepath.Base(path) - name := file[:len(file)-len(ext)] - if name == "test" || name == "spec" { - return true + %s configure +` + +const msgMissingMetadata = ` + + The exercise you are submitting doesn't have the necessary metadata. + Please see https://github.com/exercism/website-copy/blob/main/pages/cli_v1_to_v2.md for instructions on how to fix it. + +` + +// validateUserConfig validates the presence of required user config values +func validateUserConfig(cfg *viper.Viper) error { + if cfg.GetString("token") == "" { + return fmt.Errorf( + msgWelcomePleaseConfigure, + config.TokenURL(cfg.GetString("apibaseurl")), + BinaryName, + ) + } + if cfg.GetString("workspace") == "" || cfg.GetString("apibaseurl") == "" { + return fmt.Errorf(msgRerunConfigure, BinaryName) } - return regexp.MustCompile(`[\._-]?([tT]est|[sS]pec)`).MatchString(name) + return nil } -func isREADME(path string) bool { - return strings.Contains(path, "README") +// decodedAPIError decodes and returns the error message from the API response. +// If the message is blank, it returns a fallback message with the status code. +func decodedAPIError(resp *http.Response) error { + // First and foremost, handle Retry-After headers; if set, show this to the user. + if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { + // The Retry-After header can be an HTTP Date or delay seconds. + // The date can be used as-is. The delay seconds should have "seconds" appended. + if delay, err := strconv.Atoi(retryAfter); err == nil { + retryAfter = fmt.Sprintf("%d seconds", delay) + } + return fmt.Errorf( + "request failed with status %s; please try again after %s", + resp.Status, + retryAfter, + ) + } + + // Check for JSON data. On non-JSON data, show the status and content type then bail. + // Otherwise, extract the message details from the JSON. + if contentType := resp.Header.Get("Content-Type"); !jsonContentTypeRe.MatchString(contentType) { + return fmt.Errorf( + "expected response with Content-Type \"application/json\" but got status %q with Content-Type %q", + resp.Status, + contentType, + ) + } + var apiError struct { + Error struct { + Type string `json:"type"` + Message string `json:"message"` + PossibleTrackIDs []string `json:"possible_track_ids"` + } `json:"error,omitempty"` + } + if err := json.NewDecoder(resp.Body).Decode(&apiError); err != nil { + return fmt.Errorf("failed to parse API error response: %s", err) + } + if apiError.Error.Message != "" { + if apiError.Error.Type == "track_ambiguous" { + return fmt.Errorf( + "%s: %s", + apiError.Error.Message, + strings.Join(apiError.Error.PossibleTrackIDs, ", "), + ) + } + return errors.New(apiError.Error.Message) + } + return fmt.Errorf("unexpected API response: %d", resp.StatusCode) } diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index c6f65698b..d712e742b 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1,95 +1,201 @@ package cmd -import "testing" +import ( + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "testing" -func TestIsTest(t *testing.T) { + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +const cfgHomeKey = "EXERCISM_CONFIG_HOME" + +// CommandTest makes it easier to write tests for Cobra commands. +// +// To initialize, give it the three fields Cmd, InitFn, and Args. +// Then call Setup, and defer the Teardown. +// The args are the faked out os.Args. The first two arguments +// in the Args will be ignored. These represent the command (e.g. exercism) +// and the subcommand (e.g. download). +// Pass any interactive responses needed for the test in a single +// String in MockInput, delimited by newlines. +// +// Finally, when you have done whatever other setup you need in your +// test, call the command by calling Execute on the App. +// +// Example: +// +// cmdTest := &CommandTest{ +// Cmd: myCmd, +// InitFn: initMyCmd, +// Args: []string{"fakeapp", "mycommand", "arg1", "--flag", "value"}, +// MockInteractiveResponse: "first-input\nsecond\n", +// } +// +// cmdTest.Setup(t) +// defer cmdTest.Teardown(t) +// ... +// cmdTest.App.Execute() +type CommandTest struct { + App *cobra.Command + Cmd *cobra.Command + InitFn func() + TmpDir string + Args []string + MockInteractiveResponse string + OriginalValues struct { + ConfigHome string + Args []string + } +} + +// Setup does all the prep and initialization for testing a command. +// It creates a fake Cobra app to provide a clean harness for the test, +// and adds the command under test to it as a subcommand. +// It also resets and reconfigures the command under test to +// make sure we're not getting any accidental pollution from the existing +// environment or other tests. Lastly, because we need to override some of +// the global environment settings, the setup method also stores the existing +// values so that Teardown can set them back the way they were when the test +// has completed. +// The method takes a *testing.T as an argument, that way the method can +// fail the test if the creation of the temporary directory fails. +func (test *CommandTest) Setup(t *testing.T) { + dir, err := os.MkdirTemp("", "command-test") + defer os.RemoveAll(dir) + assert.NoError(t, err) + + test.TmpDir = dir + test.OriginalValues.ConfigHome = os.Getenv(cfgHomeKey) + test.OriginalValues.Args = os.Args + + os.Setenv(cfgHomeKey, test.TmpDir) + + os.Args = test.Args + + test.Cmd.ResetFlags() + test.InitFn() + + test.App = &cobra.Command{} + test.App.AddCommand(test.Cmd) + test.App.SetOutput(Err) +} + +// Teardown puts the environment back the way it was before the test. +// The method takes a *testing.T so that it can blow up if it fails to +// clean up after itself. +func (test *CommandTest) Teardown(t *testing.T) { + os.Setenv(cfgHomeKey, test.OriginalValues.ConfigHome) + os.Args = test.OriginalValues.Args + if err := os.RemoveAll(test.TmpDir); err != nil { + t.Fatal(err) + } +} + +// capturedOutput lets us more easily redirect streams in the tests. +type capturedOutput struct { + oldOut, oldErr, newOut, newErr io.Writer +} + +// newCapturedOutput creates a new value to override the streams. +func newCapturedOutput() capturedOutput { + return capturedOutput{ + oldOut: Out, + oldErr: Err, + newOut: io.Discard, + newErr: io.Discard, + } +} + +// override sets the package variables to the fake streams. +func (co capturedOutput) override() { + Out = co.newOut + Err = co.newErr +} + +// reset puts back the original streams for the commands to write to. +func (co capturedOutput) reset() { + Out = co.oldOut + Err = co.oldErr +} + +func errorResponse418(contentType string, body string) *http.Response { + response := &http.Response{ + Status: "418 I'm a teapot", + StatusCode: 418, + Header: make(http.Header), + Body: ioutil.NopCloser(strings.NewReader(body)), + ContentLength: int64(len(body)), + } + response.Header.Set("Content-Type", contentType) + return response +} + +func errorResponse429(retryAfter string) *http.Response { + body := "" + response := &http.Response{ + Status: "429 Too Many Requests", + StatusCode: 429, + Header: make(http.Header), + Body: ioutil.NopCloser(strings.NewReader(body)), + ContentLength: int64(len(body)), + } + response.Header.Set("Content-Type", "text/plain") + response.Header.Set("Retry-After", retryAfter) + return response +} + +func TestDecodeErrorResponse(t *testing.T) { testCases := []struct { - name string - isTest bool + response *http.Response + wantMessage string }{ { - name: "problem/whatever-test.ext", - isTest: true, - }, - { - name: "problem/whatever.ext", - isTest: false, - }, - { - name: "problem/whatever_test.spec.ext", - isTest: true, - }, - { - name: "problem/WhateverTest.ext", - isTest: true, - }, - { - name: "problem/Whatever.ext", - isTest: false, - }, - { - name: "problem/whatever_test.ext", - isTest: true, + response: errorResponse418("text/html", "Time for tea"), + wantMessage: `expected response with Content-Type "application/json" but got status "418 I'm a teapot" with Content-Type "text/html"`, }, { - name: "problem/whatever.ext", - isTest: false, + response: errorResponse418("application/json", `{"error": {"type": "json", "valid": no}}`), + wantMessage: "failed to parse API error response: invalid character 'o' in literal null (expecting 'u')", }, { - name: "problem/test.ext", - isTest: true, + response: errorResponse418("application/json; charset=utf-8", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), + wantMessage: "message: a, b", }, { - name: "problem/Whatever.t", // perl - isTest: true, + response: errorResponse418("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), + wantMessage: "message: a, b", }, { - name: "whatever_spec.ext", // lua - isTest: true, - }, - } - - for _, tt := range testCases { - if isTest(tt.name) != tt.isTest { - t.Fatalf("Expected isTest(%s) to be %t", tt.name, tt.isTest) - } - } -} - -func TestIsREADME(t *testing.T) { - testCases := []struct { - name string - isREADME bool - }{ - { - name: "problem/README.md", - isREADME: true, + response: errorResponse418("application/json", `{"error": {"message": "message"}}`), + wantMessage: "message", }, { - name: "problem/README", - isREADME: true, + response: errorResponse418("application/problem+json", `{"error": {"message": "new json format"}}`), + wantMessage: "new json format", }, { - name: "problem/README.txt", - isREADME: true, + response: errorResponse418("application/json", `{"error": {}}`), + wantMessage: "unexpected API response: 418", }, { - name: "problem/some_problem.py", - isREADME: false, + response: errorResponse429("30"), + wantMessage: "request failed with status 429 Too Many Requests; please try again after 30 seconds", }, { - name: "problem/readme.lua", - isREADME: false, - }, - { - name: "problem/readme_spec.lua", - isREADME: false, + response: errorResponse429("Wed, 21 Oct 2015 07:28:00 GMT"), + wantMessage: "request failed with status 429 Too Many Requests; please try again after Wed, 21 Oct 2015 07:28:00 GMT", }, } - - for _, tt := range testCases { - if isREADME(tt.name) != tt.isREADME { - t.Fatalf("Expected isREADME(%s) to be %t", tt.name, tt.isREADME) - } + tc := testCases[0] + got := decodedAPIError(tc.response) + assert.Equal(t, tc.wantMessage, got.Error()) + for _, tc = range testCases { + got := decodedAPIError(tc.response) + assert.Equal(t, tc.wantMessage, got.Error()) } } diff --git a/cmd/configure.go b/cmd/configure.go index 7cee04120..57aabc04c 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,42 +2,240 @@ package cmd import ( "fmt" - "log" "os" + "strings" + "text/tabwriter" - "github.com/codegangsta/cli" + "github.com/exercism/cli/api" "github.com/exercism/cli/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) -// Configure stores settings in a JSON file. -// If a setting is not passed as an argument, default -// values are used. -func Configure(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) +var ( + viperConfig *viper.Viper +) + +// configureCmd configures the command-line client with user-specific settings. +var configureCmd = &cobra.Command{ + Use: "configure", + Aliases: []string{"c"}, + Short: "Configure the command-line client.", + Long: `Configure the command-line client to customize it to your needs. + +This lets you set up the CLI to talk to the API on your behalf, +and tells the CLI about your setup so it puts things in the right +places. + +You can also override certain default settings to suit your preferences. +`, + RunE: func(cmd *cobra.Command, args []string) error { + configuration := config.NewConfig() + + viperConfig.AddConfigPath(configuration.Dir) + viperConfig.SetConfigName("user") + viperConfig.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = viperConfig.ReadInConfig() + configuration.UserViperConfig = viperConfig + + return runConfigure(configuration, cmd.Flags()) + }, +} + +func runConfigure(configuration config.Config, flags *pflag.FlagSet) error { + cfg := configuration.UserViperConfig + + // Show the existing configuration and exit. + show, err := flags.GetBool("show") + if err != nil { + return err + } + if show { + printCurrentConfig(configuration) + return nil + } + + // If the command is run 'bare' and we have no token, + // explain how to set the token. + if flags.NFlag() == 0 && cfg.GetString("token") == "" { + tokenURL := config.TokenURL(cfg.GetString("apibaseurl")) + return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) + } + + // Determine the base API URL. + baseURL, err := flags.GetString("api") + if err != nil { + return err + } + if baseURL == "" { + baseURL = cfg.GetString("apibaseurl") + } + if baseURL == "" { + baseURL = configuration.DefaultBaseURL + } + + // By default we verify that + // - the configured API URL is reachable. + // - the configured token is valid. + skipVerification, err := flags.GetBool("no-verify") + if err != nil { + return err + } + + // Is the API URL reachable? + if !skipVerification { + client, err := api.NewClient("", baseURL) + if err != nil { + return err + } + + if err := client.IsPingable(); err != nil { + return fmt.Errorf("The base API URL '%s' cannot be reached.\n\n%s", baseURL, err) + } + } + // Finally, configure the URL. + cfg.Set("apibaseurl", baseURL) + + // Determine the token. + token, err := flags.GetString("token") + if err != nil { + return err + } + if token == "" { + token = cfg.GetString("token") + } + + tokenURL := config.TokenURL(cfg.GetString("apibaseurl")) + + // If we don't have a token then explain how to set it and bail. + if token == "" { + return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) + } + + // Verify that the token is valid. + if !skipVerification { + client, err := api.NewClient(token, baseURL) + if err != nil { + return err + } + ok, err := client.TokenIsValid() + if err != nil { + return err + } + if !ok { + return fmt.Errorf("The token '%s' is invalid. Find your token on %s.", token, tokenURL) + } + } + + // Finally, configure the token. + cfg.Set("token", token) + + // Determine the workspace. + workspace, err := flags.GetString("workspace") if err != nil { - log.Fatal(err) + return err } + if workspace == "" { + workspace = cfg.GetString("workspace") + } + workspace = config.Resolve(workspace, configuration.Home) + + if workspace != "" { + // If there is a non-directory here, then we cannot proceed. + if info, err := os.Lstat(workspace); !os.IsNotExist(err) && !info.IsDir() { + msg := ` + + There is already something at the workspace location you are configuring: + + %s + + Please rename it, or set a different workspace location: + + %s configure %s --workspace=PATH_TO_DIFFERENT_FOLDER + ` + + return fmt.Errorf(msg, workspace, BinaryName, commandify(flags)) + } + } + + if workspace == "" { + workspace = config.DefaultWorkspaceDir(configuration) - key := ctx.String("key") - host := ctx.String("host") - dir := ctx.String("dir") - api := ctx.String("api") + // If it already exists don't clobber it with the default. + if _, err := os.Lstat(workspace); !os.IsNotExist(err) { + msg := ` + The default Exercism workspace is - if err := c.Update(key, host, dir, api); err != nil { - log.Fatalf("Error updating your configuration %s\n", err) + %s + + There is already something there. + If it's a directory, that might be fine. + If it's a file, you will need to move it first, or choose a + different location for the workspace. + + You can choose the workspace location by rerunning this command + with the --workspace flag. + + %s configure %s --workspace=%s + ` + + return fmt.Errorf(msg, workspace, BinaryName, commandify(flags), workspace) + } } + // Configure the workspace. + cfg.Set("workspace", workspace) - if err := os.MkdirAll(c.Dir, os.ModePerm); err != nil { - log.Fatalf("Error creating exercism directory %s\n", err) + // Persist the new configuration. + if err := configuration.Save("user"); err != nil { + return err } + fmt.Fprintln(Err, "\nYou have configured the Exercism command-line client:") + printCurrentConfig(configuration) + return nil +} + +func printCurrentConfig(configuration config.Config) { + w := tabwriter.NewWriter(Err, 0, 0, 2, ' ', 0) + defer w.Flush() - if err := c.Write(); err != nil { - log.Fatal(err) + v := configuration.UserViperConfig + + fmt.Fprintln(w, "") + fmt.Fprintln(w, fmt.Sprintf("Config dir:\t\t%s", configuration.Dir)) + fmt.Fprintln(w, fmt.Sprintf("Token:\t(-t, --token)\t%s", v.GetString("token"))) + fmt.Fprintln(w, fmt.Sprintf("Workspace:\t(-w, --workspace)\t%s", v.GetString("workspace"))) + fmt.Fprintln(w, fmt.Sprintf("API Base URL:\t(-a, --api)\t%s", v.GetString("apibaseurl"))) + fmt.Fprintln(w, "") +} + +func commandify(flags *pflag.FlagSet) string { + var cmd string + fn := func(f *pflag.Flag) { + if f.Changed { + cmd = fmt.Sprintf("%s --%s=%s", cmd, f.Name, f.Value.String()) + } } + flags.VisitAll(fn) + return strings.TrimLeft(cmd, " ") +} + +func initConfigureCmd() { + viperConfig = viper.New() + setupConfigureFlags(configureCmd.Flags()) +} + +func setupConfigureFlags(flags *pflag.FlagSet) { + flags.StringP("token", "t", "", "authentication token used to connect to the site") + flags.StringP("workspace", "w", "", "directory for exercism exercises") + flags.StringP("api", "a", "", "API base url") + flags.BoolP("show", "s", false, "show the current configuration") + flags.BoolP("no-verify", "", false, "skip online token authorization check") +} + +func init() { + RootCmd.AddCommand(configureCmd) - fmt.Printf("\nConfiguration written to %s\n\n", c.File) - fmt.Printf(" --key=%s\n", c.APIKey) - fmt.Printf(" --dir=%s\n", c.Dir) - fmt.Printf(" --host=%s\n", c.API) - fmt.Printf(" --api=%s\n\n", c.XAPI) + initConfigureCmd() } diff --git a/cmd/configure_test.go b/cmd/configure_test.go new file mode 100644 index 000000000..df3542328 --- /dev/null +++ b/cmd/configure_test.go @@ -0,0 +1,420 @@ +//go:build !windows + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestBareConfigure(t *testing.T) { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + err := flags.Parse([]string{}) + assert.NoError(t, err) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = runConfigure(cfg, flags) + if assert.Error(t, err) { + assert.Regexp(t, "no token configured", err.Error()) + } +} + +func TestConfigureShow(t *testing.T) { + co := newCapturedOutput() + co.newErr = &bytes.Buffer{} + co.override() + defer co.reset() + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + v.Set("token", "configured-token") + v.Set("workspace", "configured-workspace") + v.Set("apibaseurl", "http://configured.example.com") + + // it will ignore any flags + args := []string{ + "--show", + "--api", "http://override.example.com", + "--token", "token-override", + "--workspace", "workspace-override", + } + err := flags.Parse(args) + assert.NoError(t, err) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + err = runConfigure(cfg, flags) + assert.NoError(t, err) + + assert.Regexp(t, "configured.example", Err) + assert.NotRegexp(t, "override.example", Err) + + assert.Regexp(t, "configured-token", Err) + assert.NotRegexp(t, "token-override", Err) + + assert.Regexp(t, "configured-workspace", Err) + assert.NotRegexp(t, "workspace-override", Err) +} + +func TestConfigureToken(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "existing-token", + args: []string{"--no-verify"}, + expected: "existing-token", + }, + { + desc: "It writes a token when passed as a flag", + configured: "", + args: []string{"--no-verify", "--token", "a-token"}, + expected: "a-token", + }, + { + desc: "It overwrites the token", + configured: "old-token", + args: []string{"--no-verify", "--token", "replacement-token"}, + expected: "replacement-token", + }, + { + desc: "It complains when token is neither configured nor passed", + configured: "", + args: []string{"--no-verify"}, + expected: "", + err: true, + message: "no token configured", + }, + { + desc: "It validates the existing token if we're not skipping validations", + configured: "configured-token", + args: []string{}, + expected: "configured-token", + err: true, + message: "token.*invalid", + }, + { + desc: "It validates the replacement token if we're not skipping validations", + configured: "", + args: []string{"--token", "invalid-token"}, + expected: "", + err: true, + message: "token.*invalid", + }, + } + + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/validate_token" { + w.WriteHeader(http.StatusUnauthorized) + } + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + for _, tc := range testCases { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + v.Set("token", tc.configured) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + } + + err = runConfigure(cfg, flags) + if err != nil || tc.err { + assert.Regexp(t, tc.message, err.Error(), tc.desc) + } + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("token"), tc.desc) + } +} + +func TestConfigureAPIBaseURL(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ping" { + w.WriteHeader(http.StatusNotFound) + } + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "http://example.com", + args: []string{"--no-verify"}, + expected: "http://example.com", + }, + { + desc: "It writes a base url when passed as a flag", + configured: "", + args: []string{"--no-verify", "--api", "http://api.example.com"}, + expected: "http://api.example.com", + }, + { + desc: "It overwrites the base url", + configured: "http://old.example.com", + args: []string{"--no-verify", "--api", "http://replacement.example.com"}, + expected: "http://replacement.example.com", + }, + { + desc: "It validates the existing base url if we're not skipping validations", + configured: ts.URL, + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" + expected: ts.URL, + err: true, + message: "API.*cannot be reached", + }, + { + desc: "It validates the replacement base URL if we're not skipping validations", + configured: "", + args: []string{"--api", ts.URL}, + expected: "", + err: true, + message: "API.*cannot be reached", + }, + } + + for _, tc := range testCases { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + v.Set("apibaseurl", tc.configured) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + } + + err = runConfigure(cfg, flags) + if err != nil || tc.err { + assert.Regexp(t, tc.message, err.Error(), tc.desc) + } + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("apibaseurl"), tc.desc) + } +} + +func TestConfigureWorkspace(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "/the-workspace", + args: []string{"--no-verify"}, + expected: "/the-workspace", + }, + { + desc: "It writes a workspace when passed as a flag", + configured: "", + args: []string{"--no-verify", "--workspace", "/new-workspace"}, + expected: "/new-workspace", + }, + { + desc: "It overwrites the configured workspace", + configured: "/configured-workspace", + args: []string{"--no-verify", "--workspace", "/replacement-workspace"}, + expected: "/replacement-workspace", + }, + { + desc: "It gets the default workspace when neither configured nor passed as a flag", + configured: "", + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" + expected: "/home/default-workspace", + }, + { + desc: "It resolves the passed workspace to expand ~", + configured: "", + args: []string{"--workspace", "~/workspace-dir"}, + expected: "/home/workspace-dir", + }, + + { + desc: "It resolves the configured workspace to expand ~", + configured: "~/configured-dir", + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" + expected: "/home/configured-dir", // The configuration object hard-codes the home directory below + }, + } + + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 200 OK by default. Ping and TokenAuth will both pass. + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + for _, tc := range testCases { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + v.Set("token", "abc123") // set a token so we get past the no token configured logic + v.Set("workspace", tc.configured) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + DefaultDirName: "default-workspace", + Home: "/home", + OS: "linux", + } + + err = runConfigure(cfg, flags) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("workspace"), tc.desc) + } +} + +func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // Stub server to always be 200 OK + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "no-clobber") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + cfg := config.Config{ + OS: "linux", + DefaultDirName: "workspace", + Home: tmpDir, + Dir: tmpDir, + DefaultBaseURL: ts.URL, + UserViperConfig: viper.New(), + Persister: config.InMemoryPersister{}, + } + + // Create a directory at the workspace directory's location + // so that it's already present. + err = os.MkdirAll(config.DefaultWorkspaceDir(cfg), os.FileMode(0755)) + assert.NoError(t, err) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + err = flags.Parse([]string{"--token", "abc123"}) + assert.NoError(t, err) + + err = runConfigure(cfg, flags) + if assert.Error(t, err) { + assert.Regexp(t, "already something", err.Error()) + } +} + +func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + tmpDir, err := os.MkdirTemp("", "no-clobber") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + + cfg := config.Config{ + OS: "linux", + DefaultDirName: "workspace", + Home: tmpDir, + Dir: tmpDir, + UserViperConfig: v, + Persister: config.InMemoryPersister{}, + } + + // Create a file at the workspace directory's location + err = os.WriteFile(filepath.Join(tmpDir, "workspace"), []byte("This is not a directory"), os.FileMode(0755)) + assert.NoError(t, err) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + err = flags.Parse([]string{"--no-verify", "--workspace", config.DefaultWorkspaceDir(cfg)}) + assert.NoError(t, err) + + err = runConfigure(cfg, flags) + if assert.Error(t, err) { + assert.Regexp(t, "set a different workspace", err.Error()) + } +} + +func TestCommandifyFlagSet(t *testing.T) { + flags := pflag.NewFlagSet("primitives", pflag.PanicOnError) + flags.StringP("word", "w", "", "a word") + flags.BoolP("yes", "y", false, "just do it") + flags.IntP("number", "n", 1, "count to one") + + err := flags.Parse([]string{"--word", "banana", "--yes"}) + assert.NoError(t, err) + assert.Equal(t, commandify(flags), "--word=banana --yes=true") +} diff --git a/cmd/debug.go b/cmd/debug.go deleted file mode 100644 index 49c8c3862..000000000 --- a/cmd/debug.go +++ /dev/null @@ -1,131 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "net/http" - "os" - "runtime" - "sync" - "time" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/config" - "github.com/exercism/cli/paths" -) - -type pingResult struct { - URL string - Service string - Status string - Latency time.Duration -} - -// Debug provides information about the user's environment and configuration. -func Debug(ctx *cli.Context) { - defer fmt.Printf("\nIf you are having trouble and need to file a GitHub issue (https://github.com/exercism/exercism.io/issues) please include this information (except your API key. Keep that private).\n") - - client := &http.Client{Timeout: 20 * time.Second} - - fmt.Printf("\n**** Debug Information ****\n") - fmt.Printf("Exercism CLI Version: %s\n", ctx.App.Version) - - rel, err := fetchLatestRelease(*client) - if err != nil { - log.Println("unable to fetch latest release: " + err.Error()) - } else { - if rel.Version() != ctx.App.Version { - defer fmt.Printf("\nA newer version of the CLI (%s) can be downloaded here: %s\n", rel.TagName, rel.Location) - } - fmt.Printf("Exercism CLI Latest Release: %s\n", rel.Version()) - } - - fmt.Printf("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH) - fmt.Printf("Build OS/Architecture %s/%s\n", BuildOS, BuildARCH) - if BuildARM != "" { - fmt.Printf("Build ARMv%s\n", BuildARM) - } - - fmt.Printf("Home Dir: %s\n", paths.Home) - - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - - configured := true - if _, err = os.Stat(c.File); err != nil { - if os.IsNotExist(err) { - configured = false - } else { - log.Fatal(err) - } - } - - if configured { - fmt.Printf("Config file: %s\n", c.File) - if c.APIKey != "" { - fmt.Printf("API Key: %s\n", c.APIKey) - } else { - fmt.Println("API Key: Please set your API Key to access all of the CLI features") - } - } else { - fmt.Println("Config file: ") - fmt.Println("API Key: Please set your API Key to access all of the CLI features") - } - fmt.Printf("Exercises Directory: %s\n", c.Dir) - - fmt.Println("Testing API endpoints reachability") - - endpoints := map[string]string{ - "API": c.API, - "XAPI": c.XAPI, - "GitHub API": "https://api.github.com/", - } - - var wg sync.WaitGroup - results := make(chan pingResult) - defer close(results) - - wg.Add(len(endpoints)) - - for service, url := range endpoints { - go func(service, url string) { - now := time.Now() - res, err := client.Get(url) - delta := time.Since(now) - if err != nil { - results <- pingResult{ - URL: url, - Service: service, - Status: err.Error(), - Latency: delta, - } - return - } - defer res.Body.Close() - - results <- pingResult{ - URL: url, - Service: service, - Status: "connected", - Latency: delta, - } - }(service, url) - } - - go func() { - for r := range results { - fmt.Printf( - "\t* %s: %s [%s] %s\n", - r.Service, - r.URL, - r.Status, - r.Latency, - ) - wg.Done() - } - }() - - wg.Wait() -} diff --git a/cmd/download.go b/cmd/download.go index df5f4d995..0e74c49ed 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -1,66 +1,357 @@ package cmd import ( + "bytes" + "encoding/json" + "errors" "fmt" - "io/ioutil" - "log" + "io" + "net/http" + netURL "net/url" "os" "path/filepath" + "regexp" "strings" - "github.com/codegangsta/cli" "github.com/exercism/cli/api" "github.com/exercism/cli/config" + "github.com/exercism/cli/workspace" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) -// Download returns specified iteration with its related problem. -func Download(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) +// downloadCmd represents the download command +var downloadCmd = &cobra.Command{ + Use: "download", + Aliases: []string{"d"}, + Short: "Download an exercise.", + Long: `Download an exercise. + +You may download an exercise to work on. If you've already +started working on it, the command will also download your +latest solution. + +Download other people's solutions by providing the UUID. +`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.NewConfig() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + cfg.UserViperConfig = v + + return runDownload(cfg, cmd.Flags(), args) + }, +} + +func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { + usrCfg := cfg.UserViperConfig + if err := validateUserConfig(usrCfg); err != nil { + return err + } + + download, err := newDownload(flags, usrCfg) + if err != nil { + return err + } + + metadata := download.payload.metadata() + dir := metadata.Exercise(usrCfg.GetString("workspace")).MetadataDir() + + if _, err = os.Stat(dir); !download.forceoverwrite && err == nil { + return fmt.Errorf("directory '%s' already exists, use --force to overwrite", dir) + } + + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return err + } + + if err := metadata.Write(dir); err != nil { + return err + } + + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { - log.Fatal(err) + return err + } + + for _, sf := range download.payload.files() { + url, err := sf.url() + if err != nil { + return err + } + + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return err + } + + res, err := client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + // TODO: deal with it + continue + } + + path := sf.relativePath() + dir := filepath.Join(metadata.Dir, filepath.Dir(path)) + if err = os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return err + } + + f, err := os.Create(filepath.Join(metadata.Dir, path)) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, res.Body) + if err != nil { + return err + } } - client := api.NewClient(c) + fmt.Fprintf(Err, "\nDownloaded to\n") + fmt.Fprintf(Out, "%s\n", metadata.Dir) + return nil +} + +type download struct { + // either/or + slug, uuid string - args := ctx.Args() - if len(args) != 1 { - msg := "Usage: exercism download SUBMISSION_ID" - log.Fatal(msg) + // user config + token, apibaseurl, workspace string + + // optional + track string + forceoverwrite bool + + payload *downloadPayload +} + +func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { + var err error + d := &download{} + d.uuid, err = flags.GetString("uuid") + if err != nil { + return nil, err + } + d.slug, err = flags.GetString("exercise") + if err != nil { + return nil, err + } + d.track, err = flags.GetString("track") + if err != nil { + return nil, err + } + if err != nil { + return nil, err } - submission, err := client.Download(args[0]) + d.forceoverwrite, err = flags.GetBool("force") if err != nil { - log.Fatal(err) + return nil, err } - path := filepath.Join(c.Dir, "solutions", submission.Username, submission.TrackID, submission.Slug, args[0]) + d.token = usrCfg.GetString("token") + d.apibaseurl = usrCfg.GetString("apibaseurl") + d.workspace = usrCfg.GetString("workspace") - if err := os.MkdirAll(path, 0755); err != nil { - log.Fatal(err) + if err = d.needsSlugXorUUID(); err != nil { + return nil, err + } + if err = d.needsUserConfigValues(); err != nil { + return nil, err + } + if err = d.needsSlugWhenGivenTrack(); err != nil { + return nil, err } - for name, contents := range submission.ProblemFiles { - if err := writeFile(fmt.Sprintf("%s/%s", path, name), contents); err != nil { - log.Fatalf("Unable to write file %s: %s", name, err) + client, err := api.NewClient(d.token, d.apibaseurl) + if err != nil { + return nil, err + } + + req, err := client.NewRequest("GET", d.url(), nil) + if err != nil { + return nil, err + } + d.buildQueryParams(req.URL) + + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode > 299 { + return nil, decodedAPIError(res) + } + + body, _ := io.ReadAll(res.Body) + res.Body = io.NopCloser(bytes.NewReader(body)) + + if err := json.Unmarshal(body, &d.payload); err != nil { + return nil, decodedAPIError(res) + } + + return d, nil +} + +func (d download) url() string { + id := "latest" + if d.uuid != "" { + id = d.uuid + } + return fmt.Sprintf("%s/solutions/%s", d.apibaseurl, id) +} + +func (d download) buildQueryParams(url *netURL.URL) { + query := url.Query() + if d.slug != "" { + query.Add("exercise_id", d.slug) + if d.track != "" { + query.Add("track_id", d.track) } } + url.RawQuery = query.Encode() +} - for name, contents := range submission.SolutionFiles { - filename := strings.TrimPrefix(name, strings.ToLower("/"+submission.TrackID+"/"+submission.Slug+"/")) - if err := writeFile(fmt.Sprintf("%s/%s", path, filename), contents); err != nil { - log.Fatalf("Unable to write file %s: %s", name, err) +// needsSlugXorUUID checks the presence of slug XOR uuid. +func (d download) needsSlugXorUUID() error { + if d.slug != "" && d.uuid != "" || d.uuid == d.slug { + return errors.New("need an --exercise name or a solution --uuid") + } + return nil +} + +// needsUserConfigValues checks the presence of required values from the user config. +func (d download) needsUserConfigValues() error { + errMsg := "missing required user config: '%s'" + if d.token == "" { + return fmt.Errorf(errMsg, "token") + } + if d.apibaseurl == "" { + return fmt.Errorf(errMsg, "apibaseurl") + } + if d.workspace == "" { + return fmt.Errorf(errMsg, "workspace") + } + return nil +} + +// needsSlugWhenGivenTrack ensures that track arguments are also given with a slug. +// (track meaningless when given a uuid). +func (d download) needsSlugWhenGivenTrack() error { + if d.track != "" && d.slug == "" { + return errors.New("--track or requires --exercise (not --uuid)") + } + return nil +} + +type downloadPayload struct { + Solution struct { + ID string `json:"id"` + URL string `json:"url"` + User struct { + Handle string `json:"handle"` + IsRequester bool `json:"is_requester"` + } `json:"user"` + Exercise struct { + ID string `json:"id"` + InstructionsURL string `json:"instructions_url"` + AutoApprove bool `json:"auto_approve"` + Track struct { + ID string `json:"id"` + Language string `json:"language"` + } `json:"track"` + } `json:"exercise"` + FileDownloadBaseURL string `json:"file_download_base_url"` + Files []string `json:"files"` + Iteration struct { + SubmittedAt *string `json:"submitted_at"` } + } `json:"solution"` + Error struct { + Type string `json:"type"` + Message string `json:"message"` + PossibleTrackIDs []string `json:"possible_track_ids"` + } `json:"error,omitempty"` +} + +func (dp downloadPayload) metadata() workspace.ExerciseMetadata { + return workspace.ExerciseMetadata{ + AutoApprove: dp.Solution.Exercise.AutoApprove, + Track: dp.Solution.Exercise.Track.ID, + ExerciseSlug: dp.Solution.Exercise.ID, + ID: dp.Solution.ID, + URL: dp.Solution.URL, + Handle: dp.Solution.User.Handle, + IsRequester: dp.Solution.User.IsRequester, } +} - fmt.Printf("Successfully downloaded submission.\n\nThe submission can be viewed at:\n %s\n\n", path) +func (dp downloadPayload) files() []solutionFile { + fx := make([]solutionFile, 0, len(dp.Solution.Files)) + for _, file := range dp.Solution.Files { + f := solutionFile{ + path: file, + baseURL: dp.Solution.FileDownloadBaseURL, + slug: dp.Solution.Exercise.ID, + } + fx = append(fx, f) + } + return fx +} +type solutionFile struct { + path, baseURL, slug string } -// writeFile writes the given contents to the given path, creating any necessary parent directories. -// This is useful because both problem files and solution files may have directory structures. -func writeFile(path, contents string) error { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - return err +func (sf solutionFile) url() (string, error) { + url, err := netURL.ParseRequestURI(fmt.Sprintf("%s%s", sf.baseURL, sf.path)) + + if err != nil { + return "", err + } + + return url.String(), nil +} + +func (sf solutionFile) relativePath() string { + file := sf.path + + // Work around a path bug due to an early design decision (later reversed) to + // allow numeric suffixes for exercise directories, letting people have + // multiple parallel versions of an exercise. + pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, sf.slug) + rgxNumericSuffix := regexp.MustCompile(pattern) + if rgxNumericSuffix.MatchString(sf.path) { + file = string(rgxNumericSuffix.ReplaceAll([]byte(sf.path), []byte(""))) } - return ioutil.WriteFile(path, []byte(contents), 0644) + + // Rewrite paths submitted with an older, buggy client where the Windows path is being treated as part of the filename. + file = strings.Replace(file, "\\", "/", -1) + + return filepath.FromSlash(file) +} + +func setupDownloadFlags(flags *pflag.FlagSet) { + flags.StringP("uuid", "u", "", "the solution UUID") + flags.StringP("track", "t", "", "the track ID") + flags.StringP("exercise", "e", "", "the exercise slug") + flags.BoolP("force", "F", false, "overwrite existing exercise directory") +} + +func init() { + RootCmd.AddCommand(downloadCmd) + setupDownloadFlags(downloadCmd.Flags()) } diff --git a/cmd/download_test.go b/cmd/download_test.go new file mode 100644 index 000000000..fb18cfc86 --- /dev/null +++ b/cmd/download_test.go @@ -0,0 +1,421 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "testing" + + "github.com/exercism/cli/config" + "github.com/exercism/cli/workspace" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestDownloadWithoutToken(t *testing.T) { + cfg := config.Config{ + UserViperConfig: viper.New(), + } + + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "Welcome to Exercism", err.Error()) + // It uses the default base API url to infer the host + assert.Regexp(t, "exercism.org/settings", err.Error()) + } +} + +func TestDownloadWithoutWorkspace(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + cfg := config.Config{ + UserViperConfig: v, + } + + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } +} + +func TestDownloadWithoutBaseURL(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", "/home/whatever") + cfg := config.Config{ + UserViperConfig: v, + } + + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } +} + +func TestDownloadWithoutFlags(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", "/home/username") + v.Set("apibaseurl", "http://example.com") + + cfg := config.Config{ + UserViperConfig: v, + } + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + + err := runDownload(cfg, flags, []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "need an --exercise name or a solution --uuid", err.Error()) + } +} + +func TestSolutionFile(t *testing.T) { + testCases := []struct { + name, file, expectedPath, expectedURL string + }{ + { + name: "filename with special character", + file: "special-char-filename#.txt", + expectedPath: "special-char-filename#.txt", + expectedURL: "http://www.example.com/special-char-filename%23.txt", + }, + { + name: "filename with leading slash", + file: "/with-leading-slash.txt", + expectedPath: fmt.Sprintf("%cwith-leading-slash.txt", os.PathSeparator), + expectedURL: "http://www.example.com//with-leading-slash.txt", + }, + { + name: "filename with leading backslash", + file: "\\with-leading-backslash.txt", + expectedPath: fmt.Sprintf("%cwith-leading-backslash.txt", os.PathSeparator), + expectedURL: "http://www.example.com/%5Cwith-leading-backslash.txt", + }, + { + name: "filename with backslashes in path", + file: "\\backslashes\\in-path.txt", + expectedPath: fmt.Sprintf("%[1]cbackslashes%[1]cin-path.txt", os.PathSeparator), + expectedURL: "http://www.example.com/%5Cbackslashes%5Cin-path.txt", + }, + { + name: "path with a numeric suffix", + file: "/bogus-exercise-12345/numeric.txt", + expectedPath: fmt.Sprintf("%[1]cbogus-exercise-12345%[1]cnumeric.txt", os.PathSeparator), + expectedURL: "http://www.example.com//bogus-exercise-12345/numeric.txt", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sf := solutionFile{ + path: tc.file, + baseURL: "http://www.example.com/", + } + + if sf.relativePath() != tc.expectedPath { + t.Fatalf("Expected path '%s', got '%s'", tc.expectedPath, sf.relativePath()) + } + + url, err := sf.url() + if err != nil { + t.Fatal(err) + } + + if url != tc.expectedURL { + t.Fatalf("Expected URL '%s', got '%s'", tc.expectedURL, url) + } + }) + } +} + +func TestDownload(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + requester bool + expectedDir string + flags map[string]string + }{ + { + requester: true, + expectedDir: "", + flags: map[string]string{"exercise": "bogus-exercise"}, + }, + { + requester: true, + expectedDir: "", + flags: map[string]string{"uuid": "bogus-id"}, + }, + { + requester: false, + expectedDir: filepath.Join("users", "alice"), + flags: map[string]string{"uuid": "bogus-id"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := os.MkdirTemp("", "download-cmd") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + ts := fakeDownloadServer(strconv.FormatBool(tc.requester)) + defer ts.Close() + + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Config{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + for name, value := range tc.flags { + flags.Set(name, value) + } + + err = runDownload(cfg, flags, []string{}) + assert.NoError(t, err) + + targetDir := filepath.Join(tmpDir, tc.expectedDir) + assertDownloadedCorrectFiles(t, targetDir) + + dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise") + b, err := os.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) + assert.NoError(t, err) + var metadata workspace.ExerciseMetadata + err = json.Unmarshal(b, &metadata) + assert.NoError(t, err) + + assert.Equal(t, "bogus-track", metadata.Track) + assert.Equal(t, "bogus-exercise", metadata.ExerciseSlug) + assert.Equal(t, tc.requester, metadata.IsRequester) + } +} + +func TestDownloadToExistingDirectory(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + exerciseDir string + flags map[string]string + }{ + { + exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := os.MkdirTemp("", "download-cmd") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) + assert.NoError(t, err) + + ts := fakeDownloadServer("true") + defer ts.Close() + + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Config{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + for name, value := range tc.flags { + flags.Set(name, value) + } + + err = runDownload(cfg, flags, []string{}) + + if assert.Error(t, err) { + assert.Regexp(t, "directory '.+' already exists", err.Error()) + } + } +} + +func TestDownloadToExistingDirectoryWithForce(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + exerciseDir string + flags map[string]string + }{ + { + exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := os.MkdirTemp("", "download-cmd") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) + assert.NoError(t, err) + + ts := fakeDownloadServer("true") + defer ts.Close() + + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Config{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + for name, value := range tc.flags { + flags.Set(name, value) + } + flags.Set("force", "true") + + err = runDownload(cfg, flags, []string{}) + assert.NoError(t, err) + } +} + +func fakeDownloadServer(requestor string) *httptest.Server { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + mux.HandleFunc("/file-1.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this is file 1") + }) + + mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this is file 2") + }) + + mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "") + }) + + mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") + fmt.Fprint(w, payloadBody) + }) + mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) { + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") + fmt.Fprint(w, payloadBody) + }) + + return server +} + +func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { + expectedFiles := []struct { + desc string + path string + contents string + }{ + { + desc: "a file in the exercise root directory", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-1.txt"), + contents: "this is file 1", + }, + { + desc: "a file in a subdirectory", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), + contents: "this is file 2", + }, + } + + for _, file := range expectedFiles { + t.Run(file.desc, func(t *testing.T) { + b, err := os.ReadFile(file.path) + assert.NoError(t, err) + assert.Equal(t, file.contents, string(b)) + }) + } + + path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt") + _, err := os.Lstat(path) + assert.NoError(t, err) +} + +func TestDownloadError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{"error": {"type": "error", "message": "test error"}}`) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-err-tmp-dir") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + flags.Set("uuid", "value") + + err = runDownload(cfg, flags, []string{}) + + assert.Equal(t, `expected response with Content-Type "application/json" but got status "400 Bad Request" with Content-Type "text/plain; charset=utf-8"`, err.Error()) + +} + +const payloadTemplate = ` +{ + "solution": { + "id": "bogus-id", + "user": { + "handle": "alice", + "is_requester": %s + }, + "exercise": { + "id": "bogus-exercise", + "instructions_url": "http://example.com/bogus-exercise", + "auto_approve": false, + "track": { + "id": "bogus-track", + "language": "Bogus Language" + } + }, + "file_download_base_url": "%s", + "files": [ + "file-1.txt", + "subdir/file-2.txt", + "file-3.txt" + ], + "iteration": { + "submitted_at": "2017-08-21t10:11:12.130z" + } + } +} +` diff --git a/cmd/fetch.go b/cmd/fetch.go deleted file mode 100644 index 6dc587571..000000000 --- a/cmd/fetch.go +++ /dev/null @@ -1,70 +0,0 @@ -package cmd - -import ( - "log" - "path/filepath" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" - "github.com/exercism/cli/user" -) - -// Fetch downloads exercism problems and writes them to disk. -func Fetch(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - client := api.NewClient(c) - - problems, err := client.Fetch(ctx.Args()) - if err != nil { - log.Fatal(err) - } - - submissionInfo, err := client.Submissions() - if err != nil { - log.Fatal(err) - } - - if err := setSubmissionState(problems, submissionInfo); err != nil { - log.Fatal(err) - } - - dirs, err := filepath.Glob(filepath.Join(c.Dir, "*")) - if err != nil { - log.Fatal(err) - } - - dirMap := make(map[string]bool) - for _, dir := range dirs { - dirMap[dir] = true - } - hw := user.NewHomework(problems, c) - - if len(ctx.Args()) == 0 { - if err := hw.RejectMissingTracks(dirMap); err != nil { - log.Fatal(err) - } - } - - if err := hw.Save(); err != nil { - log.Fatal(err) - } - - hw.Summarize(user.HWAll) -} - -func setSubmissionState(problems []*api.Problem, submissionInfo map[string][]api.SubmissionInfo) error { - for _, problem := range problems { - langSubmissions := submissionInfo[problem.TrackID] - for _, submission := range langSubmissions { - if submission.Slug == problem.Slug { - problem.Submitted = true - } - } - } - - return nil -} diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index ee25c04fc..000000000 --- a/cmd/list.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -const msgExplainFetch = "In order to fetch a specific assignment, call the fetch command with a specific assignment.\n\nexercism fetch %s %s\n\n" - -// List returns the full list of assignments for a given track. -func List(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - args := ctx.Args() - - if len(args) != 1 { - msg := "Usage: exercism list TRACK_ID" - log.Fatal(msg) - } - - trackID := args[0] - client := api.NewClient(c) - problems, err := client.List(trackID) - if err != nil { - if err == api.ErrUnknownTrack { - log.Fatalf("There is no track with ID '%s'.", trackID) - } - log.Fatal(err) - } - - for _, p := range problems { - fmt.Printf("%s\n", p) - } - fmt.Println() - fmt.Printf(msgExplainFetch, trackID, problems[0]) -} diff --git a/cmd/open.go b/cmd/open.go index 5b4e787e0..627eac639 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -1,58 +1,34 @@ package cmd import ( - "log" - "os/exec" - "runtime" - "strings" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" + "github.com/exercism/cli/browser" + "github.com/exercism/cli/workspace" + "github.com/spf13/cobra" ) -// Open uses the given track and problem and opens it in the browser. -func Open(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - client := api.NewClient(c) - - args := ctx.Args() - if len(args) != 2 { - msg := "Usage: exercism open TRACK_ID PROBLEM" - log.Fatal(msg) - } - - trackID := args[0] - slug := args[1] - submission, err := client.SubmissionURL(trackID, slug) - if err != nil { - log.Fatal(err) - } - - url := submission.URL - // Escape characters are not allowed by cmd/bash. - switch runtime.GOOS { - case "windows": - url = strings.Replace(url, "&", `^&`, -1) - default: - url = strings.Replace(url, "&", `\&`, -1) - } - - // The command to open the browser is OS-dependent. - var cmd *exec.Cmd - switch runtime.GOOS { - case "darwin": - cmd = exec.Command("open", url) - case "freebsd", "linux", "netbsd", "openbsd": - cmd = exec.Command("xdg-open", url) - case "windows": - cmd = exec.Command("cmd", "/c", "start", url) - } +// openCmd opens the designated exercise in the browser. +var openCmd = &cobra.Command{ + Use: "open", + Aliases: []string{"o"}, + Short: "Open an exercise on the website.", + Long: `Open the specified exercise to the solution page on the Exercism website. + +Pass the path to the directory that contains the solution you want to see on the website. + `, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := "." + if len(args) == 1 { + path = args[0] + } + metadata, err := workspace.NewExerciseMetadata(path) + if err != nil { + return err + } + return browser.Open(metadata.URL) + }, +} - if err := cmd.Run(); err != nil { - log.Fatal(err) - } +func init() { + RootCmd.AddCommand(openCmd) } diff --git a/cmd/prepare.go b/cmd/prepare.go new file mode 100644 index 000000000..6e6a13811 --- /dev/null +++ b/cmd/prepare.go @@ -0,0 +1,19 @@ +package cmd + +import "github.com/spf13/cobra" + +// prepareCmd does necessary setup for Exercism and its tracks. +var prepareCmd = &cobra.Command{ + Use: "prepare", + Aliases: []string{"p"}, + Short: "Prepare does setup for Exercism and its tracks.", + Long: `Prepare downloads settings and dependencies for Exercism and the language tracks. + `, + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, +} + +func init() { + RootCmd.AddCommand(prepareCmd) +} diff --git a/cmd/release_utils.go b/cmd/release_utils.go deleted file mode 100644 index 3b09cea0b..000000000 --- a/cmd/release_utils.go +++ /dev/null @@ -1,154 +0,0 @@ -package cmd - -import ( - "archive/tar" - "archive/zip" - "bytes" - "compress/gzip" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "strings" -) - -var ( - osMap = map[string]string{ - "darwin": "mac", - "linux": "linux", - "windows": "windows", - } - - archMap = map[string]string{ - "amd64": "64bit", - "386": "32bit", - "arm": "arm", - } -) - -type asset struct { - ID int `json:"id"` - Name string `json:"name"` - ContentType string `json:"content_type"` -} - -func (a *asset) download() (*bytes.Reader, error) { - downloadURL := fmt.Sprintf("https://api.github.com/repos/exercism/cli/releases/assets/%d", a.ID) - req, err := http.NewRequest("GET", downloadURL, nil) - if err != nil { - return nil, err - } - // https://developer.github.com/v3/repos/releases/#get-a-single-release-asset - req.Header.Set("Accept", "application/octet-stream") - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - bs, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, err - } - - return bytes.NewReader(bs), nil -} - -type release struct { - Location string `json:"html_url"` - TagName string `json:"tag_name"` - Assets []asset `json:"assets"` -} - -func (r *release) Version() string { - return strings.TrimPrefix(r.TagName, "v") -} - -const installFlag = os.O_RDWR | os.O_CREATE | os.O_TRUNC - -func installTgz(source *bytes.Reader, dest string) error { - gr, err := gzip.NewReader(source) - if err != nil { - return err - } - defer gr.Close() - - tr := tar.NewReader(gr) - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - // Move the old version to a backup path that we can recover from - // in case the upgrade fails - destBackup := dest + ".bak" - if _, err := os.Stat(dest); err == nil { - os.Rename(dest, destBackup) - } - - fileCopy, err := os.OpenFile(dest, installFlag, hdr.FileInfo().Mode()) - if err != nil { - os.Rename(destBackup, dest) - return err - } - defer fileCopy.Close() - - if _, err = io.Copy(fileCopy, tr); err != nil { - os.Rename(destBackup, dest) - return err - } else { - os.Remove(destBackup) - } - } - - return nil -} - -func installZip(source *bytes.Reader, dest string) error { - zr, err := zip.NewReader(source, int64(source.Len())) - if err != nil { - return err - } - - for _, f := range zr.File { - fileCopy, err := os.OpenFile(dest, installFlag, f.Mode()) - if err != nil { - return err - } - defer fileCopy.Close() - - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - _, err = io.Copy(fileCopy, rc) - if err != nil { - return err - } - } - - return nil -} - -func fetchLatestRelease(client http.Client) (*release, error) { - resp, err := client.Get("https://api.github.com/repos/exercism/cli/releases/latest") - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var rel release - if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil { - return nil, err - } - - return &rel, nil -} diff --git a/cmd/restore.go b/cmd/restore.go deleted file mode 100644 index 89453ac9c..000000000 --- a/cmd/restore.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "log" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" - "github.com/exercism/cli/user" -) - -// Restore returns a user's solved problems. -func Restore(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - - client := api.NewClient(c) - - problems, err := client.Restore() - if err != nil { - log.Fatal(err) - } - - hw := user.NewHomework(problems, c) - if err := hw.Save(); err != nil { - log.Fatal(err) - } - hw.Summarize(user.HWNotSubmitted) -} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 000000000..d94d5c847 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "fmt" + "os" + "runtime" + + "github.com/exercism/cli/api" + "github.com/exercism/cli/cli" + "github.com/exercism/cli/config" + "github.com/exercism/cli/debug" + "github.com/spf13/cobra" +) + +// RootCmd represents the base command when called without any subcommands. +var RootCmd = &cobra.Command{ + Use: getCommandName(), + Short: "A friendly command-line interface to Exercism.", + Long: `A command-line interface for Exercism. + +Download exercises and submit your solutions.`, + SilenceUsage: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if verbose, _ := cmd.Flags().GetBool("verbose"); verbose { + debug.Verbose = verbose + } + if unmask, _ := cmd.Flags().GetBool("unmask-token"); unmask { + debug.UnmaskAPIKey = unmask + } + if timeout, _ := cmd.Flags().GetInt("timeout"); timeout > 0 { + cli.TimeoutInSeconds = timeout + api.TimeoutInSeconds = timeout + } + }, +} + +// Execute adds all child commands to the root command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + os.Exit(-1) + } +} + +func getCommandName() string { + return os.Args[0] +} + +func init() { + BinaryName = getCommandName() + config.SetDefaultDirName(BinaryName) + Out = os.Stdout + Err = os.Stderr + api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) + RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") + RootCmd.PersistentFlags().IntP("timeout", "", 0, "override the default HTTP timeout (seconds)") + RootCmd.PersistentFlags().BoolP("unmask-token", "", false, "will unmask the API during a request/response dump") +} diff --git a/cmd/skip.go b/cmd/skip.go deleted file mode 100644 index 2af5ca08a..000000000 --- a/cmd/skip.go +++ /dev/null @@ -1,36 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -// Skip allows a user to skip a specific problem. -func Skip(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - args := ctx.Args() - - if len(args) != 2 { - msg := "Usage: exercism skip TRACK_ID PROBLEM" - log.Fatal(msg) - } - - var ( - trackID = args[0] - slug = args[1] - ) - - client := api.NewClient(c) - if err := client.Skip(trackID, slug); err != nil { - log.Fatal(err) - } - - fmt.Printf("Exercise %q in %q has been skipped.\n", slug, trackID) -} diff --git a/cmd/status.go b/cmd/status.go deleted file mode 100644 index 9c6584e19..000000000 --- a/cmd/status.go +++ /dev/null @@ -1,37 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -// Status is a command that allows a user to view their progress in a given -// language track. -func Status(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - args := ctx.Args() - - if len(args) != 1 { - log.Fatal("Usage: exercism status TRACK_ID") - } - - client := api.NewClient(c) - trackID := args[0] - status, err := client.Status(trackID) - if err != nil { - if err == api.ErrUnknownTrack { - log.Fatalf("There is no track with ID '%s'.", trackID) - } else { - log.Fatal(err) - } - } - - fmt.Println(status) -} diff --git a/cmd/submit.go b/cmd/submit.go index 08c5219b4..3135f65ae 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -1,103 +1,467 @@ package cmd import ( + "bytes" + "errors" "fmt" - "log" + "io" + "mime/multipart" "os" "path/filepath" - "github.com/codegangsta/cli" "github.com/exercism/cli/api" "github.com/exercism/cli/config" - "github.com/exercism/cli/paths" + "github.com/exercism/cli/workspace" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) -// Submit posts an iteration to the API. -func Submit(ctx *cli.Context) { - if len(ctx.Args()) == 0 { - log.Fatal("Please enter a file name") +// submitCmd lets people upload a solution to the website. +var submitCmd = &cobra.Command{ + Use: "submit [ ...]", + Aliases: []string{"s"}, + Short: "Submit your solution to an exercise.", + Long: `Submit your solution to an Exercism exercise. + + Call the command with the list of files you want to submit. + If you omit the list of files, the CLI will submit the + default solution files for the exercise. +`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.NewConfig() + + usrCfg := viper.New() + usrCfg.AddConfigPath(cfg.Dir) + usrCfg.SetConfigName("user") + usrCfg.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = usrCfg.ReadInConfig() + cfg.UserViperConfig = usrCfg + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("cli") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + if len(args) == 0 { + files, err := getExerciseSolutionFiles(".") + if err != nil { + return err + } + args = files + } + + return runSubmit(cfg, cmd.Flags(), args) + }, +} + +func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { + if err := validateUserConfig(cfg.UserViperConfig); err != nil { + return err + } + + ctx := newSubmitCmdContext(cfg.UserViperConfig, flags) + + if err := ctx.validator.filesExistAndNotADir(args); err != nil { + return err + } + + submitPaths, err := ctx.evaluatedSymlinks(args) + if err != nil { + return err + } + + submitPaths = ctx.removeDuplicatePaths(submitPaths) + + if err = ctx.validator.filesBelongToSameExercise(submitPaths); err != nil { + return err + } + + exercise, err := ctx.exercise(submitPaths[0]) + if err != nil { + return err + } + + if err = ctx.migrateLegacyMetadata(exercise); err != nil { + return err + } + + if err = ctx.validator.fileSizesWithinMax(submitPaths); err != nil { + return err + } + + documents, err := ctx.documents(submitPaths, exercise) + if err != nil { + return err } - c, err := config.New(ctx.GlobalString("config")) + if err = ctx.validator.submissionNotEmpty(documents); err != nil { + return err + } + + metadata, err := ctx.metadata(exercise) if err != nil { - log.Fatal(err) + return err } - if ctx.GlobalBool("verbose") { - log.Printf("Exercises dir: %s", c.Dir) - dir, err := os.Getwd() + if err := ctx.validator.metadataMatchesExercise(metadata, exercise); err != nil { + return err + } + + if err := ctx.validator.isRequestor(metadata); err != nil { + return err + } + + if err := ctx.submit(metadata, documents); err != nil { + return err + } + + ctx.printResult(metadata) + return nil +} + +func getExerciseSolutionFiles(baseDir string) ([]string, error) { + v := viper.New() + v.AddConfigPath(filepath.Join(baseDir, ".exercism")) + v.SetConfigName("config") + v.SetConfigType("json") + err := v.ReadInConfig() + if err != nil { + return nil, errors.New("no files to submit") + } + solutionFiles := v.GetStringSlice("files.solution") + if len(solutionFiles) == 0 { + return nil, errors.New("no files to submit") + } + + return solutionFiles, nil +} + +type submitCmdContext struct { + usrCfg *viper.Viper + flags *pflag.FlagSet + validator submitValidator +} + +func newSubmitCmdContext(usrCfg *viper.Viper, flags *pflag.FlagSet) *submitCmdContext { + return &submitCmdContext{ + usrCfg: usrCfg, + flags: flags, + validator: submitValidator{usrCfg: usrCfg}, + } +} + +// evaluatedSymlinks returns the submit paths with evaluated symlinks. +func (s *submitCmdContext) evaluatedSymlinks(submitPaths []string) ([]string, error) { + evalSymlinkSubmitPaths := make([]string, 0, len(submitPaths)) + for _, path := range submitPaths { + var err error + path, err = filepath.Abs(path) if err != nil { - log.Printf("Unable to get current working directory - %s", err) - } else { - log.Printf("Current dir: %s", dir) + return nil, err } + + src, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + evalSymlinkSubmitPaths = append(evalSymlinkSubmitPaths, src) } + return evalSymlinkSubmitPaths, nil +} - if !c.IsAuthenticated() { - log.Fatal(msgPleaseAuthenticate) +func (s *submitCmdContext) removeDuplicatePaths(submitPaths []string) []string { + seen := make(map[string]bool) + result := make([]string, 0, len(submitPaths)) + + for _, val := range submitPaths { + if _, ok := seen[val]; !ok { + seen[val] = true + result = append(result, val) + } } - dir, err := filepath.EvalSymlinks(c.Dir) + return result +} + +// exercise creates an exercise using one of the submitted filepaths. +// This assumes prior verification that submit paths belong to the same exercise. +func (s *submitCmdContext) exercise(aSubmitPath string) (workspace.Exercise, error) { + ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { - log.Fatal(err) + return workspace.Exercise{}, err } - if ctx.GlobalBool("verbose") { - log.Printf("eval symlinks (dir): %s", dir) + dir, err := ws.ExerciseDir(aSubmitPath) + if err != nil { + return workspace.Exercise{}, err } + return workspace.NewExerciseFromDir(dir), nil +} + +func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) error { + migrationStatus, err := exercise.MigrateLegacyMetadataFile() + if err != nil { + return err + } + if verbose, _ := s.flags.GetBool("verbose"); verbose { + fmt.Fprintf(Err, migrationStatus.String()) + } + return nil +} - files := []string{} - for _, filename := range ctx.Args() { - if ctx.GlobalBool("verbose") { - log.Printf("file name: %s", filename) +// documents builds the documents that get submitted. +// Empty files are skipped, printing a warning. +func (s *submitCmdContext) documents(submitPaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(submitPaths)) + for _, file := range submitPaths { + // Don't submit empty files + info, err := os.Stat(file) + if err != nil { + return nil, err } + if info.Size() == 0 { + + msg := ` - if isTest(filename) && !ctx.Bool("test") { - log.Fatal("You're trying to submit a test file. If this is really what " + - "you want, please pass the --test flag to exercism submit.") + WARNING: Skipping empty file + %s + + ` + fmt.Fprintf(Err, msg, file) + continue + } + doc, err := workspace.NewDocument(exercise.Filepath(), file) + if err != nil { + return nil, err } + docs = append(docs, doc) + } + return docs, nil +} - if isREADME(filename) { - log.Fatal("You cannot submit the README as a solution.") +func (s *submitCmdContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) + if err != nil { + return nil, err + } + return metadata, nil +} + +// submit submits the documents to the Exercism API. +func (s *submitCmdContext) submit(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + for _, doc := range docs { + file, err := os.Open(doc.Filepath()) + if err != nil { + return err + } + defer file.Close() + + part, err := writer.CreateFormFile("files[]", doc.Path()) + if err != nil { + return err + } + _, err = io.Copy(part, file) + if err != nil { + return err + } + } + if err := writer.Close(); err != nil { + return err + } + + client, err := api.NewClient(s.usrCfg.GetString("token"), s.usrCfg.GetString("apibaseurl")) + if err != nil { + return err + } + url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), metadata.ID) + req, err := client.NewRequest("PATCH", url, body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return decodedAPIError(resp) + } + + bb := &bytes.Buffer{} + _, err = bb.ReadFrom(resp.Body) + if err != nil { + return err + } + return nil +} + +func (s *submitCmdContext) printResult(metadata *workspace.ExerciseMetadata) { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if metadata.AutoApprove { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", metadata.URL) +} + +// submitValidator contains the validation rules for a submission. +type submitValidator struct { + usrCfg *viper.Viper +} + +// filesExistAndNotADir checks that each file exists and is not a directory. +func (s submitValidator) filesExistAndNotADir(submitPaths []string) error { + for _, path := range submitPaths { + path, err := filepath.Abs(path) + if err != nil { + return err } - if paths.IsDir(filename) { - log.Fatal("Please specify each file that should be submitted, e.g. `exercism submit file1 file2 file3`.") + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + msg := ` + + The file you are trying to submit cannot be found. + + %s + + ` + return fmt.Errorf(msg, path) + } + return err + } + if info.IsDir() { + msg := ` + + You are submitting a directory, which is not currently supported. + + %s + + Please change into the directory and provide the path to the file(s) you wish to submit + + %s submit FILENAME + + ` + return fmt.Errorf(msg, path, BinaryName) } + } + return nil +} - file, err := filepath.Abs(filename) +// filesBelongToSameExercise checks that each file belongs to the same exercise. +func (s submitValidator) filesBelongToSameExercise(submitPaths []string) error { + ws, err := workspace.New(s.usrCfg.GetString("workspace")) + if err != nil { + return err + } + + var exerciseDir string + for _, f := range submitPaths { + dir, err := ws.ExerciseDir(f) if err != nil { - log.Fatal(err) + if workspace.IsMissingMetadata(err) { + return errors.New(msgMissingMetadata) + } + return err } + if exerciseDir != "" && dir != exerciseDir { + msg := ` + + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. - if ctx.GlobalBool("verbose") { - log.Printf("absolute path: %s", file) + ` + return errors.New(msg) } + exerciseDir = dir + } + return nil +} - file, err = filepath.EvalSymlinks(file) +// fileSizesWithinMax checks that each file does not exceed the max allowed size. +func (s submitValidator) fileSizesWithinMax(submitPaths []string) error { + for _, file := range submitPaths { + info, err := os.Stat(file) if err != nil { - log.Fatal(err) + return err } + const maxFileSize int64 = 65535 + if info.Size() >= maxFileSize { + msg := ` - if ctx.GlobalBool("verbose") { - log.Printf("eval symlinks (file): %s", file) + The submitted file '%s' is larger than the max allowed file size of %d bytes. + Please reduce the size of the file and try again. + + ` + return fmt.Errorf(msg, file, maxFileSize) } + } + return nil +} + +// submissionNotEmpty checks that there is at least one file to submit. +func (s submitValidator) submissionNotEmpty(docs []workspace.Document) error { + if len(docs) == 0 { + msg := ` + + No files found to submit. - files = append(files, file) + ` + return errors.New(msg) } + return nil +} - iteration, err := api.NewIteration(dir, files) - if err != nil { - log.Fatalf("Unable to submit - %s", err) +// metadataMatchesExercise checks that the metadata refers to the exercise being submitted. +func (s submitValidator) metadataMatchesExercise(metadata *workspace.ExerciseMetadata, exercise workspace.Exercise) error { + if metadata.ExerciseSlug != exercise.Slug { + // TODO: error msg should suggest running future doctor command + msg := ` + + The exercise directory does not match exercise slug in metadata: + + expected '%[1]s' but got '%[2]s' + + Please rename the directory '%[1]s' to '%[2]s' and try again. + + ` + return fmt.Errorf(msg, exercise.Slug, metadata.ExerciseSlug) } - iteration.Key = c.APIKey - iteration.Comment = ctx.String("comment") + return nil +} - client := api.NewClient(c) - submission, err := client.Submit(iteration) - if err != nil { - log.Fatal(err) +// isRequestor checks that the submission requestor is listed as the author in the metadata. +func (s submitValidator) isRequestor(metadata *workspace.ExerciseMetadata) error { + if !metadata.IsRequester { + msg := ` + + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. + + %s download --exercise=%s --track=%s + + ` + return fmt.Errorf(msg, BinaryName, metadata.ExerciseSlug, metadata.Track) } + return nil +} - fmt.Printf("%s - %s\n%s\n\n", submission.Language, submission.Name, submission.URL) +func init() { + RootCmd.AddCommand(submitCmd) } diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go new file mode 100644 index 000000000..093895fb3 --- /dev/null +++ b/cmd/submit_symlink_test.go @@ -0,0 +1,62 @@ +//go:build !windows + +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestSubmitFilesInSymlinkedPath(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "symlink-destination") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + dstDir := filepath.Join(tmpDir, "workspace") + + srcDir, err := os.MkdirTemp("", "symlink-source") + defer os.RemoveAll(srcDir) + assert.NoError(t, err) + + err = os.Symlink(srcDir, dstDir) + assert.NoError(t, err) + + dir := filepath.Join(dstDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", dstDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file := filepath.Join(dir, "file.txt") + err = os.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("symlinks", pflag.PanicOnError), []string{file}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) +} diff --git a/cmd/submit_test.go b/cmd/submit_test.go new file mode 100644 index 000000000..38caf9d93 --- /dev/null +++ b/cmd/submit_test.go @@ -0,0 +1,741 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/exercism/cli/workspace" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestSubmitWithoutToken(t *testing.T) { + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: viper.New(), + } + + err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "Welcome to Exercism", err.Error()) + assert.Regexp(t, "exercism.org/settings", err.Error()) + } +} + +func TestSubmitWithoutWorkspace(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } +} + +func TestSubmitNonExistentFile(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "submit-no-such-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = os.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + err = os.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + assert.NoError(t, err) + files := []string{ + filepath.Join(tmpDir, "file-1.txt"), + "no-such-file.txt", + filepath.Join(tmpDir, "file-2.txt"), + } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + if assert.Error(t, err) { + assert.Regexp(t, "cannot be found", err.Error()) + } +} + +func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "no-metadata-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + file := filepath.Join(dir, "file.txt") + err = os.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + if assert.Error(t, err) { + assert.Regexp(t, "doesn't have the necessary metadata", err.Error()) + } +} + +func TestGetExerciseSolutionFiles(t *testing.T) { + + tmpDir, err := os.MkdirTemp("", "dir-with-no-metadata") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + _, err = getExerciseSolutionFiles(tmpDir) + if assert.Error(t, err) { + assert.Regexp(t, "no files to submit", err.Error()) + } + + validTmpDir, err := os.MkdirTemp("", "dir-with-valid-metadata") + defer os.RemoveAll(validTmpDir) + assert.NoError(t, err) + + metadataDir := filepath.Join(validTmpDir, ".exercism") + err = os.MkdirAll(metadataDir, os.FileMode(0755)) + assert.NoError(t, err) + + err = os.WriteFile( + filepath.Join(metadataDir, "config.json"), + []byte(` +{ + "files": { + "solution": [ + "expenses.go" + ] + } +} +`), os.FileMode(0755)) + assert.NoError(t, err) + + files, err := getExerciseSolutionFiles(validTmpDir) + assert.NoError(t, err) + if assert.Equal(t, len(files), 1) { + assert.Equal(t, files[0], "expenses.go") + } +} + +func TestSubmitFilesAndDir(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "submit-no-such-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = os.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + err = os.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + assert.NoError(t, err) + files := []string{ + filepath.Join(tmpDir, "file-1.txt"), + tmpDir, + filepath.Join(tmpDir, "file-2.txt"), + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + if assert.Error(t, err) { + assert.Regexp(t, "submitting a directory", err.Error()) + assert.Regexp(t, "Please change into the directory and provide the path to the file\\(s\\) you wish to submit", err.Error()) + } +} + +func TestDuplicateFiles(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "duplicate-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file1 := filepath.Join(dir, "file-1.txt") + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file1}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"]) +} + +func TestSubmitFiles(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + Err = &bytes.Buffer{} + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + file1 := filepath.Join(dir, "file-1.txt") + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + file2 := filepath.Join(dir, "subdir", "file-2.txt") + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + assert.NoError(t, err) + + // We don't filter *.md files if you explicitly pass the file path. + readme := filepath.Join(dir, "README.md") + err = os.WriteFile(readme, []byte("This is the readme."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + files := []string{ + file1, file2, readme, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + + if assert.NoError(t, err) { + assert.Equal(t, 3, len(submittedFiles)) + assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"]) + assert.Equal(t, "This is file 2.", submittedFiles["subdir/file-2.txt"]) + assert.Equal(t, "This is the readme.", submittedFiles["README.md"]) + assert.Regexp(t, "submitted successfully", Err) + } +} + +func TestLegacyMetadataMigration(t *testing.T) { + co := newCapturedOutput() + co.newErr = &bytes.Buffer{} + co.override() + defer co.reset() + + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "legacy-metadata-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + metadata := &workspace.ExerciseMetadata{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + ExerciseSlug: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: true, + } + b, err := json.Marshal(metadata) + assert.NoError(t, err) + exercise := workspace.NewExerciseFromDir(dir) + err = os.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) + assert.NoError(t, err) + + file := filepath.Join(dir, "file.txt") + err = os.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + ok, _ := exercise.HasLegacyMetadata() + assert.True(t, ok) + ok, _ = exercise.HasMetadata() + assert.False(t, ok) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + flags.Bool("verbose", true, "") + + err = runSubmit(cfg, flags, []string{file}) + assert.NoError(t, err) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) + + ok, _ = exercise.HasLegacyMetadata() + assert.False(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + assert.Regexp(t, "Migrated metadata", Err) +} + +func TestSubmitWithEmptyFile(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "empty-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file1 := filepath.Join(dir, "file-1.txt") + err = os.WriteFile(file1, []byte(""), os.FileMode(0755)) + file2 := filepath.Join(dir, "file-2.txt") + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is file 2.", submittedFiles["file-2.txt"]) +} + +func TestSubmitWithEnormousFile(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "enormous-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file := filepath.Join(dir, "file.txt") + err = os.WriteFile(file, make([]byte, 65535), os.FileMode(0755)) + if err != nil { + t.Fatal(err) + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + + if assert.Error(t, err) { + assert.Regexp(t, "Please reduce the size of the file and try again.", err.Error()) + } +} + +func TestSubmitOnlyEmptyFile(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + tmpDir, err := os.MkdirTemp("", "just-an-empty-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file := filepath.Join(dir, "file.txt") + err = os.WriteFile(file, []byte(""), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + if assert.Error(t, err) { + assert.Regexp(t, "No files found", err.Error()) + } +} + +func TestSubmitFilesFromDifferentSolutions(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "dir-1-submit") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir1 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-1") + os.MkdirAll(dir1, os.FileMode(0755)) + writeFakeMetadata(t, dir1, "bogus-track", "bogus-exercise-1") + + dir2 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-2") + os.MkdirAll(dir2, os.FileMode(0755)) + writeFakeMetadata(t, dir2, "bogus-track", "bogus-exercise-2") + + file1 := filepath.Join(dir1, "file-1.txt") + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + file2 := filepath.Join(dir2, "file-2.txt") + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) + if assert.Error(t, err) { + assert.Regexp(t, "different solutions", err.Error()) + } +} + +func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest.Server { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(2 << 10) + if err != nil { + t.Fatal(err) + } + mf := r.MultipartForm + + files := mf.File["files[]"] + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + t.Fatal(err) + } + defer file.Close() + body, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + // Following RFC 7578, Go 1.17+ strips the directory information in fileHeader.Filename. + // Validating the submitted files directory tree is important so Content-Disposition is used for + // obtaining the unmodified filename. + v := fileHeader.Header.Get("Content-Disposition") + _, dispositionParams, err := mime.ParseMediaType(v) + if err != nil { + t.Fatalf("failed to obtain submitted filename from multipart header: %s", err.Error()) + } + filename := dispositionParams["filename"] + submittedFiles[filename] = string(body) + } + + fmt.Fprint(w, "{}") + }) + return httptest.NewServer(handler) +} + +func TestSubmitRelativePath(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "relative-path") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + err = os.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + + err = os.Chdir(dir) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) +} + +func TestSubmitServerErr(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{"error": {"type": "error", "message": "test error"}}`) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-err-tmp-dir") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + err = os.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + files := []string{ + filepath.Join(dir, "file-1.txt"), + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + + assert.Regexp(t, `expected response with Content-Type "application/json" but got status "400 Bad Request" with Content-Type "text/plain; charset=utf-8"`, err.Error()) +} + +func TestHandleErrorResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-nonsuccess") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + err = os.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + files := []string{ + filepath.Join(dir, "file-1.txt"), + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + assert.Error(t, err) +} + +func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + + metadata := &workspace.ExerciseMetadata{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + ExerciseSlug: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: false, + } + err = metadata.Write(dir) + assert.NoError(t, err) + + file1 := filepath.Join(dir, "file-1.txt") + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1}) + if assert.Error(t, err) { + assert.Regexp(t, "not connected to your account", err.Error()) + } +} + +func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := os.MkdirTemp("", "submit-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-doesnt-match-metadata-slug") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + file1 := filepath.Join(dir, "file-1.txt") + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1}) + if assert.Error(t, err) { + assert.Regexp(t, "directory does not match exercise slug", err.Error()) + } +} + +func writeFakeMetadata(t *testing.T, dir, trackID, exerciseSlug string) { + metadata := &workspace.ExerciseMetadata{ + ID: "bogus-solution-uuid", + Track: trackID, + ExerciseSlug: exerciseSlug, + URL: "http://example.com/bogus-url", + IsRequester: true, + } + err := metadata.Write(dir) + assert.NoError(t, err) +} diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 000000000..8f5b54e35 --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/exercism/cli/workspace" + "github.com/spf13/cobra" +) + +var testCmd = &cobra.Command{ + Use: "test", + Aliases: []string{"t"}, + Short: "Run the exercise's tests.", + Long: `Run the exercise's tests. + + Run this command in an exercise's root directory.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runTest(args) + }, +} + +func runTest(args []string) error { + track, err := getTrack() + if err != nil { + return err + } + + testConf, ok := workspace.TestConfigurations[track] + + if !ok { + return fmt.Errorf("the \"%s\" track does not yet support running tests using the Exercism CLI. Please see HELP.md for testing instructions", track) + } + + command, err := testConf.GetTestCommand() + if err != nil { + return err + } + cmdParts := strings.Split(command, " ") + + // pass args/flags to this command down to the test handler + if len(args) > 0 { + cmdParts = append(cmdParts, args...) + } + + fmt.Printf("Running tests via `%s`\n\n", strings.Join(cmdParts, " ")) + exerciseTestCmd := exec.Command(cmdParts[0], cmdParts[1:]...) + + // pipe output directly out, preserving any color + exerciseTestCmd.Stdout = os.Stdout + exerciseTestCmd.Stderr = os.Stderr + + err = exerciseTestCmd.Run() + if err != nil { + // unclear what other errors would pop up here, but it pays to be defensive + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode := exitErr.ExitCode() + // if subcommand returned a non-zero exit code, exit with the same + os.Exit(exitCode) + } else { + log.Fatalf("Failed to get error from failed subcommand: %v", err) + } + } + return nil +} + +func getTrack() (string, error) { + metadata, err := workspace.NewExerciseMetadata(".") + if err != nil { + return "", err + } + if metadata.Track == "" { + return "", fmt.Errorf("no track found in exercise metadata") + } + + return metadata.Track, nil +} + +func init() { + RootCmd.AddCommand(testCmd) +} diff --git a/cmd/tracks.go b/cmd/tracks.go deleted file mode 100644 index 90fa59025..000000000 --- a/cmd/tracks.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" - "github.com/exercism/cli/user" -) - -// Tracks lists available tracks. -func Tracks(ctx *cli.Context) { - c, err := config.New(ctx.GlobalString("config")) - if err != nil { - log.Fatal(err) - } - client := api.NewClient(c) - - tracks, err := client.Tracks() - if err != nil { - log.Fatal(err) - } - - curr := user.NewCurriculum(tracks) - fmt.Println("\nActive language tracks:") - curr.Report(user.TrackActive) - fmt.Println("\nInactive language tracks:") - curr.Report(user.TrackInactive) - - msg := ` -Related commands: - exercism fetch (see 'exercism help fetch') - exercism list (see 'exercism help list') - ` - fmt.Println(msg) -} diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go new file mode 100644 index 000000000..f83d5cc96 --- /dev/null +++ b/cmd/troubleshoot.go @@ -0,0 +1,268 @@ +package cmd + +import ( + "bytes" + "fmt" + "html/template" + "runtime" + "sync" + "time" + + "github.com/exercism/cli/cli" + "github.com/exercism/cli/config" + "github.com/exercism/cli/debug" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// fullAPIKey flag for troubleshoot command. +var fullAPIKey bool + +// troubleshootCmd does a diagnostic self-check. +var troubleshootCmd = &cobra.Command{ + Use: "troubleshoot", + Aliases: []string{"debug"}, + Short: "Troubleshoot does a diagnostic self-check.", + Long: `Provides output to help with troubleshooting. + +If you're running into trouble, copy and paste the output from the troubleshoot +command into a topic on the Exercism forum so we can help figure out what's going on. +`, + RunE: func(cmd *cobra.Command, args []string) error { + cli.TimeoutInSeconds = cli.TimeoutInSeconds * 2 + c := cli.New(Version) + + cfg := config.NewConfig() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + cfg.UserViperConfig = v + + status := newStatus(c, cfg) + status.Censor = !fullAPIKey + s, err := status.check() + if err != nil { + return err + } + + fmt.Printf("%s", s) + return nil + }, +} + +// Status represents the results of a CLI self test. +type Status struct { + Censor bool + Version versionStatus + System systemStatus + Configuration configurationStatus + APIReachability apiReachabilityStatus + cfg config.Config + cli *cli.CLI +} + +type versionStatus struct { + Current string + Latest string + Status string + Error error + UpToDate bool +} + +type systemStatus struct { + OS string + Architecture string + Build string +} + +type configurationStatus struct { + Home string + Workspace string + Dir string + Token string + TokenURL string +} + +type apiReachabilityStatus struct { + Services []*apiPing +} + +type apiPing struct { + Service string + URL string + Status string + Latency time.Duration +} + +// newStatus prepares a value to perform a diagnostic self-check. +func newStatus(cli *cli.CLI, cfg config.Config) Status { + status := Status{ + cfg: cfg, + cli: cli, + } + return status +} + +// check runs the CLI's diagnostic self-check. +func (status *Status) check() (string, error) { + status.Version = newVersionStatus(status.cli) + status.System = newSystemStatus() + status.Configuration = newConfigurationStatus(status) + status.APIReachability = newAPIReachabilityStatus(status.cfg) + + return status.compile() +} +func (status *Status) compile() (string, error) { + t, err := template.New("self-test").Parse(tmplSelfTest) + if err != nil { + return "", err + } + + var bb bytes.Buffer + if err = t.Execute(&bb, status); err != nil { + return "", err + } + return bb.String(), nil +} + +func newAPIReachabilityStatus(cfg config.Config) apiReachabilityStatus { + baseURL := cfg.UserViperConfig.GetString("apibaseurl") + if baseURL == "" { + baseURL = cfg.DefaultBaseURL + } + ar := apiReachabilityStatus{ + Services: []*apiPing{ + {Service: "GitHub", URL: "https://api.github.com"}, + {Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)}, + }, + } + var wg sync.WaitGroup + wg.Add(len(ar.Services)) + for _, service := range ar.Services { + go service.Call(&wg) + } + wg.Wait() + return ar +} + +func newVersionStatus(c *cli.CLI) versionStatus { + vs := versionStatus{ + Current: c.Version, + } + ok, err := c.IsUpToDate() + if err == nil { + vs.Latest = c.LatestRelease.Version() + } else { + vs.Error = fmt.Errorf("Error: %s", err) + } + vs.UpToDate = ok + return vs +} + +func newSystemStatus() systemStatus { + ss := systemStatus{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + if cli.BuildOS != "" && cli.BuildARCH != "" { + ss.Build = fmt.Sprintf("%s/%s", cli.BuildOS, cli.BuildARCH) + } + if cli.BuildARM != "" { + ss.Build = fmt.Sprintf("%s ARMv%s", ss.Build, cli.BuildARM) + } + return ss +} + +func newConfigurationStatus(status *Status) configurationStatus { + v := status.cfg.UserViperConfig + + workspace := v.GetString("workspace") + if workspace == "" { + workspace = fmt.Sprintf("%s (default)", config.DefaultWorkspaceDir(status.cfg)) + } + + cs := configurationStatus{ + Home: status.cfg.Home, + Workspace: workspace, + Dir: status.cfg.Dir, + Token: v.GetString("token"), + TokenURL: config.TokenURL(v.GetString("apibaseurl")), + } + if status.Censor && cs.Token != "" { + cs.Token = debug.Redact(cs.Token) + } + return cs +} + +func (ping *apiPing) Call(wg *sync.WaitGroup) { + defer wg.Done() + + now := time.Now() + res, err := cli.HTTPClient.Get(ping.URL) + delta := time.Since(now) + ping.Latency = delta + if err != nil { + ping.Status = err.Error() + return + } + res.Body.Close() + ping.Status = "connected" +} + +const tmplSelfTest = ` +Troubleshooting Information +=========================== + +Version +---------------- +Current: {{ .Version.Current }} +Latest: {{ with .Version.Latest }}{{ . }}{{ else }}{{ end }} +{{ with .Version.Error }} +{{ . }} +{{ end -}} +{{ if not .Version.UpToDate }} +Call 'exercism upgrade' to get the latest version. +See the release notes at https://github.com/exercism/cli/releases/tag/v{{ .Version.Latest }} for details. +{{ end }} + +Operating System +---------------- +OS: {{ .System.OS }} +Architecture: {{ .System.Architecture }} +{{ with .System.Build }} +Build: {{ . }} +{{ end }} + +Configuration +---------------- +Home: {{ .Configuration.Home }} +Workspace: {{ .Configuration.Workspace }} +Config: {{ .Configuration.Dir }} +API key: {{ with .Configuration.Token }}{{ . }}{{ else }} +Find your API key at {{ .Configuration.TokenURL }}{{ end }} + +API Reachability +---------------- +{{ range .APIReachability.Services }} +{{ .Service }}: + * {{ .URL }} + * [{{ .Status }}] + * {{ .Latency }} +{{ end }} + +If you are having trouble, please create a new topic in the Exercism forum +at https://forum.exercism.org/c/support/cli/10 and include +this information. +{{ if not .Censor }} +Don't share your API key. Keep that private. +{{ end }}` + +func init() { + RootCmd.AddCommand(troubleshootCmd) + troubleshootCmd.Flags().BoolVarP(&fullAPIKey, "full-api-key", "f", false, "display the user's full API key, censored by default") +} diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 5458b26b7..e4ec04508 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -1,90 +1,58 @@ package cmd import ( - "bytes" "fmt" - "log" - "net/http" - "runtime" - "strings" - "time" - "github.com/codegangsta/cli" - "github.com/kardianos/osext" + "github.com/exercism/cli/cli" + "github.com/spf13/cobra" ) -var ( - // BuildOS is the operating system (GOOS) used during the build process. - BuildOS string - // BuildARM is the ARM version (GOARM) used during the build process. - BuildARM string - // BuildARCH is the architecture (GOARCH) used during the build process. - BuildARCH string -) +// upgradeCmd downloads and installs the most recent version of the CLI. +var upgradeCmd = &cobra.Command{ + Use: "upgrade", + Aliases: []string{"u"}, + Short: "Upgrade to the latest version of the CLI.", + Long: `Upgrade to the latest version of the CLI. -// Upgrade allows the user to upgrade to the latest version of the CLI. -func Upgrade(ctx *cli.Context) { - client := http.Client{Timeout: 10 * time.Second} - rel, err := fetchLatestRelease(client) - if err != nil { - log.Fatal("unable to check latest release version: " + err.Error()) - } +This finds and downloads the latest release, if you don't +already have it. - // TODO: This checks strings against string - // Version 2.2.0 against release 2.1.0 - // will trigger an upgrade... - // should probably parse semver and compare - if rel.Version() == ctx.App.Version { - fmt.Println("Your CLI is up to date!") - return - } +On Windows the old CLI will be left on disk, marked as hidden. +The next time you upgrade, the hidden file will be overwritten. +You can always delete this file. + `, + RunE: func(cmd *cobra.Command, args []string) error { + c := cli.New(Version) + err := updateCLI(c) + if err != nil { + return fmt.Errorf(` - // Locate the path to the current `exercism` executable. - dest, err := osext.Executable() - if err != nil { - log.Fatalf("Unable to find current executable path: %s", err) - } - - var ( - OS = osMap[runtime.GOOS] - ARCH = archMap[runtime.GOARCH] - ) +We were not able to upgrade the cli because we encountered an error: +%s - if OS == "" || ARCH == "" { - log.Fatalf("unable to upgrade: OS %s ARCH %s", OS, ARCH) - } +Please check the FAQ for solutions to common upgrading issues. - buildName := fmt.Sprintf("%s-%s", OS, ARCH) - if BuildARCH == "arm" { - if BuildARM == "" { - log.Fatalf("unable to upgrade: arm version not found") +https://exercism.org/faqs`, err) } - buildName = fmt.Sprintf("%s-v%s", buildName, BuildARM) - } + return nil + }, +} - var downloadRC *bytes.Reader - for _, a := range rel.Assets { - if strings.Contains(a.Name, buildName) { - fmt.Printf("Downloading %s\n", a.Name) - downloadRC, err = a.download() - if err != nil { - log.Fatalf("error downloading executable: %s\n", err) - } - break - } - } - if downloadRC == nil { - log.Fatalf("No executable found for %s/%s%s", BuildOS, BuildARCH, BuildARM) +// updateCLI updates CLI to the latest available version, if it is out of date. +func updateCLI(c cli.Updater) error { + ok, err := c.IsUpToDate() + if err != nil { + return err } - if OS == "windows" { - err = installZip(downloadRC, dest) - } else { - err = installTgz(downloadRC, dest) - } - if err != nil { - log.Fatal(err) + if ok { + fmt.Fprintln(Out, "Your CLI version is up to date.") + return nil } - fmt.Println("Successfully upgraded!") + return c.Upgrade() +} + +func init() { + RootCmd.AddCommand(upgradeCmd) } diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go new file mode 100644 index 000000000..b2489ea83 --- /dev/null +++ b/cmd/upgrade_test.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type fakeCLI struct { + UpToDate bool + UpgradeCalled bool +} + +func (fc *fakeCLI) IsUpToDate() (bool, error) { + return fc.UpToDate, nil +} + +func (fc *fakeCLI) Upgrade() error { + fc.UpgradeCalled = true + return nil +} + +func TestUpgrade(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + desc string + upToDate bool + expected bool + }{ + { + desc: "upgrade should be called for an outdated CLI", + upToDate: false, + expected: true, + }, + { + desc: "upgrade should not be called for an already updated CLI", + upToDate: true, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + fc := &fakeCLI{UpToDate: tc.upToDate} + + err := updateCLI(fc) + assert.NoError(t, err) + assert.Equal(t, tc.expected, fc.UpgradeCalled) + }) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 000000000..7eec21d45 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "fmt" + + "github.com/exercism/cli/cli" + "github.com/spf13/cobra" +) + +// Version is the version of the current build. +// It follows semantic versioning. +const Version = "3.5.8" + +// checkLatest flag for version command. +var checkLatest bool + +// versionCmd outputs the version of the CLI. +var versionCmd = &cobra.Command{ + Use: "version", + Aliases: []string{"v"}, + Short: "Version outputs the version of CLI.", + Long: `Version outputs the version of the exercism binary that is in use. + +To check for the latest available version, call the command with the +--latest flag. + `, + + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println(currentVersion()) + + if checkLatest { + c := cli.New(Version) + l, err := checkForUpdate(c) + if err != nil { + return err + } + + fmt.Println(l) + } + + return nil + }, +} + +// currentVersion returns a formatted version string for the Exercism CLI. +func currentVersion() string { + return fmt.Sprintf("exercism version %s", Version) +} + +// checkForUpdate verifies if the CLI is running the latest version. +// If the client is out of date, the function returns upgrade instructions. +func checkForUpdate(c *cli.CLI) (string, error) { + + ok, err := c.IsUpToDate() + if err != nil { + return "", err + } + + if ok { + return "Your CLI version is up to date.", nil + } + + // Anything but ok is out of date. + msg := fmt.Sprintf("A new CLI version is available. Run `exercism upgrade` to update to %s", c.LatestRelease.Version()) + return msg, nil + +} + +func init() { + RootCmd.AddCommand(versionCmd) + versionCmd.Flags().BoolVarP(&checkLatest, "latest", "l", false, "check latest available version") +} diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 000000000..6c8916282 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/exercism/cli/cli" + "github.com/stretchr/testify/assert" +) + +func TestCurrentVersion(t *testing.T) { + expected := fmt.Sprintf("exercism version %s", Version) + + actual := currentVersion() + assert.Equal(t, expected, actual) +} + +func TestVersionUpdateCheck(t *testing.T) { + fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"tag_name": "v2.0.0"}`) + }) + ts := httptest.NewServer(fakeEndpoint) + defer ts.Close() + cli.ReleaseURL = ts.URL + + testCases := []struct { + desc string + version string + expected string + }{ + { + desc: "It returns new version available for versions older than latest.", + version: "1.0.0", + expected: "A new CLI version is available. Run `exercism upgrade` to update to 2.0.0", + }, + { + desc: "It returns up to date for versions matching latest.", + version: "2.0.0", + expected: "Your CLI version is up to date.", + }, + { + desc: "It returns up to date for versions newer than latest.", + version: "2.0.1", + expected: "Your CLI version is up to date.", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + c := &cli.CLI{ + Version: tc.version, + } + + actual, err := checkForUpdate(c) + + assert.NoError(t, err) + assert.NotEmpty(t, actual) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/cmd/workspace.go b/cmd/workspace.go new file mode 100644 index 000000000..673ab367e --- /dev/null +++ b/cmd/workspace.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + + "github.com/exercism/cli/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// workspaceCmd outputs the path to the person's workspace directory. +var workspaceCmd = &cobra.Command{ + Use: "workspace", + Aliases: []string{"w"}, + Short: "Print out the path to your Exercism workspace.", + Long: `Print out the path to your Exercism workspace. + +This command can be used for scripting, or it can be combined with shell +commands to take you to your workspace. + +For example you can run: + + cd $(exercism workspace) + +On Windows, this will work only with Powershell, however you would +need to be on the same drive as your workspace directory. Otherwise +nothing will happen. + `, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.NewConfig() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + fmt.Fprintf(Out, "%s\n", v.GetString("workspace")) + return nil + }, +} + +func init() { + RootCmd.AddCommand(workspaceCmd) +} diff --git a/config/config.go b/config/config.go index 4e6cc7566..f16b2b6ce 100644 --- a/config/config.go +++ b/config/config.go @@ -1,178 +1,134 @@ package config import ( - "bufio" - "bytes" - "encoding/json" "fmt" - "io" - "log" "os" + "path/filepath" + "regexp" + "runtime" "strings" - "github.com/exercism/cli/paths" + "github.com/spf13/viper" ) -const ( - // hostAPI is the endpoint to submit solutions to, and to get personalized data - hostAPI = "http://exercism.io" - // hostXAPI is the endpoint to fetch problems from - hostXAPI = "http://x.exercism.io" +var ( + defaultBaseURL = "https://api.exercism.org/v1" + + // DefaultDirName is the default name used for config and workspace directories. + DefaultDirName string ) -// Config represents the settings for particular user. -// This defines both the auth for talking to the API, as well as -// where to put problems that get downloaded. +// Config lets us inject configuration options into commands. type Config struct { - APIKey string `json:"apiKey"` - Dir string `json:"dir"` - API string `json:"api"` - XAPI string `json:"xapi"` - File string `json:"-"` // full path to config file -} - -// New returns a configuration struct with content from the exercism.json file -func New(path string) (*Config, error) { - c := &Config{} - err := c.load(paths.Config(path)) - return c, err + OS string + Home string + Dir string + DefaultBaseURL string + DefaultDirName string + UserViperConfig *viper.Viper + Persister Persister } -// Update sets new values where given. -func (c *Config) Update(key, host, dir, xapi string) error { - key = strings.TrimSpace(key) - if key != "" { - c.APIKey = key - } - - host = strings.TrimSpace(host) - if host != "" { - c.API = host +// NewConfig provides a configuration with default values. +func NewConfig() Config { + home := userHome() + dir := Dir() + + return Config{ + OS: runtime.GOOS, + Dir: Dir(), + Home: home, + DefaultBaseURL: defaultBaseURL, + DefaultDirName: DefaultDirName, + Persister: FilePersister{Dir: dir}, } - - if dir != "" { - c.Dir = paths.Exercises(dir) - } - - xapi = strings.TrimSpace(xapi) - if xapi != "" { - c.XAPI = xapi - } - - return nil } -// Write saves the config as JSON. -func (c *Config) Write() error { - // truncates existing file if it exists - f, err := os.Create(c.File) - if err != nil { - return err - } - defer f.Close() - - b, err := json.MarshalIndent(c, "", "\t") - if err != nil { - return err - } - - if _, err := f.Write(b); err != nil { - return err - } - - return nil +// SetDefaultDirName configures the default directory name based on the name of the binary. +func SetDefaultDirName(binaryName string) { + DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) } -func (c *Config) load(argPath string) error { - c.File = argPath - - if err := c.read(); err != nil { - return err - } - - // in case people manually update the config file - // with weird formatting - c.APIKey = strings.TrimSpace(c.APIKey) - c.Dir = strings.TrimSpace(c.Dir) - c.API = strings.TrimSpace(c.API) - c.XAPI = strings.TrimSpace(c.XAPI) - - return c.setDefaults() -} - -func (c *Config) read() error { - if _, err := os.Stat(c.File); err != nil { - if os.IsNotExist(err) { - return nil +// Dir is the configured config home directory. +// All the cli-related config files live in this directory. +func Dir() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("APPDATA") + if dir != "" { + return filepath.Join(dir, DefaultDirName) } - return err - } - f, err := os.Open(c.File) - if err != nil { - return err - } - defer f.Close() - - if err := json.NewDecoder(f).Decode(&c); err != nil { - var extra string - if serr, ok := err.(*json.SyntaxError); ok { - if _, serr := f.Seek(0, os.SEEK_SET); serr != nil { - log.Fatalf("seek error: %v", serr) - } - line, str := findInvalidJSON(f, serr.Offset) - extra = fmt.Sprintf(":\ninvalid JSON syntax at line %d:\n%s", - line, str) + } else { + dir := os.Getenv("EXERCISM_CONFIG_HOME") + if dir != "" { + return dir + } + dir = os.Getenv("XDG_CONFIG_HOME") + if dir == "" { + dir = filepath.Join(os.Getenv("HOME"), ".config") + } + if dir != "" { + return filepath.Join(dir, DefaultDirName) } - return fmt.Errorf("error parsing JSON in the config file %s%s\n%s", f.Name(), extra, err) } - - return nil + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir } -func findInvalidJSON(f io.Reader, pos int64) (int, string) { - var ( - col int - line int - errLine []byte - ) - buf := new(bytes.Buffer) - fb := bufio.NewReader(f) - - for c := int64(0); c < pos; { - b, err := fb.ReadBytes('\n') - if err != nil { - log.Fatalf("read error: %v", err) +func userHome() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("USERPROFILE") + if dir != "" { + return dir + } + dir = filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) + if dir != "" { + return dir + } + } else { + dir = os.Getenv("HOME") + if dir != "" { + return dir } - c += int64(len(b)) - col = len(b) - int(c-pos) - - line++ - errLine = b } + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir +} - if len(errLine) != 0 { - buf.WriteString(fmt.Sprintf("%5d: %s <~", line, errLine[:col])) +// DefaultWorkspaceDir provides a sensible default for the Exercism workspace. +// The default is different depending on the platform, in order to best match +// the conventions for that platform. +// It places the directory in the user's home path. +func DefaultWorkspaceDir(cfg Config) string { + dir := cfg.DefaultDirName + if cfg.OS != "linux" { + dir = strings.Title(dir) } - - return line, buf.String() + return filepath.Join(cfg.Home, dir) } -// IsAuthenticated returns true if the config contains an API key. -// This does not check whether or not that key is valid. -func (c *Config) IsAuthenticated() bool { - return c.APIKey != "" +// Save persists a viper config of the base name. +func (c Config) Save(basename string) error { + return c.Persister.Save(c.UserViperConfig, basename) } -func (c *Config) setDefaults() error { - if c.API == "" { - c.API = hostAPI +// InferSiteURL guesses what the website URL is. +// The basis for the guess is which API we're submitting to. +func InferSiteURL(apiURL string) string { + if apiURL == "" { + apiURL = defaultBaseURL } - - if c.XAPI == "" { - c.XAPI = hostXAPI + if apiURL == "https://api.exercism.org/v1" { + return "https://exercism.org" } + re := regexp.MustCompile("^(https?://[^/]*).*") + return re.ReplaceAllString(apiURL, "$1") +} - c.Dir = paths.Exercises(c.Dir) - - return nil +// TokenURL provides a link to where the user can find their API token. +func TokenURL(apiURL string) string { + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/settings/api_cli") } diff --git a/config/config_notwin_test.go b/config/config_notwin_test.go new file mode 100644 index 000000000..33912cc9b --- /dev/null +++ b/config/config_notwin_test.go @@ -0,0 +1,30 @@ +//go:build !windows + +package config + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultWorkspaceDir(t *testing.T) { + testCases := []struct { + cfg Config + expected string + }{ + { + cfg: Config{OS: "darwin", Home: "/User/charlie", DefaultDirName: "apple"}, + expected: "/User/charlie/Apple", + }, + { + cfg: Config{OS: "linux", Home: "/home/bob", DefaultDirName: "banana"}, + expected: "/home/bob/banana", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, DefaultWorkspaceDir(tc.cfg), fmt.Sprintf("Operating System: %s", tc.cfg.OS)) + } +} diff --git a/config/config_test.go b/config/config_test.go index 6b1169fa7..40ecb0c36 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,180 +1,24 @@ package config import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" "testing" - "github.com/exercism/cli/paths" "github.com/stretchr/testify/assert" ) -func TestLoad(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatal(err) - } - configPath := filepath.Join(tmpDir, "config.json") - if err := os.Link(fixturePath(t, "config.json"), configPath); err != nil { - t.Fatal(err) - } - dirtyPath := filepath.Join(tmpDir, "dirty.json") - if err := os.Link(fixturePath(t, "dirty.json"), dirtyPath); err != nil { - t.Fatal(err) - } - paths.Home = tmpDir - +func TestInferSiteURL(t *testing.T) { testCases := []struct { - desc string - in string // the name of the file passed as a command line argument - out string // the name of the file that the config will be written to - dir, key, api, xapi string // the actual config values + api, url string }{ - { - desc: "defaults", - in: "", - out: paths.Config(""), - dir: paths.Exercises(""), - key: "", - api: hostAPI, - xapi: hostXAPI, - }, - { - desc: "file exists", - in: configPath, - out: configPath, - dir: "/a/b/c", - key: "abc123", - api: "http://api.example.com", - xapi: "http://x.example.com", - }, - { - desc: "unexpanded path", - in: "~/config.json", - out: configPath, - dir: "/a/b/c", - key: "abc123", - api: "http://api.example.com", - xapi: "http://x.example.com", - }, - { - desc: "sanitizes whitespace", - in: "~/dirty.json", - out: filepath.Join(tmpDir, "dirty.json"), - dir: "/a/b/c", - key: "abc123", - api: "http://api.example.com", - xapi: "http://x.example.com", - }, + {"https://api.exercism.org/v1", "https://exercism.org"}, + {"https://v2.exercism.org/api/v1", "https://v2.exercism.org"}, + {"https://mentors-beta.exercism.org/api/v1", "https://mentors-beta.exercism.org"}, + {"http://localhost:3000/api/v1", "http://localhost:3000"}, + {"", "https://exercism.org"}, // use the default + {"http://whatever", "http://whatever"}, // you're on your own, pal } for _, tc := range testCases { - c, err := New(tc.in) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, tc.out, c.File, tc.desc) - assert.Equal(t, tc.dir, c.Dir, tc.desc) - assert.Equal(t, tc.key, c.APIKey, tc.desc) - assert.Equal(t, tc.api, c.API, tc.desc) - assert.Equal(t, tc.xapi, c.XAPI, tc.desc) - } -} - -func TestReadDirectory(t *testing.T) { - // if the provided path is a directory, append the default filename - tmpDir, err := ioutil.TempDir("", "") - assert.NoError(t, err) - - myConfig, err := New(tmpDir) - assert.NoError(t, err) - - expected := filepath.Join(tmpDir, paths.File) - actual := myConfig.File - assert.Equal(t, expected, actual) -} - -func TestLoad_InvalidJSON(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatal(err) - } - invalidPath := filepath.Join(tmpDir, "config_invalid.json") - if err := os.Link(fixturePath(t, "config_invalid.json"), invalidPath); err != nil { - t.Fatal(err) + assert.Equal(t, InferSiteURL(tc.api), tc.url) } - - _, err = New(invalidPath) - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "invalid JSON syntax") - } -} - -func TestReadingWritingConfig(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "") - filename := fmt.Sprintf("%s/%s", tmpDir, paths.File) - assert.NoError(t, err) - - c1 := &Config{ - APIKey: "MyKey", - Dir: "/exercism/directory", - API: "localhost", - XAPI: "localhost", - File: filename, - } - - c1.Write() - - c2, err := New(filename) - assert.NoError(t, err) - - assert.Equal(t, c1.APIKey, c2.APIKey) - assert.Equal(t, c1.Dir, c2.Dir) - assert.Equal(t, c1.API, c2.API) - assert.Equal(t, c1.XAPI, c2.XAPI) -} - -func TestUpdateConfig(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatal(err) - } - paths.Home = tmpDir - - c := &Config{ - APIKey: "MyKey", - API: "localhost", - Dir: "/exercism/directory", - XAPI: "localhost", - } - - // Test the blank values don't overwrite existing values - err = c.Update("", "", "", "") - assert.Equal(t, "MyKey", c.APIKey) - assert.Equal(t, "localhost", c.API) - assert.Equal(t, "/exercism/directory", c.Dir) - assert.Equal(t, "localhost", c.XAPI) - assert.NoError(t, err) - - // Test that each value can be overwritten - err = c.Update("NewKey", "http://example.com", "/tmp/exercism", "http://x.example.org") - assert.Equal(t, "NewKey", c.APIKey) - assert.Equal(t, "http://example.com", c.API) - assert.Equal(t, "/tmp/exercism", c.Dir) - assert.Equal(t, "http://x.example.org", c.XAPI) - assert.NoError(t, err) - - // Test home is expanded on update - err = c.Update("", "", "~/myexercism", "") - assert.Equal(t, filepath.Join(tmpDir, "myexercism"), c.Dir) - assert.NoError(t, err) -} - -func fixturePath(t *testing.T, filename string) string { - _, caller, _, ok := runtime.Caller(0) - assert.True(t, ok) - return filepath.Join(filepath.Dir(caller), "..", "fixtures", filename) } diff --git a/config/config_windows_test.go b/config/config_windows_test.go new file mode 100644 index 000000000..5757b783f --- /dev/null +++ b/config/config_windows_test.go @@ -0,0 +1,14 @@ +//go:build windows + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultWindowsWorkspaceDir(t *testing.T) { + cfg := Config{OS: "windows", Home: "C:\\Something", DefaultDirName: "basename"} + assert.Equal(t, "C:\\Something\\Basename", DefaultWorkspaceDir(cfg)) +} diff --git a/config/persister.go b/config/persister.go new file mode 100644 index 000000000..1b29dc720 --- /dev/null +++ b/config/persister.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +// Persister saves viper configs. +type Persister interface { + Save(*viper.Viper, string) error +} + +// FilePersister saves viper configs to the file system. +type FilePersister struct { + Dir string +} + +// Save writes the viper config to the target location on the filesystem. +func (p FilePersister) Save(v *viper.Viper, basename string) error { + v.SetConfigType("json") + v.AddConfigPath(p.Dir) + v.SetConfigName(basename) + + if _, err := os.Stat(p.Dir); os.IsNotExist(err) { + if err := os.MkdirAll(p.Dir, os.FileMode(0700)); err != nil { + return err + } + } + + // WriteConfig is broken. + // Someone proposed a fix in https://github.com/spf13/viper/pull/503, + // but the fix doesn't work yet. + // When it's fixed and merged we can get rid of `path` + // and use viperConfig.WriteConfig() directly. + path := filepath.Join(p.Dir, fmt.Sprintf("%s.json", basename)) + return v.WriteConfigAs(path) +} + +// InMemoryPersister is a noop persister for use in unit tests. +type InMemoryPersister struct{} + +// Save does nothing. +func (p InMemoryPersister) Save(*viper.Viper, string) error { + return nil +} diff --git a/config/resolve.go b/config/resolve.go new file mode 100644 index 000000000..93c1645bc --- /dev/null +++ b/config/resolve.go @@ -0,0 +1,30 @@ +package config + +import ( + "os" + "path/filepath" + "strings" +) + +// Resolve cleans up filesystem paths. +func Resolve(path, home string) string { + if path == "" { + return "" + } + if strings.HasPrefix(path, "~/") { + path = strings.Replace(path, "~/", "", 1) + return filepath.Join(home, path) + } + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + // if using "/dir" on Windows + if strings.HasPrefix(path, "/") { + return filepath.Join(home, filepath.Clean(path)) + } + cwd, err := os.Getwd() + if err != nil { + return path + } + return filepath.Join(cwd, path) +} diff --git a/config/resolve_notwin_test.go b/config/resolve_notwin_test.go new file mode 100644 index 000000000..fb70d5f69 --- /dev/null +++ b/config/resolve_notwin_test.go @@ -0,0 +1,35 @@ +//go:build !windows + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolve(t *testing.T) { + cwd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + in, out string + }{ + {"", ""}, // don't make wild guesses + {"/home/alice///foobar", "/home/alice/foobar"}, + {"~/foobar", "/home/alice/foobar"}, + {"/foobar/~/noexpand", "/foobar/~/noexpand"}, + {"/no/modification", "/no/modification"}, + {"relative", filepath.Join(cwd, "relative")}, + {"relative///path", filepath.Join(cwd, "relative", "path")}, + } + + for _, tc := range testCases { + testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + t.Run(testName, func(t *testing.T) { + assert.Equal(t, tc.out, Resolve(tc.in, "/home/alice"), testName) + }) + } +} diff --git a/config/resolve_windows.go b/config/resolve_windows.go new file mode 100644 index 000000000..20035a312 --- /dev/null +++ b/config/resolve_windows.go @@ -0,0 +1,32 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolve(t *testing.T) { + cwd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + in, out string + }{ + {"", ""}, // don't make wild guesses + {"C:\\alice\\\\foobar", "C:\\alice\\\\foobar"}, + {"\\foobar\\~\\noexpand", "\\foobar\\~\\noexpand"}, + {"\\no\\modification", "\\no\\modification"}, + {"relative", filepath.Join(cwd, "relative")}, + {"relative\\path", filepath.Join(cwd, "relative", "path")}, + } + + for _, tc := range testCases { + t.Run(tc.in, func(t *testing.T) { + desc := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + assert.Equal(t, tc.out, Resolve(tc.in, ""), desc) + }) + } +} diff --git a/debug/debug.go b/debug/debug.go index c95d171ca..a96d1f3bd 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -1,15 +1,22 @@ package debug import ( + "bytes" "fmt" "io" + "log" + "net/http" + "net/http/httputil" "os" + "strings" ) var ( // Verbose determines if debugging output is displayed to the user Verbose bool output io.Writer = os.Stderr + // UnmaskAPIKey determines if the API key should de displayed during a dump + UnmaskAPIKey bool ) // Println conditionally outputs a message to Stderr @@ -25,3 +32,65 @@ func Printf(format string, args ...interface{}) { fmt.Fprintf(output, format, args...) } } + +// DumpRequest dumps out the provided http.Request +func DumpRequest(req *http.Request) { + if !Verbose { + return + } + + var bodyCopy bytes.Buffer + body := io.TeeReader(req.Body, &bodyCopy) + req.Body = io.NopCloser(body) + + authHeader := req.Header.Get("Authorization") + + if authParts := strings.Split(authHeader, " "); len(authParts) > 1 && !UnmaskAPIKey { + if token := authParts[1]; token != "" { + req.Header.Set("Authorization", "Bearer "+Redact(token)) + } + } + + dump, err := httputil.DumpRequest(req, req.ContentLength > 0) + if err != nil { + log.Fatal(err) + } + + Println("\n========================= BEGIN DumpRequest =========================") + Println(string(dump)) + Println("========================= END DumpRequest =========================") + Println("") + + req.Header.Set("Authorization", authHeader) + req.Body = io.NopCloser(&bodyCopy) +} + +// DumpResponse dumps out the provided http.Response +func DumpResponse(res *http.Response) { + if !Verbose { + return + } + + var bodyCopy bytes.Buffer + body := io.TeeReader(res.Body, &bodyCopy) + res.Body = io.NopCloser(body) + + dump, err := httputil.DumpResponse(res, res.ContentLength > 0) + if err != nil { + log.Fatal(err) + } + + Println("\n========================= BEGIN DumpResponse =========================") + Println(string(dump)) + Println("========================= END DumpResponse =========================") + Println("") + + res.Body = io.NopCloser(body) +} + +// Redact masks the given token by replacing part of the string with * +func Redact(token string) string { + str := token[4 : len(token)-3] + redaction := strings.Repeat("*", len(str)) + return string(token[:4]) + redaction + string(token[len(token)-3:]) +} diff --git a/debug/debug_test.go b/debug/debug_test.go index d4adf3f43..f9be5f864 100644 --- a/debug/debug_test.go +++ b/debug/debug_test.go @@ -2,7 +2,10 @@ package debug import ( "bytes" + "net/http" "testing" + + "github.com/stretchr/testify/assert" ) func TestVerboseEnabled(t *testing.T) { @@ -26,3 +29,80 @@ func TestVerboseDisabled(t *testing.T) { t.Error("expected '' got", b.String()) } } + +func TestDumpRequest(t *testing.T) { + testCases := []struct { + desc string + auth string + verbose bool + unmask bool + }{ + { + desc: "Do not attempt to dump request if 'Verbose' is set to false", + auth: "", + verbose: false, + unmask: false, + }, + { + desc: "Dump request without authorization header", + auth: "", //not set + verbose: true, + unmask: false, + }, + { + desc: "Dump request with malformed 'Authorization' header", + auth: "malformed", + verbose: true, + unmask: true, + }, + { + desc: "Dump request with properly formed 'Authorization' header", + auth: "Bearer abc12-345abcde1234-5abc12", + verbose: true, + unmask: false, + }, + } + + b := &bytes.Buffer{} + output = b + for _, tc := range testCases { + Verbose = tc.verbose + UnmaskAPIKey = tc.unmask + r, _ := http.NewRequest("GET", "https://api.example.com/bogus", nil) + if tc.auth != "" { + r.Header.Set("Authorization", tc.auth) + } + + DumpRequest(r) + if tc.verbose { + assert.Regexp(t, "GET /bogus", b.String(), tc.desc) + assert.Equal(t, tc.auth, r.Header.Get("Authorization"), tc.desc) + if tc.unmask { + assert.Regexp(t, "Authorization: "+tc.auth, b.String(), tc.desc) + } + } else { + assert.NotRegexp(t, "GET /bogus", b.String(), tc.desc) + } + } +} + +func TestDumpResponse(t *testing.T) { + b := &bytes.Buffer{} + output = b + Verbose = true + r := &http.Response{ + StatusCode: 200, + ProtoMajor: 1, + ProtoMinor: 1, + } + + DumpResponse(r) + assert.Regexp(t, "HTTP/1.1 200 OK", b.String()) +} + +func TestRedact(t *testing.T) { + fakeToken := "1a11111aaaa111aa1a11111a11111aa1" + expected := "1a11*************************aa1" + + assert.Equal(t, expected, Redact(fakeToken)) +} diff --git a/exercism/doc.go b/exercism/doc.go index a60d6e7b2..3e00d9015 100644 --- a/exercism/doc.go +++ b/exercism/doc.go @@ -1,5 +1,5 @@ /* -Command exercism allows users to interact with the exercism.io platform. +Command exercism allows users to interact with the exercism.org platform. The primary actions are to fetch problems to be solved, and submit iterations of these problems. diff --git a/exercism/main.go b/exercism/main.go index a5f00caa2..0ac55033d 100644 --- a/exercism/main.go +++ b/exercism/main.go @@ -1,174 +1,7 @@ package main -import ( - "fmt" - "log" - "os" - "runtime" - - "github.com/codegangsta/cli" - "github.com/exercism/cli/api" - "github.com/exercism/cli/cmd" - "github.com/exercism/cli/debug" -) - -const ( - // Version is the current release of the command-line app. - // We try to follow Semantic Versioning (http://semver.org), - // but with the http://exercism.io app being a prototype, a - // lot of things get out of hand. - Version = "2.2.4" - - descConfigure = "Writes config values to a JSON file." - descDebug = "Outputs useful debug information." - descDownload = "Downloads a solution given the ID of the latest iteration." - descFetch = "Fetches the next unsubmitted problem in each track." - descList = "Lists the available problems for a language track, given its ID." - descOpen = "Opens exercism.io to your most recent iteration of a problem given the track ID and problem slug." - descRestore = "Downloads the most recent iteration for each of your solutions on exercism.io." - descSkip = "Skips a problem given a track ID and problem slug." - descStatus = "Fetches information about your progress with a given language track." - descSubmit = "Submits a new iteration to a problem on exercism.io." - descTracks = "Lists the available language tracks." - descUpgrade = "Upgrades the CLI to the latest released version." - - descLongDownload = "The submission ID is the last part of the URL when looking at a solution on exercism.io." - descLongRestore = "Restore will pull the latest revisions of exercises that have already been submitted. It will *not* overwrite existing files. If you have made changes to a file and have not submitted it, and you're trying to restore the last submitted version, first move that file out of the way, then call restore." -) +import "github.com/exercism/cli/cmd" func main() { - api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) - - app := cli.NewApp() - app.Name = "exercism" - app.Usage = "A command line tool to interact with http://exercism.io" - app.Version = Version - app.Before = func(ctx *cli.Context) error { - debug.Verbose = ctx.GlobalBool("verbose") - debug.Println("verbose logging enabled") - - return nil - } - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Usage: "path to config file", - EnvVar: "EXERCISM_CONFIG_FILE,XDG_CONFIG_HOME", - }, - cli.BoolFlag{ - Name: "verbose", - Usage: "turn on verbose logging", - }, - } - app.Commands = []cli.Command{ - { - Name: "configure", - Usage: descConfigure, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "dir, d", - Usage: "path to exercises directory", - }, - cli.StringFlag{ - Name: "host, u", - Usage: "exercism api host", - }, - cli.StringFlag{ - Name: "key, k", - Usage: "exercism.io API key (see http://exercism.io/account/key)", - }, - cli.StringFlag{ - Name: "api, a", - Usage: "exercism xapi host", - }, - }, - Action: cmd.Configure, - }, - { - Name: "debug", - Usage: descDebug, - Action: cmd.Debug, - }, - { - Name: "download", - ShortName: "dl", - Usage: descDownload, - Description: descLongDownload, - Action: cmd.Download, - }, - { - Name: "fetch", - ShortName: "f", - Usage: descFetch, - Action: cmd.Fetch, - }, - { - Name: "list", - ShortName: "li", - Usage: descList, - Action: cmd.List, - }, - { - Name: "open", - ShortName: "op", - Usage: descOpen, - Action: cmd.Open, - }, - { - Name: "restore", - ShortName: "r", - Usage: descRestore, - Description: descLongRestore, - Action: cmd.Restore, - }, - { - Name: "skip", - Usage: descSkip, - Action: cmd.Skip, - }, - { - Name: "status", - ShortName: "st", - Usage: descStatus, - Action: cmd.Status, - }, - { - Name: "submit", - ShortName: "s", - Usage: descSubmit, - Action: cmd.Submit, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "test", - Usage: "allow submission of test files", - }, - cli.StringFlag{ - Name: "comment", - Usage: "includes a comment with the submission", - }, - }, - }, - { - Name: "tracks", - ShortName: "t", - Usage: descTracks, - Action: cmd.Tracks, - }, - { - Name: "unsubmit", - ShortName: "u", - Usage: "REMOVED", - Action: func(*cli.Context) { - fmt.Println("For security reasons, this command is no longer in use.\nYou can delete iterations in the web interface.") - }, - }, - { - Name: "upgrade", - Usage: descUpgrade, - Action: cmd.Upgrade, - }, - } - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } + cmd.Execute() } diff --git a/fixtures/config.json b/fixtures/config.json deleted file mode 100644 index 2b7c0c63b..000000000 --- a/fixtures/config.json +++ /dev/null @@ -1 +0,0 @@ -{"apiKey":"abc123","dir":"/a/b/c","api":"http://api.example.com","xapi":"http://x.example.com"} diff --git a/fixtures/config_invalid.json b/fixtures/config_invalid.json deleted file mode 100644 index dae85401f..000000000 --- a/fixtures/config_invalid.json +++ /dev/null @@ -1 +0,0 @@ -{"apiKey":"abc123","dir":"D:\\Users\escaped","api":"http://api.example.com","xapi":"http://x.example.com"} diff --git a/fixtures/detect-path-type/a-dir/.keep b/fixtures/detect-path-type/a-dir/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/detect-path-type/a-file.txt b/fixtures/detect-path-type/a-file.txt new file mode 100644 index 000000000..4dd1ef756 --- /dev/null +++ b/fixtures/detect-path-type/a-file.txt @@ -0,0 +1 @@ +This is a file. diff --git a/fixtures/detect-path-type/symlinked-dir b/fixtures/detect-path-type/symlinked-dir new file mode 120000 index 000000000..81398e249 --- /dev/null +++ b/fixtures/detect-path-type/symlinked-dir @@ -0,0 +1 @@ +a-dir \ No newline at end of file diff --git a/fixtures/detect-path-type/symlinked-file.txt b/fixtures/detect-path-type/symlinked-file.txt new file mode 120000 index 000000000..ee26a0518 --- /dev/null +++ b/fixtures/detect-path-type/symlinked-file.txt @@ -0,0 +1 @@ +a-file.txt \ No newline at end of file diff --git a/fixtures/dirty.json b/fixtures/dirty.json deleted file mode 100644 index c77c4ea7c..000000000 --- a/fixtures/dirty.json +++ /dev/null @@ -1 +0,0 @@ -{"apiKey":" abc123 ","dir":"\r\n/a/b/c \r \n","api":" \t\r\nhttp://api.example.com\n \r\n","xapi":" http://x.example.com "} diff --git a/fixtures/is-solution-path/broken/.exercism/metadata.json b/fixtures/is-solution-path/broken/.exercism/metadata.json new file mode 100644 index 000000000..004e1e203 --- /dev/null +++ b/fixtures/is-solution-path/broken/.exercism/metadata.json @@ -0,0 +1 @@ +{,} diff --git a/fixtures/is-solution-path/nope/.keep b/fixtures/is-solution-path/nope/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/is-solution-path/yepp/.exercism/metadata.json b/fixtures/is-solution-path/yepp/.exercism/metadata.json new file mode 100644 index 000000000..c2fbc8016 --- /dev/null +++ b/fixtures/is-solution-path/yepp/.exercism/metadata.json @@ -0,0 +1,3 @@ +{ + "id": "abc" +} diff --git a/fixtures/iteration/python/leap/lib/three.py b/fixtures/iteration/python/leap/lib/three.py deleted file mode 100644 index f49407daa..000000000 --- a/fixtures/iteration/python/leap/lib/three.py +++ /dev/null @@ -1 +0,0 @@ -# three diff --git a/fixtures/iteration/python/leap/one.py b/fixtures/iteration/python/leap/one.py deleted file mode 100644 index b8aad57f9..000000000 --- a/fixtures/iteration/python/leap/one.py +++ /dev/null @@ -1 +0,0 @@ -# one diff --git a/fixtures/iteration/python/leap/two.py b/fixtures/iteration/python/leap/two.py deleted file mode 100644 index 02f0d6c20..000000000 --- a/fixtures/iteration/python/leap/two.py +++ /dev/null @@ -1 +0,0 @@ -# two diff --git a/fixtures/iteration/python/leap/utf16be.py b/fixtures/iteration/python/leap/utf16be.py deleted file mode 100644 index 316c25d2d..000000000 Binary files a/fixtures/iteration/python/leap/utf16be.py and /dev/null differ diff --git a/fixtures/iteration/python/leap/utf16le.py b/fixtures/iteration/python/leap/utf16le.py deleted file mode 100644 index dd06d31dd..000000000 Binary files a/fixtures/iteration/python/leap/utf16le.py and /dev/null differ diff --git a/fixtures/locate-exercise/equipment/bat/.keep b/fixtures/locate-exercise/equipment/bat/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/food/squash/.keep b/fixtures/locate-exercise/food/squash/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/symlinked-workspace b/fixtures/locate-exercise/symlinked-workspace new file mode 120000 index 000000000..f1981605f --- /dev/null +++ b/fixtures/locate-exercise/symlinked-workspace @@ -0,0 +1 @@ +workspace \ No newline at end of file diff --git a/fixtures/locate-exercise/workspace/actions/batten/.keep b/fixtures/locate-exercise/workspace/actions/batten/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/actions/date/.keep b/fixtures/locate-exercise/workspace/actions/date/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/actions/squash/.keep b/fixtures/locate-exercise/workspace/actions/squash/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/creatures/bat/.keep b/fixtures/locate-exercise/workspace/creatures/bat/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/creatures/crane-2/.keep b/fixtures/locate-exercise/workspace/creatures/crane-2/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/creatures/crane/.keep b/fixtures/locate-exercise/workspace/creatures/crane/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/creatures/duck/.keep b/fixtures/locate-exercise/workspace/creatures/duck/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/creatures/horse/.keep b/fixtures/locate-exercise/workspace/creatures/horse/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/food/date b/fixtures/locate-exercise/workspace/food/date new file mode 120000 index 000000000..782468922 --- /dev/null +++ b/fixtures/locate-exercise/workspace/food/date @@ -0,0 +1 @@ +../text-files/date \ No newline at end of file diff --git a/fixtures/locate-exercise/workspace/food/squash b/fixtures/locate-exercise/workspace/food/squash new file mode 120000 index 000000000..e6c93335c --- /dev/null +++ b/fixtures/locate-exercise/workspace/food/squash @@ -0,0 +1 @@ +../../food/squash \ No newline at end of file diff --git a/fixtures/locate-exercise/workspace/friends/alice/creatures/bat/.keep b/fixtures/locate-exercise/workspace/friends/alice/creatures/bat/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/friends/alice/creatures/fly/.keep b/fixtures/locate-exercise/workspace/friends/alice/creatures/fly/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/locate-exercise/workspace/text-files/date b/fixtures/locate-exercise/workspace/text-files/date new file mode 100644 index 000000000..f81fce04c --- /dev/null +++ b/fixtures/locate-exercise/workspace/text-files/date @@ -0,0 +1 @@ +this is a file diff --git a/fixtures/locate-exercise/workspace/text-files/duck b/fixtures/locate-exercise/workspace/text-files/duck new file mode 100644 index 000000000..f81fce04c --- /dev/null +++ b/fixtures/locate-exercise/workspace/text-files/duck @@ -0,0 +1 @@ +this is a file diff --git a/fixtures/problems.json b/fixtures/problems.json deleted file mode 100644 index f076c5d60..000000000 --- a/fixtures/problems.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "problems": [ - { - "files": { - "README.md": "# Gigasecond\n\nWrite a program that will calculate the date that someone turned or will celebrate their 1 Gs anniversary.\n\nA gigasecond is one billion (10**9) seconds.\n\n\n## Source\n\nChapter 9 in Chris Pine's online Learn to Program tutorial. [view source](http://pine.fm/LearnToProgram/?Chapter=09)\n", - "cases_test.go": "package gigasecond\n\n// Source: exercism/x-common\n// Commit: f362340 Merge pull request #36 from soniakeys/gigasecond-test-data\n\n// Add one gigasecond to the input.\nvar addCases = []struct {\n\tin string\n\twant string\n}{\n\t{\n\t\t\"2011-04-25\",\n\t\t\"2043-01-01T01:46:40\",\n\t},\n\t{\n\t\t\"1977-06-13\",\n\t\t\"2009-02-19T01:46:40\",\n\t},\n\t{\n\t\t\"1959-07-19\",\n\t\t\"1991-03-27T01:46:40\",\n\t},\n}\n", - "gigasecond_test.go": "package gigasecond\n\n// Write a function AddGigasecond that works with time.Time.\n// Also define a variable Birthday set to your (or someone else's) birthday.\n// Run go test -v to see your gigasecond anniversary.\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst testVersion = 1\n\n// Retired testVersions\n// (none) 98807b314216ff27492378a00df60410cc971d32\n\n// date formats used in test data\nconst (\n\tfmtD = \"2006-01-02\"\n\tfmtDT = \"2006-01-02T15:04:05\"\n)\n\nfunc TestAddGigasecond(t *testing.T) {\n\tif TestVersion != testVersion {\n\t\tt.Fatalf(\"Found TestVersion = %v, want %v.\", TestVersion, testVersion)\n\t}\n\tfor _, tc := range addCases {\n\t\tin := parse(tc.in, fmtD, t)\n\t\twant := parse(tc.want, fmtDT, t)\n\t\tgot := AddGigasecond(in)\n\t\tif !got.Equal(want) {\n\t\t\tt.Fatalf(`AddGigasecond(%s)\n = %s\nwant %s`, in, got, want)\n\t\t}\n\t}\n\tt.Log(\"Tested\", len(addCases), \"cases.\")\n}\n\nfunc TestYourAnniversary(t *testing.T) {\n\tt.Logf(`\nYour birthday: %s\nYour gigasecond anniversary: %s`, Birthday, AddGigasecond(Birthday))\n}\n\nfunc parse(s string, f string, t *testing.T) time.Time {\n\ttt, err := time.Parse(f, s)\n\tif err != nil {\n\t\t// can't run tests if input won't parse. if this seems to be a\n\t\t// development or ci environment, raise an error. if this condition\n\t\t// makes it to the solver though, ask for a bug report.\n\t\t_, statErr := os.Stat(\"example_gen.go\")\n\t\tif statErr == nil || os.Getenv(\"TRAVIS_GO_VERSION\") > \"\" {\n\t\t\tt.Fatal(err)\n\t\t} else {\n\t\t\tt.Log(err)\n\t\t\tt.Skip(\"(Not your problem. \" +\n\t\t\t\t\"please file issue at https://github.com/exercism/xgo.)\")\n\t\t}\n\t}\n\treturn tt\n}\n\nfunc BenchmarkAddGigasecond(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tAddGigasecond(time.Time{})\n\t}\n}\n" - }, - "fresh": false, - "id": "go/gigasecond", - "language": "go", - "name": "Gigasecond", - "slug": "gigasecond", - "track_id": "go" - }, - { - "files": { - "README.md": "# Leap\n\nWrite a program that will take a year and report if it is a leap year.\n\nThe tricky thing here is that a leap year occurs:\n\n```plain\non every year that is evenly divisible by 4\n except every year that is evenly divisible by 100\n unless the year is also evenly divisible by 400\n```\n\nFor example, 1997 is not a leap year, but 1996 is. 1900 is not a leap\nyear, but 2000 is.\n\nIf your language provides a method in the standard library that does\nthis look-up, pretend it doesn't exist and implement it yourself.\n\n## Notes\n\nFor a delightful, four minute explanation of the whole leap year\nphenomenon, go watch [this youtube video][video].\n\n[video]: http://www.youtube.com/watch?v=xX96xng7sAE\n\n\n## Source\n\nJavaRanch Cattle Drive, exercise 3 [view source](http://www.javaranch.com/leap.jsp)\n", - "leap_test.go": "package leap\n\nimport (\n\t\"testing\"\n)\n\nvar testCases = []struct {\n\tyear int\n\texpected bool\n\tdescription string\n}{\n\t{1996, true, \"vanilla leap year\"},\n\t{1997, false, \"normal year\"},\n\t{1900, false, \"century\"},\n\t{2400, true, \"exceptional century\"},\n}\n\nfunc TestLeapYears(t *testing.T) {\n\tfor _, test := range testCases {\n\t\tobserved := IsLeapYear(test.year)\n\t\tif observed != test.expected {\n\t\t\tt.Fatalf(\"IsLeapYear(%d) = %t, want %t (%s)\",\n\t\t\t\ttest.year, observed, test.expected, test.description)\n\t\t}\n\t}\n}\n\nfunc BenchmarkLeapYears(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, test := range testCases {\n\t\t\tIsLeapYear(test.year)\n\t\t}\n\t}\n}\n" - }, - "fresh": false, - "id": "go/leap", - "language": "go", - "name": "Leap", - "slug": "leap", - "track_id": "go" - }, - { - "files": { - "README.md": "# Raindrops\n\nWrite a program that converts a number to a string, the contents of which depends on the number's prime factors.\n\n- If the number contains 3 as a prime factor, output 'Pling'.\n- If the number contains 5 as a prime factor, output 'Plang'.\n- If the number contains 7 as a prime factor, output 'Plong'.\n- If the number does not contain 3, 5, or 7 as a prime factor,\n just pass the number's digits straight through.\n\n## Examples\n\n- 28's prime-factorization is 2, 2, 7.\n - In raindrop-speak, this would be a simple \"Plong\".\n- 1755 prime-factorization is 3, 3, 3, 5, 13.\n - In raindrop-speak, this would be a \"PlingPlang\".\n- The prime factors of 34 are 2 and 17.\n - Raindrop-speak doesn't know what to make of that,\n so it just goes with the straightforward \"34\".\n\n\n## Source\n\nA variation on a famous interview question intended to weed out potential candidates. [view source](http://jumpstartlab.com)\n", - "raindrops_test.go": "package raindrops\n\nimport \"testing\"\n\nvar tests = []struct {\n\tinput int\n\texpected string\n}{\n\t{1, \"1\"},\n\t{3, \"Pling\"},\n\t{5, \"Plang\"},\n\t{7, \"Plong\"},\n\t{6, \"Pling\"},\n\t{9, \"Pling\"},\n\t{10, \"Plang\"},\n\t{14, \"Plong\"},\n\t{15, \"PlingPlang\"},\n\t{21, \"PlingPlong\"},\n\t{25, \"Plang\"},\n\t{35, \"PlangPlong\"},\n\t{49, \"Plong\"},\n\t{52, \"52\"},\n\t{105, \"PlingPlangPlong\"},\n\t{12121, \"12121\"},\n}\n\nfunc TestConvert(t *testing.T) {\n\tfor _, test := range tests {\n\t\tif actual := Convert(test.input); actual != test.expected {\n\t\t\tt.Errorf(\"Convert(%d) = %q, expected %q.\",\n\t\t\t\ttest.input, actual, test.expected)\n\t\t}\n\t}\n}\n\nfunc BenchmarkConvert(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, test := range tests {\n\t\t\tConvert(test.input)\n\t\t}\n\t}\n}\n" - }, - "fresh": false, - "id": "go/raindrops", - "language": "go", - "name": "Raindrops", - "slug": "raindrops", - "track_id": "go" - } - ] -} diff --git a/fixtures/solution-dir/file.txt b/fixtures/solution-dir/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/solution-dir/workspace/exercise/.exercism/metadata.json b/fixtures/solution-dir/workspace/exercise/.exercism/metadata.json new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/solution-dir/workspace/exercise/file.txt b/fixtures/solution-dir/workspace/exercise/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/solution-dir/workspace/exercise/in/a/subdir/file.txt b/fixtures/solution-dir/workspace/exercise/in/a/subdir/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/solution-dir/workspace/not-exercise/file.txt b/fixtures/solution-dir/workspace/not-exercise/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/solution-path/creatures/gazelle-2/.exercism/metadata.json b/fixtures/solution-path/creatures/gazelle-2/.exercism/metadata.json new file mode 100644 index 000000000..c6d7de2c7 --- /dev/null +++ b/fixtures/solution-path/creatures/gazelle-2/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "bbb"} diff --git a/fixtures/solution-path/creatures/gazelle-3/.exercism/metadata.json b/fixtures/solution-path/creatures/gazelle-3/.exercism/metadata.json new file mode 100644 index 000000000..ae9e57c45 --- /dev/null +++ b/fixtures/solution-path/creatures/gazelle-3/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "ccc"} diff --git a/fixtures/solution-path/creatures/gazelle/.exercism/metadata.json b/fixtures/solution-path/creatures/gazelle/.exercism/metadata.json new file mode 100644 index 000000000..640669c6d --- /dev/null +++ b/fixtures/solution-path/creatures/gazelle/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "aaa"} diff --git a/fixtures/solutions/alpha/.exercism/metadata.json b/fixtures/solutions/alpha/.exercism/metadata.json new file mode 100644 index 000000000..139f8115b --- /dev/null +++ b/fixtures/solutions/alpha/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "alpha"} diff --git a/fixtures/solutions/bravo/.exercism/metadata.json b/fixtures/solutions/bravo/.exercism/metadata.json new file mode 100644 index 000000000..c1348f523 --- /dev/null +++ b/fixtures/solutions/bravo/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "bravo"} diff --git a/fixtures/solutions/charlie/.exercism/metadata.json b/fixtures/solutions/charlie/.exercism/metadata.json new file mode 100644 index 000000000..a9135f4cf --- /dev/null +++ b/fixtures/solutions/charlie/.exercism/metadata.json @@ -0,0 +1 @@ +{"id": "charlie"} diff --git a/fixtures/solutions/delta/.keep b/fixtures/solutions/delta/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/submission.json b/fixtures/submission.json deleted file mode 100644 index d618f990e..000000000 --- a/fixtures/submission.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "url": "127.0.0.1/submissions/a185fcdafd094882ae27b2b457857f8f", - "track_id": "go", - "slug": "leap" -} \ No newline at end of file diff --git a/fixtures/submit.json b/fixtures/submit.json deleted file mode 100644 index d40cd8d23..000000000 --- a/fixtures/submit.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error": null, - "url": "a url", - "track_id": "a track_id", - "language": "ruby", - "iteration": 1 -} diff --git a/fixtures/tracks.json b/fixtures/tracks.json deleted file mode 100644 index aae48707d..000000000 --- a/fixtures/tracks.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "track":{ - "language":"Clojure", - "active":true, - "id":"clojure", - "slug":"clojure", - "problems":[ - "clojure/bob", - "clojure/rna-transcription", - "clojure/word-count", - "clojure/anagram", - "clojure/beer-song", - "clojure/nucleotide-count", - "clojure/point-mutations", - "clojure/phone-number", - "clojure/grade-school", - "clojure/robot-name", - "clojure/leap", - "clojure/etl", - "clojure/meetup", - "clojure/space-age", - "clojure/grains", - "clojure/gigasecond", - "clojure/triangle", - "clojure/scrabble-score", - "clojure/roman-numerals", - "clojure/binary", - "clojure/prime-factors", - "clojure/raindrops", - "clojure/allergies", - "clojure/atbash-cipher", - "clojure/bank-account", - "clojure/crypto-square", - "clojure/kindergarten-garden", - "clojure/robot-simulator", - "clojure/queen-attack", - "clojure/accumulate", - "clojure/binary-search-tree", - "clojure/difference-of-squares", - "clojure/hexadecimal", - "clojure/largest-series-product" - ], - "repository":"https://github.com/exercism/xclojure" - } -} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..e56c3f97a --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module github.com/exercism/cli + +go 1.20 + +require ( + github.com/blang/semver v3.5.1+incompatible + github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf + github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.23.0 + golang.org/x/text v0.14.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/sys v0.18.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..c042354a7 --- /dev/null +++ b/go.sum @@ -0,0 +1,492 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/paths/paths.go b/paths/paths.go deleted file mode 100644 index da3551dda..000000000 --- a/paths/paths.go +++ /dev/null @@ -1,121 +0,0 @@ -package paths - -import ( - "errors" - "os" - "path/filepath" - "runtime" - "strings" -) - -const ( - // File is the default name of the JSON file where the config written. - // The user can pass an alternate filename when using the CLI. - File = ".exercism.json" - // DirExercises is the default name of the directory for active users. - // Make this non-exported when handlers.Login is deleted. - DirExercises = "exercism" -) - -var ( - // Home by default will contact the location of your home directory. - Home string - - // XDGConfigHome will contain $XDG_CONFIG_HOME if it exists. - XDGConfigHome string - errHomeNotFound = errors.New("unable to locate home directory") -) - -func init() { - // on startup set default values - Recalculate() -} - -// Config will return the correct input path given any input. -// Blank input will return the default configuration location. -// Non-blank input will expand home to be an absolute path. -// If the target is known to be a directory, the config filename -// will be appended. -func Config(path string) string { - if path == "" { - if XDGConfigHome == "" { - return filepath.Join(Home, File) - } - - return filepath.Join(XDGConfigHome, File) - } - - expandedPath := expandPath(path) - if IsDir(path) { - expandedPath = filepath.Join(expandedPath, File) - } - return expandedPath -} - -// Exercises will return the correct exercises path given any input. -// Blank input will return the default location for exercises. -// Non-blank input will expand home to be an absolute path. -func Exercises(path string) string { - if path == "" { - return filepath.Join(Home, DirExercises) - } - return expandPath(path) -} - -// Recalculate sets exercism paths based on Home. -func Recalculate() { - if Home == "" { - home, err := findHome() - if err != nil { - panic(err) - } - Home = home - } - XDGConfigHome = os.Getenv("XDG_CONFIG_HOME") -} - -// IsDir determines whether the given path is a valid directory path. -func IsDir(path string) bool { - fi, _ := os.Stat(path) - return fi != nil && fi.IsDir() -} - -func expandPath(path string) string { - return makeAbsolute(expandHome(strings.TrimSpace(path))) -} - -func findHome() (string, error) { - var dir string - if runtime.GOOS == "windows" { - dir = os.Getenv("USERPROFILE") - if dir == "" { - dir = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - } - } else { - dir = os.Getenv("HOME") - } - - if dir == "" { - return "", errHomeNotFound - } - - return dir, nil -} - -func makeAbsolute(path string) string { - if !filepath.IsAbs(path) { - wd, err := os.Getwd() - if err != nil { - panic(err) - } - return filepath.Join(wd, path) - } - return path -} - -func expandHome(path string) string { - if path[:2] == "~"+string(os.PathSeparator) { - return strings.Replace(path, "~", Home, 1) - } - return path -} diff --git a/paths/paths_test.go b/paths/paths_test.go deleted file mode 100644 index b9325a0a4..000000000 --- a/paths/paths_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package paths - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHome(t *testing.T) { - assert.Equal(t, os.Getenv("HOME"), Home) -} - -func TestExercises(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - Home = "/test/home" - Recalculate() - - testCases := []struct { - givenPath string - expectedPath string - }{ - {"", "/test/home/exercism"}, - {"~/foobar", "/test/home/foobar"}, - {"/foobar/~/noexpand", "/foobar/~/noexpand"}, - {"/no/modification", "/no/modification"}, - {"relativePath", filepath.Join(dir, "relativePath")}, - } - - for _, testCase := range testCases { - actual := Exercises(testCase.givenPath) - assert.Equal(t, testCase.expectedPath, actual) - } -} - -func TestConfig(t *testing.T) { - dir, err := os.Getwd() - assert.NoError(t, err) - - Home = dir - Recalculate() - - testCases := []struct { - desc string - givenPath string - expectedPath string - }{ - { - "blank path", - "", - filepath.Join(Home, ".exercism.json"), - }, - { - "unknown path is expanded, but not modified", - "~/unknown", - filepath.Join(Home, "unknown"), - }, - { - "absolute path is unmodified", - Config(Config("")), - Config(""), - }, - { - "dir path has the config file appended", - dir, - filepath.Join(dir, File), - }, - } - - for _, tc := range testCases { - actual := Config(tc.givenPath) - assert.Equal(t, tc.expectedPath, actual, tc.desc) - } -} - -func TestXDGConfig(t *testing.T) { - XDGConfigHome = "/home/user/.xdg_config" - - assert.Equal(t, filepath.Join(XDGConfigHome, File), Config("")) - -} diff --git a/shell/README.md b/shell/README.md new file mode 100644 index 000000000..beee3c47a --- /dev/null +++ b/shell/README.md @@ -0,0 +1,54 @@ +## Executable +Unpack the archive relevant to your machine and place in $PATH + +## Shell Completion Scripts + +### Bash + + mkdir -p ~/.config/exercism + mv ../shell/exercism_completion.bash ~/.config/exercism/exercism_completion.bash + +Load the completion in your `.bashrc`, `.bash_profile` or `.profile` by +adding the following snippet: + + if [ -f ~/.config/exercism/exercism_completion.bash ]; then + source ~/.config/exercism/exercism_completion.bash + fi + +### Zsh + +Load up the completion by placing the `exercism_completion.zsh` somewhere on +your `$fpath` as `_exercism`. For example: + + mkdir -p ~/.zsh/functions + mv ../shell/exercism_completion.zsh ~/.zsh/functions/_exercism + +and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or +`.profile` before running `compinit`: + + export fpath=(~/.zsh/functions $fpath) + autoload -U compinit && compinit + + +#### Oh My Zsh + +If you are using the popular [Oh My Zsh][oh-my-zsh] framework to manage your +zsh plugins, you need to move the file `exercism_completion.zsh` to a new +custom plugin: + +[oh-my-zsh]: https://github.com/ohmyzsh/ohmyzsh + + mkdir -p $ZSH_CUSTOM/plugins/exercism + cp exercism_completion.zsh $ZSH_CUSTOM/plugins/exercism/_exercism + +Then edit the file `~/.zshrc` to include `exercism` in the list of plugins. +Completions will be activated the next time you open a new shell. If the +completions do not work, you should update Oh My Zsh to the latest version with +`omz update`. Oh My Zsh now checks whether the plugin list has changed (more +accurately, `$fpath`) and resets the `zcompdump` file. + +### Fish + +Completions must go in the user defined `$fish_complete_path`. By default, this is `~/.config/fish/completions` + + mv ../shell/exercism.fish ~/.config/fish/exercism.fish diff --git a/shell/exercism.fish b/shell/exercism.fish new file mode 100644 index 000000000..4abe9b21e --- /dev/null +++ b/shell/exercism.fish @@ -0,0 +1,57 @@ +# Configure +complete -f -c exercism -n "__fish_use_subcommand" -a "configure" -d "Writes config values to a JSON file." +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s t -l token -d "Set token" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s w -l workspace -d "Set workspace" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s a -l api -d "set API base url" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s s -l show -d "show settings" + +# Download +complete -f -c exercism -n "__fish_use_subcommand" -a "download" -d "Downloads and saves a specified submission into the local system" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s e -l exercise -d "the exercise slug" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s h -l help -d "help for download" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s t -l track -d "the track ID" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s u -l uuid -d "the solution UUID" + +# Help +complete -f -c exercism -n "__fish_use_subcommand" -a "help" -d "Shows a list of commands or help for one command" +complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit test troubleshoot upgrade version workspace" + +# Open +complete -f -c exercism -n "__fish_use_subcommand" -a "open" -d "Opens a browser to exercism.org for the specified submission." +complete -f -c exercism -n "__fish_seen_subcommand_from open" -s h -l help -d "help for open" + +# Submit +complete -f -c exercism -n "__fish_use_subcommand" -a "submit" -d "Submits a new iteration to a problem on exercism.org." +complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for submit" + +# Test +complete -f -c exercism -n "__fish_use_subcommand" -a "test" -d "Run the exercise's tests." +complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for test" + +# Troubleshoot +complete -f -c exercism -n "__fish_use_subcommand" -a "troubleshoot" -d "Outputs useful debug information." +complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s f -l full-api-key -d "display full API key (censored by default)" +complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s h -l help -d "help for troubleshoot" + +# Upgrade +complete -f -c exercism -n "__fish_use_subcommand" -a "upgrade" -d "Upgrades to the latest available version." +complete -f -c exercism -n "__fish_seen_subcommand_from help" -s h -l help -d "help for help" + +# Version +complete -f -c exercism -n "__fish_use_subcommand" -a "version" -d "Outputs version information." +complete -f -c exercism -n "__fish_seen_subcommand_from version" -s l -l latest -d "check latest available version" +complete -f -c exercism -n "__fish_seen_subcommand_from version" -s h -l help -d "help for version" + +# Workspace +complete -f -c exercism -n "__fish_use_subcommand" -a "workspace" -d "Outputs the root directory for Exercism exercises." +complete -f -c exercism -n "__fish_seen_subcommand_from workspace" -s h -l help -d "help for workspace" + +# Options +complete -f -c exercism -s h -l help -d "show help" +complete -f -c exercism -l timeout -a "10" -d "10 seconds" +complete -f -c exercism -l timeout -a "30" -d "30 seconds" +complete -f -c exercism -l timeout -a "60" -d "1 minute" +complete -f -c exercism -l timeout -a "300" -d "5 minutes" +complete -f -c exercism -l timeout -a "600" -d "10 minutes" +complete -f -c exercism -l timeout -a "" -d "override default HTTP timeout" +complete -f -c exercism -s v -l verbose -d "turn on verbose logging" diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash new file mode 100644 index 000000000..6864ec153 --- /dev/null +++ b/shell/exercism_completion.bash @@ -0,0 +1,51 @@ +_exercism () { + local cur prev + + COMPREPLY=() # Array variable storing the possible completions. + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + opts="--verbose --timeout" + + commands="configure download open + submit test troubleshoot upgrade version workspace help" + config_opts="--show" + version_opts="--latest" + + if [ "${#COMP_WORDS[@]}" -eq 2 ]; then + case "${cur}" in + -*) + COMPREPLY=( $( compgen -W "${opts}" -- "${cur}" ) ) + return 0 + ;; + *) + COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) + return 0 + ;; + esac + fi + + if [ "${#COMP_WORDS[@]}" -eq 3 ]; then + case "${prev}" in + configure) + COMPREPLY=( $( compgen -W "${config_opts}" -- "${cur}" ) ) + return 0 + ;; + version) + COMPREPLY=( $( compgen -W "${version_opts}" -- "${cur}" ) ) + return 0 + ;; + help) + COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) + return 0 + ;; + *) + return 0 + ;; + esac + fi + + return 0 +} + +complete -o bashdefault -o default -o nospace -F _exercism exercism 2>/dev/null \ + || complete -o default -o nospace -F _exercism exercism diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh new file mode 100644 index 000000000..542c8a576 --- /dev/null +++ b/shell/exercism_completion.zsh @@ -0,0 +1,35 @@ +#compdef exercism + +local curcontext="$curcontext" state line +typeset -A opt_args + +local -a options +options=(configure:"Writes config values to a JSON file." + download:"Downloads and saves a specified submission into the local system" + open:"Opens a browser to exercism.org for the specified submission." + submit:"Submits a new iteration to a problem on exercism.org." + test:"Run the exercise's tests." + troubleshoot:"Outputs useful debug information." + upgrade:"Upgrades to the latest available version." + version:"Outputs version information." + workspace:"Outputs the root directory for Exercism exercises." + help:"Shows a list of commands or help for one command") + +_arguments -s -S \ + {-h,--help}"[show help]" \ + {-t,--timeout}"[override default HTTP timeout]" \ + {-v,--verbose}"[turn on verbose logging]" \ + '(-): :->command' \ + '(-)*:: :->option-or-argument' \ + && return 0; + +case $state in + (command) + _describe 'commands' options ;; + (option-or-argument) + case $words[1] in + s*) + _files + ;; + esac +esac diff --git a/user/curriculum.go b/user/curriculum.go deleted file mode 100644 index 6b48001e3..000000000 --- a/user/curriculum.go +++ /dev/null @@ -1,77 +0,0 @@ -package user - -import ( - "fmt" - "strings" - - "github.com/exercism/cli/api" -) - -// Status is the status of a track (active/inactive). -type Status bool - -const ( - // TrackActive represents an active track. - // Problems from active tracks will be delivered with the `fetch` command. - TrackActive Status = true - // TrackInactive represents an inactive track. - // It is possible to fetch problems from an inactive track, and - // submit them to the website, but these will not automatically be - // delivered in the global `fetch` command. - TrackInactive Status = false -) - -// Curriculum is a collection of language tracks. -type Curriculum struct { - Tracks []*api.Track - wLang int - wID int -} - -// NewCurriculum returns a collection of language tracks. -func NewCurriculum(tracks []*api.Track) *Curriculum { - return &Curriculum{Tracks: tracks} -} - -// Report creates a table of the tracks that have the requested status. -func (cur *Curriculum) Report(status Status) { - for _, track := range cur.Tracks { - if Status(track.Active) == status { - fmt.Println( - " ", - track.Language, - strings.Repeat(" ", cur.lenLang()-len(track.Language)+1), - track.ID, - strings.Repeat(" ", cur.lenID()-len(track.ID)+1), - track.Len(), - "problems", - ) - } - } -} - -func (cur *Curriculum) lenLang() int { - if cur.wLang > 0 { - return cur.wLang - } - - for _, track := range cur.Tracks { - if len(track.Language) > cur.wLang { - cur.wLang = len(track.Language) - } - } - return cur.wLang -} - -func (cur *Curriculum) lenID() int { - if cur.wID > 0 { - return cur.wID - } - - for _, track := range cur.Tracks { - if len(track.ID) > cur.wID { - cur.wID = len(track.ID) - } - } - return cur.wID -} diff --git a/user/homework.go b/user/homework.go deleted file mode 100644 index a76ef4b29..000000000 --- a/user/homework.go +++ /dev/null @@ -1,167 +0,0 @@ -package user - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -// HWFilter is used to categorize homework items. -type HWFilter int - -// SummaryOption allows selective display of summary items. -type SummaryOption HWFilter - -const ( - // HWAll represents all items in the collection. - HWAll = iota - // HWUpdated represents problems where files have been added. - HWUpdated - // HWNew represents newly fetched problems. - HWNew - // HWNotSubmitted represents problems that have not yet been submitted for review. - HWNotSubmitted -) - -// Homework is a collection of problems that were fetched from the APIs. -type Homework struct { - Items []*Item - template string -} - -// NewHomework decorates a problem set with some additional data based on the -// user's system. -func NewHomework(problems []*api.Problem, c *config.Config) *Homework { - hw := Homework{} - for _, problem := range problems { - item := &Item{ - Problem: problem, - dir: c.Dir, - } - hw.Items = append(hw.Items, item) - } - - hw.template = "%s%s %s\n" - return &hw -} - -// Save saves all problems in the problem set. -func (hw *Homework) Save() error { - for _, item := range hw.Items { - if err := item.Save(); err != nil { - return err - } - } - return nil -} - -// RejectMissingTracks removes any items that are part of tracks the user -// doesn't currently have a folder for on their local machine. This -// only happens when a user calls `exercism fetch` without any arguments. -func (hw *Homework) RejectMissingTracks(dirMap map[string]bool) error { - items := []*Item{} - for _, item := range hw.Items { - dir := filepath.Join(item.dir, item.TrackID) - if dirMap[dir] { - items = append(items, item) - } - } - if len(items) == 0 { - return fmt.Errorf(` -You have yet to start a language track! -View all available language tracks with "exercism tracks" -Fetch exercises for your first track with "exercism fetch TRACK_ID"`) - } - hw.Items = items - return nil -} - -// ItemsMatching returns a subset of the set of problems. -func (hw *Homework) ItemsMatching(filter HWFilter) []*Item { - items := []*Item{} - for _, item := range hw.Items { - if item.Matches(filter) { - items = append(items, item) - } - } - return items -} - -// Report outputs a list of the problems in the set. -// It prints the track name, the problem name, and the full -// path to the problem on the user's filesystem. -func (hw *Homework) Report(filter HWFilter) { - if hw == nil { - return - } - width := hw.maxTitleWidth() - items := hw.ItemsMatching(filter) - hw.heading(filter, len(items), width) - for _, item := range items { - fmt.Print(item.Report(hw.template, width)) - } -} - -func (hw *Homework) heading(filter HWFilter, count, width int) { - if count == 0 { - return - } - fmt.Println() - - if filter == HWAll { - return - } - - unit := "problems" - if count == 1 { - unit = "problem" - } - - var status string - switch filter { - case HWUpdated: - status = "Updated:" - case HWNew: - status = "New:" - case HWNotSubmitted: - status = "Not Submitted:" - } - summary := fmt.Sprintf("%d %s", count, unit) - var padding string - if width > len(status) { - padding = strings.Repeat(" ", width-len(status)) - } - fmt.Printf(hw.template, status, padding, summary) -} - -func (hw *Homework) maxTitleWidth() int { - if hw == nil { - return 0 - } - var width int - for _, item := range hw.Items { - if len(item.String()) > width { - width = len(item.String()) - } - } - return width -} - -// Summarize prints a full report of new and updated items in the set. -func (hw *Homework) Summarize(summaryFilter SummaryOption) { - hw.Report(HWUpdated) - - if summaryFilter != HWNotSubmitted { - hw.Report(HWNotSubmitted) - } - - hw.Report(HWNew) - - fresh := len(hw.ItemsMatching(HWNew)) - updated := len(hw.ItemsMatching(HWUpdated)) - unchanged := len(hw.Items) - updated - fresh - fmt.Printf("\nunchanged: %d, updated: %d, new: %d\n\n", unchanged, updated, fresh) -} diff --git a/user/homework_test.go b/user/homework_test.go deleted file mode 100644 index 77244044a..000000000 --- a/user/homework_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package user - -import ( - "testing" - - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -func TestRejectMissingtracks(t *testing.T) { - problem1 := &api.Problem{ - TrackID: "go", - Slug: "clock", - Language: "Go", - Name: "Clock", - } - problem2 := &api.Problem{ - TrackID: "ruby", - Slug: "clock", - Language: "Ruby", - Name: "Clock", - } - dirMap := map[string]bool{ - "/tmp/go": true, - "/tmp/haskell": true, - } - emptyDirMap := make(map[string]bool) - - hw := NewHomework([]*api.Problem{problem1, problem2}, &config.Config{Dir: "/tmp"}) - err := hw.RejectMissingTracks(dirMap) - - if err != nil { - t.Error(err) - } - - if len(hw.Items) == 2 { - t.Error("Should have rejected the Ruby problem but did not reject any problems!") - } - - if len(hw.Items) == 1 && hw.Items[0].TrackID == "ruby" { - t.Error("Should have rejected the Ruby problem and rejected the Go problem instead!") - } - - if err := hw.RejectMissingTracks(emptyDirMap); err == nil { - t.Error("Should have returned error because user hasn't started any tracks but didn't!") - } -} diff --git a/user/item.go b/user/item.go deleted file mode 100644 index 27383d651..000000000 --- a/user/item.go +++ /dev/null @@ -1,83 +0,0 @@ -package user - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/exercism/cli/api" -) - -// Item is a problem that has been fetched from the APIs. -// It contains some data specific to this particular request and user -// in order to give a useful report to the user about what has been fetched. -type Item struct { - *api.Problem - dir string - isNew bool - isUpdated bool -} - -// Path is the location of this item on the user's filesystem. -func (it *Item) Path() string { - return filepath.Join(it.dir, it.TrackID, it.Slug) -} - -// Matches determines whether or not this item matches the given filter. -func (it *Item) Matches(filter HWFilter) bool { - switch filter { - case HWNew: - return it.isNew - case HWUpdated: - return it.isUpdated - case HWNotSubmitted: - return !it.Submitted - } - return true -} - -// Save writes the embedded problem to the filesystem. -func (it *Item) Save() error { - if _, err := os.Stat(it.Path()); err != nil { - if !os.IsNotExist(err) { - return err - } - it.isNew = true - } - - for name, text := range it.Files { - file := filepath.Join(it.Path(), name) - - if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { - return err - } - - if _, err := os.Stat(file); err != nil { - if !os.IsNotExist(err) { - return err - } - - if !it.isNew { - it.isUpdated = true - } - - if runtime.GOOS == "windows" { - text = strings.Replace(text, "\n", "\r\n", -1) - } - - if err := ioutil.WriteFile(file, []byte(text), 0644); err != nil { - return err - } - } - } - return nil -} - -// Report outputs the line's string and path in the format of the passed in template. -func (it *Item) Report(template string, max int) string { - padding := strings.Repeat(" ", max-len(it.String())) - return fmt.Sprintf(template, it.String(), padding, it.Path()) -} diff --git a/user/item_test.go b/user/item_test.go deleted file mode 100644 index 0bda1ad58..000000000 --- a/user/item_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package user - -import ( - "errors" - "testing" - - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" -) - -func TestItemReport(t *testing.T) { - testCases := []struct { - max int - expected string - }{ - {10, "Go (Clock) /tmp/go/clock\n"}, - {15, "Go (Clock) /tmp/go/clock\n"}, - {25, "Go (Clock) /tmp/go/clock\n"}, - } - - for _, tc := range testCases { - problem1 := &api.Problem{ - TrackID: "go", - Slug: "clock", - Language: "Go", - Name: "Clock", - } - - hw := NewHomework([]*api.Problem{problem1}, &config.Config{Dir: "/tmp"}) - if len(hw.Items) == 0 { - t.Fatal(errors.New("failed to initialize homework correctly")) - } - item := hw.Items[0] - actual := item.Report(hw.template, tc.max) - if tc.expected != actual { - t.Errorf("Expected:\n'%s', Got:\n'%s'\n", tc.expected, actual) - } - } -} diff --git a/vendor/github.com/codegangsta/cli/.travis.yml b/vendor/github.com/codegangsta/cli/.travis.yml deleted file mode 100644 index 87ba52f98..000000000 --- a/vendor/github.com/codegangsta/cli/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: go -sudo: false - -go: -- 1.0.3 -- 1.1.2 -- 1.2.2 -- 1.3.3 -- 1.4.2 -- 1.5.1 -- tip - -matrix: - allow_failures: - - go: tip - -script: -- go vet ./... -- go test -v ./... diff --git a/vendor/github.com/codegangsta/cli/LICENSE b/vendor/github.com/codegangsta/cli/LICENSE deleted file mode 100644 index 5515ccfb7..000000000 --- a/vendor/github.com/codegangsta/cli/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (C) 2013 Jeremy Saenz -All Rights Reserved. - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/codegangsta/cli/README.md b/vendor/github.com/codegangsta/cli/README.md deleted file mode 100644 index 26a183865..000000000 --- a/vendor/github.com/codegangsta/cli/README.md +++ /dev/null @@ -1,341 +0,0 @@ -[![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) -[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli) -[![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) - -# cli.go -`cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. - -## Overview -Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. - -**This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive! - -## Installation -Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). - -To install `cli.go`, simply run: -``` -$ go get github.com/codegangsta/cli -``` - -Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: -``` -export PATH=$PATH:$GOPATH/bin -``` - -## Getting Started -One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. - -``` go -package main - -import ( - "os" - "github.com/codegangsta/cli" -) - -func main() { - cli.NewApp().Run(os.Args) -} -``` - -This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: - -``` go -package main - -import ( - "os" - "github.com/codegangsta/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "boom" - app.Usage = "make an explosive entrance" - app.Action = func(c *cli.Context) { - println("boom! I say!") - } - - app.Run(os.Args) -} -``` - -Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below. - -## Example - -Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! - -Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it: - -``` go -package main - -import ( - "os" - "github.com/codegangsta/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "greet" - app.Usage = "fight the loneliness!" - app.Action = func(c *cli.Context) { - println("Hello friend!") - } - - app.Run(os.Args) -} -``` - -Install our command to the `$GOPATH/bin` directory: - -``` -$ go install -``` - -Finally run our new command: - -``` -$ greet -Hello friend! -``` - -`cli.go` also generates neat help text: - -``` -$ greet help -NAME: - greet - fight the loneliness! - -USAGE: - greet [global options] command [command options] [arguments...] - -VERSION: - 0.0.0 - -COMMANDS: - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS - --version Shows version information -``` - -### Arguments -You can lookup arguments by calling the `Args` function on `cli.Context`. - -``` go -... -app.Action = func(c *cli.Context) { - println("Hello", c.Args()[0]) -} -... -``` - -### Flags -Setting and querying flags is simple. -``` go -... -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, -} -app.Action = func(c *cli.Context) { - name := "someone" - if len(c.Args()) > 0 { - name = c.Args()[0] - } - if c.String("lang") == "spanish" { - println("Hola", name) - } else { - println("Hello", name) - } -} -... -``` - -You can also set a destination variable for a flag, to which the content will be scanned. -``` go -... -var language string -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, -} -app.Action = func(c *cli.Context) { - name := "someone" - if len(c.Args()) > 0 { - name = c.Args()[0] - } - if language == "spanish" { - println("Hola", name) - } else { - println("Hello", name) - } -} -... -``` - -See full list of flags at http://godoc.org/github.com/codegangsta/cli - -#### Alternate Names - -You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. - -``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - }, -} -``` - -That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. - -#### Values from the Environment - -You can also have the default value set from the environment via `EnvVar`. e.g. - -``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "APP_LANG", - }, -} -``` - -The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. - -``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", - }, -} -``` - -### Subcommands - -Subcommands can be defined for a more git-like command line app. -```go -... -app.Commands = []cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) { - println("added task: ", c.Args().First()) - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) { - println("completed task: ", c.Args().First()) - }, - }, - { - Name: "template", - Aliases: []string{"r"}, - Usage: "options for task templates", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(c *cli.Context) { - println("new task template: ", c.Args().First()) - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(c *cli.Context) { - println("removed task template: ", c.Args().First()) - }, - }, - }, - }, -} -... -``` - -### Bash Completion - -You can enable completion commands by setting the `EnableBashCompletion` -flag on the `App` object. By default, this setting will only auto-complete to -show an app's subcommands, but you can write your own completion methods for -the App or its subcommands. -```go -... -var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} -app := cli.NewApp() -app.EnableBashCompletion = true -app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) { - println("completed task: ", c.Args().First()) - }, - BashComplete: func(c *cli.Context) { - // This will complete if no args are passed - if len(c.Args()) > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } - }, - } -} -... -``` - -#### To Enable - -Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while -setting the `PROG` variable to the name of your program: - -`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` - -#### To Distribute - -Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename -it to the name of the program you wish to add autocomplete support for (or -automatically install it there if you are distributing a package). Don't forget -to source the file to make it active in the current shell. - -``` - sudo cp src/bash_autocomplete /etc/bash_completion.d/ - source /etc/bash_completion.d/ -``` - -Alternatively, you can just document that users should source the generic -`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set -to the name of their program (as above). - -## Contribution Guidelines -Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. - -If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. - -If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. diff --git a/vendor/github.com/codegangsta/cli/app.go b/vendor/github.com/codegangsta/cli/app.go deleted file mode 100644 index 2f992d0d7..000000000 --- a/vendor/github.com/codegangsta/cli/app.go +++ /dev/null @@ -1,334 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path" - "time" -) - -// App is the main structure of a cli application. It is recomended that -// an app be created with the cli.NewApp() function -type App struct { - // The name of the program. Defaults to path.Base(os.Args[0]) - Name string - // Full name of command for help, defaults to Name - HelpName string - // Description of the program. - Usage string - // Description of the program argument format. - ArgsUsage string - // Version of the program - Version string - // List of commands to execute - Commands []Command - // List of flags to parse - Flags []Flag - // Boolean to enable bash completion commands - EnableBashCompletion bool - // Boolean to hide built-in help command - HideHelp bool - // Boolean to hide built-in version flag - HideVersion bool - // An action to execute when the bash-completion flag is set - BashComplete func(context *Context) - // An action to execute before any subcommands are run, but after the context is ready - // If a non-nil error is returned, no subcommands are run - Before func(context *Context) error - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After func(context *Context) error - // The action to execute when no subcommands are specified - Action func(context *Context) - // Execute this function if the proper command cannot be found - CommandNotFound func(context *Context, command string) - // Compilation date - Compiled time.Time - // List of all authors who contributed - Authors []Author - // Copyright of the binary if any - Copyright string - // Name of Author (Note: Use App.Authors, this is deprecated) - Author string - // Email of Author (Note: Use App.Authors, this is deprecated) - Email string - // Writer writer to write output to - Writer io.Writer -} - -// Tries to find out when this binary was compiled. -// Returns the current time if it fails to find it. -func compileTime() time.Time { - info, err := os.Stat(os.Args[0]) - if err != nil { - return time.Now() - } - return info.ModTime() -} - -// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. -func NewApp() *App { - return &App{ - Name: path.Base(os.Args[0]), - HelpName: path.Base(os.Args[0]), - Usage: "A new cli application", - Version: "0.0.0", - BashComplete: DefaultAppComplete, - Action: helpCommand.Action, - Compiled: compileTime(), - Writer: os.Stdout, - } -} - -// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { - if a.Author != "" || a.Email != "" { - a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) - } - - newCmds := []Command{} - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - // append help to commands - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - - //append version/help flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - - if !a.HideVersion { - a.appendFlag(VersionFlag) - } - - // parse flags - set := flagSet(a.Name, a.Flags) - set.SetOutput(ioutil.Discard) - err = set.Parse(arguments[1:]) - nerr := normalizeFlags(a.Flags, set) - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - context := NewContext(a, set, nil) - ShowAppHelp(context) - return nerr - } - context := NewContext(a, set, nil) - - if checkCompletions(context) { - return nil - } - - if err != nil { - fmt.Fprintln(a.Writer, "Incorrect Usage.") - fmt.Fprintln(a.Writer) - ShowAppHelp(context) - return err - } - - if !a.HideHelp && checkHelp(context) { - ShowAppHelp(context) - return nil - } - - if !a.HideVersion && checkVersion(context) { - ShowVersion(context) - return nil - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - err := a.Before(context) - if err != nil { - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - a.Action(context) - return nil -} - -// Another entry point to the cli app, takes care of passing arguments and error handling -func (a *App) RunAndExitOnError() { - if err := a.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { - // append help to commands - if len(a.Commands) > 0 { - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - } - - newCmds := []Command{} - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - // append flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - - // parse flags - set := flagSet(a.Name, a.Flags) - set.SetOutput(ioutil.Discard) - err = set.Parse(ctx.Args().Tail()) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, ctx) - - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - fmt.Fprintln(a.Writer) - if len(a.Commands) > 0 { - ShowSubcommandHelp(context) - } else { - ShowCommandHelp(ctx, context.Args().First()) - } - return nerr - } - - if checkCompletions(context) { - return nil - } - - if err != nil { - fmt.Fprintln(a.Writer, "Incorrect Usage.") - fmt.Fprintln(a.Writer) - ShowSubcommandHelp(context) - return err - } - - if len(a.Commands) > 0 { - if checkSubcommandHelp(context) { - return nil - } - } else { - if checkCommandHelp(ctx, context.Args().First()) { - return nil - } - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - err := a.Before(context) - if err != nil { - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - a.Action(context) - - return nil -} - -// Returns the named command on App. Returns nil if the command does not exist -func (a *App) Command(name string) *Command { - for _, c := range a.Commands { - if c.HasName(name) { - return &c - } - } - - return nil -} - -func (a *App) hasFlag(flag Flag) bool { - for _, f := range a.Flags { - if flag == f { - return true - } - } - - return false -} - -func (a *App) appendFlag(flag Flag) { - if !a.hasFlag(flag) { - a.Flags = append(a.Flags, flag) - } -} - -// Author represents someone who has contributed to a cli project. -type Author struct { - Name string // The Authors name - Email string // The Authors email -} - -// String makes Author comply to the Stringer interface, to allow an easy print in the templating process -func (a Author) String() string { - e := "" - if a.Email != "" { - e = "<" + a.Email + "> " - } - - return fmt.Sprintf("%v %v", a.Name, e) -} diff --git a/vendor/github.com/codegangsta/cli/autocomplete/bash_autocomplete b/vendor/github.com/codegangsta/cli/autocomplete/bash_autocomplete deleted file mode 100644 index 21a232f1f..000000000 --- a/vendor/github.com/codegangsta/cli/autocomplete/bash_autocomplete +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/bash - -: ${PROG:=$(basename ${BASH_SOURCE})} - -_cli_bash_autocomplete() { - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - } - - complete -F _cli_bash_autocomplete $PROG diff --git a/vendor/github.com/codegangsta/cli/autocomplete/zsh_autocomplete b/vendor/github.com/codegangsta/cli/autocomplete/zsh_autocomplete deleted file mode 100644 index 5430a18f9..000000000 --- a/vendor/github.com/codegangsta/cli/autocomplete/zsh_autocomplete +++ /dev/null @@ -1,5 +0,0 @@ -autoload -U compinit && compinit -autoload -U bashcompinit && bashcompinit - -script_dir=$(dirname $0) -source ${script_dir}/bash_autocomplete diff --git a/vendor/github.com/codegangsta/cli/cli.go b/vendor/github.com/codegangsta/cli/cli.go deleted file mode 100644 index 31dc9124d..000000000 --- a/vendor/github.com/codegangsta/cli/cli.go +++ /dev/null @@ -1,40 +0,0 @@ -// Package cli provides a minimal framework for creating and organizing command line -// Go applications. cli is designed to be easy to understand and write, the most simple -// cli application can be written as follows: -// func main() { -// cli.NewApp().Run(os.Args) -// } -// -// Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := cli.NewApp() -// app.Name = "greet" -// app.Usage = "say a greeting" -// app.Action = func(c *cli.Context) { -// println("Greetings") -// } -// -// app.Run(os.Args) -// } -package cli - -import ( - "strings" -) - -type MultiError struct { - Errors []error -} - -func NewMultiError(err ...error) MultiError { - return MultiError{Errors: err} -} - -func (m MultiError) Error() string { - errs := make([]string, len(m.Errors)) - for i, err := range m.Errors { - errs[i] = err.Error() - } - - return strings.Join(errs, "\n") -} diff --git a/vendor/github.com/codegangsta/cli/command.go b/vendor/github.com/codegangsta/cli/command.go deleted file mode 100644 index 824e77bae..000000000 --- a/vendor/github.com/codegangsta/cli/command.go +++ /dev/null @@ -1,216 +0,0 @@ -package cli - -import ( - "fmt" - "io/ioutil" - "strings" -) - -// Command is a subcommand for a cli.App. -type Command struct { - // The name of the command - Name string - // short name of the command. Typically one character (deprecated, use `Aliases`) - ShortName string - // A list of aliases for the command - Aliases []string - // A short description of the usage of this command - Usage string - // A longer explanation of how the command works - Description string - // A short description of the arguments of this command - ArgsUsage string - // The function to call when checking for bash command completions - BashComplete func(context *Context) - // An action to execute before any sub-subcommands are run, but after the context is ready - // If a non-nil error is returned, no sub-subcommands are run - Before func(context *Context) error - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After func(context *Context) error - // The function to call when this command is invoked - Action func(context *Context) - // List of child commands - Subcommands []Command - // List of flags to parse - Flags []Flag - // Treat all flags as normal arguments if true - SkipFlagParsing bool - // Boolean to hide built-in help command - HideHelp bool - - // Full name of command for help, defaults to full command name, including parent commands. - HelpName string - commandNamePath []string -} - -// Returns the full name of the command. -// For subcommands this ensures that parent commands are part of the command path -func (c Command) FullName() string { - if c.commandNamePath == nil { - return c.Name - } - return strings.Join(c.commandNamePath, " ") -} - -// Invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) error { - if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil { - return c.startApp(ctx) - } - - if !c.HideHelp && (HelpFlag != BoolFlag{}) { - // append help to flags - c.Flags = append( - c.Flags, - HelpFlag, - ) - } - - if ctx.App.EnableBashCompletion { - c.Flags = append(c.Flags, BashCompletionFlag) - } - - set := flagSet(c.Name, c.Flags) - set.SetOutput(ioutil.Discard) - - var err error - if !c.SkipFlagParsing { - firstFlagIndex := -1 - terminatorIndex := -1 - for index, arg := range ctx.Args() { - if arg == "--" { - terminatorIndex = index - break - } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { - firstFlagIndex = index - } - } - - if firstFlagIndex > -1 { - args := ctx.Args() - regularArgs := make([]string, len(args[1:firstFlagIndex])) - copy(regularArgs, args[1:firstFlagIndex]) - - var flagArgs []string - if terminatorIndex > -1 { - flagArgs = args[firstFlagIndex:terminatorIndex] - regularArgs = append(regularArgs, args[terminatorIndex:]...) - } else { - flagArgs = args[firstFlagIndex:] - } - - err = set.Parse(append(flagArgs, regularArgs...)) - } else { - err = set.Parse(ctx.Args().Tail()) - } - } else { - if c.SkipFlagParsing { - err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) - } - } - - if err != nil { - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return err - } - - nerr := normalizeFlags(c.Flags, set) - if nerr != nil { - fmt.Fprintln(ctx.App.Writer, nerr) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return nerr - } - context := NewContext(ctx.App, set, ctx) - - if checkCommandCompletions(context, c.Name) { - return nil - } - - if checkCommandHelp(context, c.Name) { - return nil - } - context.Command = c - c.Action(context) - return nil -} - -func (c Command) Names() []string { - names := []string{c.Name} - - if c.ShortName != "" { - names = append(names, c.ShortName) - } - - return append(names, c.Aliases...) -} - -// Returns true if Command.Name or Command.ShortName matches given name -func (c Command) HasName(name string) bool { - for _, n := range c.Names() { - if n == name { - return true - } - } - return false -} - -func (c Command) startApp(ctx *Context) error { - app := NewApp() - - // set the name and usage - app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) - if c.HelpName == "" { - app.HelpName = c.HelpName - } else { - app.HelpName = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) - } - - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } - - // set CommandNotFound - app.CommandNotFound = ctx.App.CommandNotFound - - // set the flags and commands - app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - - app.Version = ctx.App.Version - app.HideVersion = ctx.App.HideVersion - app.Compiled = ctx.App.Compiled - app.Author = ctx.App.Author - app.Email = ctx.App.Email - app.Writer = ctx.App.Writer - - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - - // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpSubcommand.Action - } - - var newCmds []Command - for _, cc := range app.Commands { - cc.commandNamePath = []string{c.Name, cc.Name} - newCmds = append(newCmds, cc) - } - app.Commands = newCmds - - return app.RunAsSubcommand(ctx) -} diff --git a/vendor/github.com/codegangsta/cli/context.go b/vendor/github.com/codegangsta/cli/context.go deleted file mode 100644 index 0513d34f6..000000000 --- a/vendor/github.com/codegangsta/cli/context.go +++ /dev/null @@ -1,388 +0,0 @@ -package cli - -import ( - "errors" - "flag" - "strconv" - "strings" - "time" -) - -// Context is a type that is passed through to -// each Handler action in a cli application. Context -// can be used to retrieve context-specific Args and -// parsed command-line options. -type Context struct { - App *App - Command Command - flagSet *flag.FlagSet - setFlags map[string]bool - globalSetFlags map[string]bool - parentContext *Context -} - -// Creates a new context. For use in when invoking an App or Command action. -func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - return &Context{App: app, flagSet: set, parentContext: parentCtx} -} - -// Looks up the value of a local int flag, returns 0 if no int flag exists -func (c *Context) Int(name string) int { - return lookupInt(name, c.flagSet) -} - -// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists -func (c *Context) Duration(name string) time.Duration { - return lookupDuration(name, c.flagSet) -} - -// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists -func (c *Context) Float64(name string) float64 { - return lookupFloat64(name, c.flagSet) -} - -// Looks up the value of a local bool flag, returns false if no bool flag exists -func (c *Context) Bool(name string) bool { - return lookupBool(name, c.flagSet) -} - -// Looks up the value of a local boolT flag, returns false if no bool flag exists -func (c *Context) BoolT(name string) bool { - return lookupBoolT(name, c.flagSet) -} - -// Looks up the value of a local string flag, returns "" if no string flag exists -func (c *Context) String(name string) string { - return lookupString(name, c.flagSet) -} - -// Looks up the value of a local string slice flag, returns nil if no string slice flag exists -func (c *Context) StringSlice(name string) []string { - return lookupStringSlice(name, c.flagSet) -} - -// Looks up the value of a local int slice flag, returns nil if no int slice flag exists -func (c *Context) IntSlice(name string) []int { - return lookupIntSlice(name, c.flagSet) -} - -// Looks up the value of a local generic flag, returns nil if no generic flag exists -func (c *Context) Generic(name string) interface{} { - return lookupGeneric(name, c.flagSet) -} - -// Looks up the value of a global int flag, returns 0 if no int flag exists -func (c *Context) GlobalInt(name string) int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt(name, fs) - } - return 0 -} - -// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists -func (c *Context) GlobalDuration(name string) time.Duration { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -// Looks up the value of a global bool flag, returns false if no bool flag exists -func (c *Context) GlobalBool(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -// Looks up the value of a global string flag, returns "" if no string flag exists -func (c *Context) GlobalString(name string) string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -// Looks up the value of a global string slice flag, returns nil if no string slice flag exists -func (c *Context) GlobalStringSlice(name string) []string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -// Looks up the value of a global int slice flag, returns nil if no int slice flag exists -func (c *Context) GlobalIntSlice(name string) []int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -// Looks up the value of a global generic flag, returns nil if no generic flag exists -func (c *Context) GlobalGeneric(name string) interface{} { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil -} - -// Returns the number of flags set -func (c *Context) NumFlags() int { - return c.flagSet.NFlag() -} - -// Determines if the flag was actually set -func (c *Context) IsSet(name string) bool { - if c.setFlags == nil { - c.setFlags = make(map[string]bool) - c.flagSet.Visit(func(f *flag.Flag) { - c.setFlags[f.Name] = true - }) - } - return c.setFlags[name] == true -} - -// Determines if the global flag was actually set -func (c *Context) GlobalIsSet(name string) bool { - if c.globalSetFlags == nil { - c.globalSetFlags = make(map[string]bool) - ctx := c - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { - ctx.flagSet.Visit(func(f *flag.Flag) { - c.globalSetFlags[f.Name] = true - }) - } - } - return c.globalSetFlags[name] -} - -// Returns a slice of flag names used in this context. -func (c *Context) FlagNames() (names []string) { - for _, flag := range c.Command.Flags { - name := strings.Split(flag.GetName(), ",")[0] - if name == "help" { - continue - } - names = append(names, name) - } - return -} - -// Returns a slice of global flag names used by the app. -func (c *Context) GlobalFlagNames() (names []string) { - for _, flag := range c.App.Flags { - name := strings.Split(flag.GetName(), ",")[0] - if name == "help" || name == "version" { - continue - } - names = append(names, name) - } - return -} - -// Returns the parent context, if any -func (c *Context) Parent() *Context { - return c.parentContext -} - -type Args []string - -// Returns the command line arguments associated with the context. -func (c *Context) Args() Args { - args := Args(c.flagSet.Args()) - return args -} - -// Returns the nth argument, or else a blank string -func (a Args) Get(n int) string { - if len(a) > n { - return a[n] - } - return "" -} - -// Returns the first argument, or else a blank string -func (a Args) First() string { - return a.Get(0) -} - -// Return the rest of the arguments (not the first one) -// or else an empty string slice -func (a Args) Tail() []string { - if len(a) >= 2 { - return []string(a)[1:] - } - return []string{} -} - -// Checks if there are any arguments present -func (a Args) Present() bool { - return len(a) != 0 -} - -// Swaps arguments at the given indexes -func (a Args) Swap(from, to int) error { - if from >= len(a) || to >= len(a) { - return errors.New("index out of range") - } - a[from], a[to] = a[to], a[from] - return nil -} - -func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil; ctx = ctx.parentContext { - if f := ctx.flagSet.Lookup(name); f != nil { - return ctx.flagSet - } - } - return nil -} - -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - val, err := strconv.Atoi(f.Value.String()) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - val, err := time.ParseDuration(f.Value.String()) - if err == nil { - return val - } - } - - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - return f.Value.String() - } - - return "" -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*StringSlice)).Value() - - } - - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*IntSlice)).Value() - - } - - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - return f.Value - } - return nil -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return val - } - - return false -} - -func lookupBoolT(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return true - } - return val - } - - return false -} - -func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { - switch ff.Value.(type) { - case *StringSlice: - default: - set.Set(name, ff.Value.String()) - } -} - -func normalizeFlags(flags []Flag, set *flag.FlagSet) error { - visited := make(map[string]bool) - set.Visit(func(f *flag.Flag) { - visited[f.Name] = true - }) - for _, f := range flags { - parts := strings.Split(f.GetName(), ",") - if len(parts) == 1 { - continue - } - var ff *flag.Flag - for _, name := range parts { - name = strings.Trim(name, " ") - if visited[name] { - if ff != nil { - return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) - } - ff = set.Lookup(name) - } - } - if ff == nil { - continue - } - for _, name := range parts { - name = strings.Trim(name, " ") - if !visited[name] { - copyFlag(name, ff, set) - } - } - } - return nil -} diff --git a/vendor/github.com/codegangsta/cli/flag.go b/vendor/github.com/codegangsta/cli/flag.go deleted file mode 100644 index 49f30994e..000000000 --- a/vendor/github.com/codegangsta/cli/flag.go +++ /dev/null @@ -1,527 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "os" - "strconv" - "strings" - "time" -) - -// This flag enables bash-completion for all commands and subcommands -var BashCompletionFlag = BoolFlag{ - Name: "generate-bash-completion", -} - -// This flag prints the version for the application -var VersionFlag = BoolFlag{ - Name: "version, v", - Usage: "print the version", -} - -// This flag prints the help for all commands and subcommands -// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand -// unless HideHelp is set to true) -var HelpFlag = BoolFlag{ - Name: "help, h", - Usage: "show help", -} - -// Flag is a common interface related to parsing flags in cli. -// For more advanced flag parsing techniques, it is recomended that -// this interface be implemented. -type Flag interface { - fmt.Stringer - // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) - GetName() string -} - -func flagSet(name string, flags []Flag) *flag.FlagSet { - set := flag.NewFlagSet(name, flag.ContinueOnError) - - for _, f := range flags { - f.Apply(set) - } - return set -} - -func eachName(longName string, fn func(string)) { - parts := strings.Split(longName, ",") - for _, name := range parts { - name = strings.Trim(name, " ") - fn(name) - } -} - -// Generic is a generic parseable type identified by a specific flag -type Generic interface { - Set(value string) error - String() string -} - -// GenericFlag is the flag type for types implementing Generic -type GenericFlag struct { - Name string - Value Generic - Usage string - EnvVar string -} - -// String returns the string representation of the generic flag to display the -// help text to the user (uses the String() method of the generic flag to show -// the value) -func (f GenericFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s \"%v\"\t%v", prefixFor(f.Name), f.Name, f.Value, f.Usage)) -} - -// Apply takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -func (f GenericFlag) Apply(set *flag.FlagSet) { - val := f.Value - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - val.Set(envVal) - break - } - } - } - - eachName(f.Name, func(name string) { - set.Var(f.Value, name, f.Usage) - }) -} - -func (f GenericFlag) GetName() string { - return f.Name -} - -// StringSlice is an opaque type for []string to satisfy flag.Value -type StringSlice []string - -// Set appends the string value to the list of values -func (f *StringSlice) Set(value string) error { - *f = append(*f, value) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *StringSlice) String() string { - return fmt.Sprintf("%s", *f) -} - -// Value returns the slice of strings set by this flag -func (f *StringSlice) Value() []string { - return *f -} - -// StringSlice is a string flag that can be specified multiple times on the -// command-line -type StringSliceFlag struct { - Name string - Value *StringSlice - Usage string - EnvVar string -} - -// String returns the usage -func (f StringSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f StringSliceFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - newVal := &StringSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - newVal.Set(s) - } - f.Value = newVal - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &StringSlice{} - } - set.Var(f.Value, name, f.Usage) - }) -} - -func (f StringSliceFlag) GetName() string { - return f.Name -} - -// StringSlice is an opaque type for []int to satisfy flag.Value -type IntSlice []int - -// Set parses the value into an integer and appends it to the list of values -func (f *IntSlice) Set(value string) error { - tmp, err := strconv.Atoi(value) - if err != nil { - return err - } else { - *f = append(*f, tmp) - } - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *IntSlice) String() string { - return fmt.Sprintf("%d", *f) -} - -// Value returns the slice of ints set by this flag -func (f *IntSlice) Value() []int { - return *f -} - -// IntSliceFlag is an int flag that can be specified multiple times on the -// command-line -type IntSliceFlag struct { - Name string - Value *IntSlice - Usage string - EnvVar string -} - -// String returns the usage -func (f IntSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f IntSliceFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - newVal := &IntSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - } - } - f.Value = newVal - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &IntSlice{} - } - set.Var(f.Value, name, f.Usage) - }) -} - -func (f IntSliceFlag) GetName() string { - return f.Name -} - -// BoolFlag is a switch that defaults to false -type BoolFlag struct { - Name string - Usage string - EnvVar string - Destination *bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f BoolFlag) Apply(set *flag.FlagSet) { - val := false - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool - } - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.BoolVar(f.Destination, name, val, f.Usage) - return - } - set.Bool(name, val, f.Usage) - }) -} - -func (f BoolFlag) GetName() string { - return f.Name -} - -// BoolTFlag this represents a boolean flag that is true by default, but can -// still be set to false by --some-flag=false -type BoolTFlag struct { - Name string - Usage string - EnvVar string - Destination *bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolTFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f BoolTFlag) Apply(set *flag.FlagSet) { - val := true - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool - break - } - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.BoolVar(f.Destination, name, val, f.Usage) - return - } - set.Bool(name, val, f.Usage) - }) -} - -func (f BoolTFlag) GetName() string { - return f.Name -} - -// StringFlag represents a flag that takes as string value -type StringFlag struct { - Name string - Value string - Usage string - EnvVar string - Destination *string -} - -// String returns the usage -func (f StringFlag) String() string { - var fmtString string - fmtString = "%s %v\t%v" - - if len(f.Value) > 0 { - fmtString = "%s \"%v\"\t%v" - } else { - fmtString = "%s %v\t%v" - } - - return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f StringFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - f.Value = envVal - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - return - } - set.String(name, f.Value, f.Usage) - }) -} - -func (f StringFlag) GetName() string { - return f.Name -} - -// IntFlag is a flag that takes an integer -// Errors if the value provided cannot be parsed -type IntFlag struct { - Name string - Value int - Usage string - EnvVar string - Destination *int -} - -// String returns the usage -func (f IntFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f IntFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = int(envValInt) - break - } - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.IntVar(f.Destination, name, f.Value, f.Usage) - return - } - set.Int(name, f.Value, f.Usage) - }) -} - -func (f IntFlag) GetName() string { - return f.Name -} - -// DurationFlag is a flag that takes a duration specified in Go's duration -// format: https://golang.org/pkg/time/#ParseDuration -type DurationFlag struct { - Name string - Value time.Duration - Usage string - EnvVar string - Destination *time.Duration -} - -// String returns a readable representation of this value (for usage defaults) -func (f DurationFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f DurationFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration - break - } - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.DurationVar(f.Destination, name, f.Value, f.Usage) - return - } - set.Duration(name, f.Value, f.Usage) - }) -} - -func (f DurationFlag) GetName() string { - return f.Name -} - -// Float64Flag is a flag that takes an float value -// Errors if the value provided cannot be parsed -type Float64Flag struct { - Name string - Value float64 - Usage string - EnvVar string - Destination *float64 -} - -// String returns the usage -func (f Float64Flag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f Float64Flag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) - } - } - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.Float64Var(f.Destination, name, f.Value, f.Usage) - return - } - set.Float64(name, f.Value, f.Usage) - }) -} - -func (f Float64Flag) GetName() string { - return f.Name -} - -func prefixFor(name string) (prefix string) { - if len(name) == 1 { - prefix = "-" - } else { - prefix = "--" - } - - return -} - -func prefixedNames(fullName string) (prefixed string) { - parts := strings.Split(fullName, ",") - for i, name := range parts { - name = strings.Trim(name, " ") - prefixed += prefixFor(name) + name - if i < len(parts)-1 { - prefixed += ", " - } - } - return -} - -func withEnvHint(envVar, str string) string { - envText := "" - if envVar != "" { - envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $")) - } - return str + envText -} diff --git a/vendor/github.com/codegangsta/cli/help.go b/vendor/github.com/codegangsta/cli/help.go deleted file mode 100644 index a246f63ac..000000000 --- a/vendor/github.com/codegangsta/cli/help.go +++ /dev/null @@ -1,246 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "strings" - "text/tabwriter" - "text/template" -) - -// The text template for the Default help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} - -USAGE: - {{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - {{if .Version}} -VERSION: - {{.Version}} - {{end}}{{if len .Authors}} -AUTHOR(S): - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{end}}{{if .Flags}} -GLOBAL OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{end}}{{if .Copyright }} -COPYRIGHT: - {{.Copyright}} - {{end}} -` - -// The text template for the command help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} - -USAGE: - {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if .Flags}} - -OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{ end }} -` - -// The text template for the subcommand help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} - -USAGE: - {{.HelpName}} command{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{if .Flags}} -OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{end}} -` - -var helpCommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) { - args := c.Args() - if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowAppHelp(c) - } - }, -} - -var helpSubcommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) { - args := c.Args() - if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowSubcommandHelp(c) - } - }, -} - -// Prints help for the App or Command -type helpPrinter func(w io.Writer, templ string, data interface{}) - -var HelpPrinter helpPrinter = printHelp - -// Prints version for the App -var VersionPrinter = printVersion - -func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) -} - -// Prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { - for _, name := range command.Names() { - fmt.Fprintln(c.App.Writer, name) - } - } -} - -// Prints help for the given command -func ShowCommandHelp(ctx *Context, command string) { - // show the subcommand help for a command with subcommands - if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return - } - - for _, c := range ctx.App.Commands { - if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) - return - } - } - - if ctx.App.CommandNotFound != nil { - ctx.App.CommandNotFound(ctx, command) - } else { - fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command) - } -} - -// Prints help for the given subcommand -func ShowSubcommandHelp(c *Context) { - ShowCommandHelp(c, c.Command.Name) -} - -// Prints the version number of the App -func ShowVersion(c *Context) { - VersionPrinter(c) -} - -func printVersion(c *Context) { - fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) -} - -// Prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App - if a != nil && a.BashComplete != nil { - a.BashComplete(c) - } -} - -// Prints the custom completions for a given command -func ShowCommandCompletions(ctx *Context, command string) { - c := ctx.App.Command(command) - if c != nil && c.BashComplete != nil { - c.BashComplete(ctx) - } -} - -func printHelp(out io.Writer, templ string, data interface{}) { - funcMap := template.FuncMap{ - "join": strings.Join, - } - - w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) - t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) - err := t.Execute(w, data) - if err != nil { - panic(err) - } - w.Flush() -} - -func checkVersion(c *Context) bool { - found := false - if VersionFlag.Name != "" { - eachName(VersionFlag.Name, func(name string) { - if c.GlobalBool(name) || c.Bool(name) { - found = true - } - }) - } - return found -} - -func checkHelp(c *Context) bool { - found := false - if HelpFlag.Name != "" { - eachName(HelpFlag.Name, func(name string) { - if c.GlobalBool(name) || c.Bool(name) { - found = true - } - }) - } - return found -} - -func checkCommandHelp(c *Context, name string) bool { - if c.Bool("h") || c.Bool("help") { - ShowCommandHelp(c, name) - return true - } - - return false -} - -func checkSubcommandHelp(c *Context) bool { - if c.GlobalBool("h") || c.GlobalBool("help") { - ShowSubcommandHelp(c) - return true - } - - return false -} - -func checkCompletions(c *Context) bool { - if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { - ShowCompletions(c) - return true - } - - return false -} - -func checkCommandCompletions(c *Context, name string) bool { - if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { - ShowCommandCompletions(c, name) - return true - } - - return false -} diff --git a/vendor/github.com/kardianos/osext/LICENSE b/vendor/github.com/kardianos/osext/LICENSE deleted file mode 100644 index 744875676..000000000 --- a/vendor/github.com/kardianos/osext/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/kardianos/osext/README.md b/vendor/github.com/kardianos/osext/README.md deleted file mode 100644 index 61350baba..000000000 --- a/vendor/github.com/kardianos/osext/README.md +++ /dev/null @@ -1,16 +0,0 @@ -### Extensions to the "os" package. - -## Find the current Executable and ExecutableFolder. - -There is sometimes utility in finding the current executable file -that is running. This can be used for upgrading the current executable -or finding resources located relative to the executable file. Both -working directory and the os.Args[0] value are arbitrary and cannot -be relied on; os.Args[0] can be "faked". - -Multi-platform and supports: - * Linux - * OS X - * Windows - * Plan 9 - * BSDs. diff --git a/vendor/github.com/kardianos/osext/osext.go b/vendor/github.com/kardianos/osext/osext.go deleted file mode 100644 index c0de8b7f5..000000000 --- a/vendor/github.com/kardianos/osext/osext.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Extensions to the standard "os" package. -package osext - -import "path/filepath" - -var cx, ce = executableClean() - -func executableClean() (string, error) { - p, err := executable() - return filepath.Clean(p), err -} - -// Executable returns an absolute path that can be used to -// re-invoke the current program. -// It may not be valid after the current program exits. -func Executable() (string, error) { - return cx, ce -} - -// Returns same path as Executable, returns just the folder -// path. Excludes the executable name and any trailing slash. -func ExecutableFolder() (string, error) { - p, err := Executable() - if err != nil { - return "", err - } - - return filepath.Dir(p), nil -} diff --git a/vendor/github.com/kardianos/osext/osext_plan9.go b/vendor/github.com/kardianos/osext/osext_plan9.go deleted file mode 100644 index 655750c54..000000000 --- a/vendor/github.com/kardianos/osext/osext_plan9.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package osext - -import ( - "os" - "strconv" - "syscall" -) - -func executable() (string, error) { - f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text") - if err != nil { - return "", err - } - defer f.Close() - return syscall.Fd2path(int(f.Fd())) -} diff --git a/vendor/github.com/kardianos/osext/osext_procfs.go b/vendor/github.com/kardianos/osext/osext_procfs.go deleted file mode 100644 index d59847ee5..000000000 --- a/vendor/github.com/kardianos/osext/osext_procfs.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build linux netbsd solaris dragonfly - -package osext - -import ( - "errors" - "fmt" - "os" - "runtime" - "strings" -) - -func executable() (string, error) { - switch runtime.GOOS { - case "linux": - const deletedTag = " (deleted)" - execpath, err := os.Readlink("/proc/self/exe") - if err != nil { - return execpath, err - } - execpath = strings.TrimSuffix(execpath, deletedTag) - execpath = strings.TrimPrefix(execpath, deletedTag) - return execpath, nil - case "netbsd": - return os.Readlink("/proc/curproc/exe") - case "dragonfly": - return os.Readlink("/proc/curproc/file") - case "solaris": - return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid())) - } - return "", errors.New("ExecPath not implemented for " + runtime.GOOS) -} diff --git a/vendor/github.com/kardianos/osext/osext_sysctl.go b/vendor/github.com/kardianos/osext/osext_sysctl.go deleted file mode 100644 index 66da0bcf9..000000000 --- a/vendor/github.com/kardianos/osext/osext_sysctl.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin freebsd openbsd - -package osext - -import ( - "os" - "os/exec" - "path/filepath" - "runtime" - "syscall" - "unsafe" -) - -var initCwd, initCwdErr = os.Getwd() - -func executable() (string, error) { - var mib [4]int32 - switch runtime.GOOS { - case "freebsd": - mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1} - case "darwin": - mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1} - case "openbsd": - mib = [4]int32{1 /* CTL_KERN */, 55 /* KERN_PROC_ARGS */, int32(os.Getpid()), 1 /* KERN_PROC_ARGV */} - } - - n := uintptr(0) - // Get length. - _, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0) - if errNum != 0 { - return "", errNum - } - if n == 0 { // This shouldn't happen. - return "", nil - } - buf := make([]byte, n) - _, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0) - if errNum != 0 { - return "", errNum - } - if n == 0 { // This shouldn't happen. - return "", nil - } - - var execPath string - switch runtime.GOOS { - case "openbsd": - // buf now contains **argv, with pointers to each of the C-style - // NULL terminated arguments. - var args []string - argv := uintptr(unsafe.Pointer(&buf[0])) - Loop: - for { - argp := *(**[1 << 20]byte)(unsafe.Pointer(argv)) - if argp == nil { - break - } - for i := 0; uintptr(i) < n; i++ { - // we don't want the full arguments list - if string(argp[i]) == " " { - break Loop - } - if argp[i] != 0 { - continue - } - args = append(args, string(argp[:i])) - n -= uintptr(i) - break - } - if n < unsafe.Sizeof(argv) { - break - } - argv += unsafe.Sizeof(argv) - n -= unsafe.Sizeof(argv) - } - execPath = args[0] - // There is no canonical way to get an executable path on - // OpenBSD, so check PATH in case we are called directly - if execPath[0] != '/' && execPath[0] != '.' { - execIsInPath, err := exec.LookPath(execPath) - if err == nil { - execPath = execIsInPath - } - } - default: - for i, v := range buf { - if v == 0 { - buf = buf[:i] - break - } - } - execPath = string(buf) - } - - var err error - // execPath will not be empty due to above checks. - // Try to get the absolute path if the execPath is not rooted. - if execPath[0] != '/' { - execPath, err = getAbs(execPath) - if err != nil { - return execPath, err - } - } - // For darwin KERN_PROCARGS may return the path to a symlink rather than the - // actual executable. - if runtime.GOOS == "darwin" { - if execPath, err = filepath.EvalSymlinks(execPath); err != nil { - return execPath, err - } - } - return execPath, nil -} - -func getAbs(execPath string) (string, error) { - if initCwdErr != nil { - return execPath, initCwdErr - } - // The execPath may begin with a "../" or a "./" so clean it first. - // Join the two paths, trailing and starting slashes undetermined, so use - // the generic Join function. - return filepath.Join(initCwd, filepath.Clean(execPath)), nil -} diff --git a/vendor/github.com/kardianos/osext/osext_windows.go b/vendor/github.com/kardianos/osext/osext_windows.go deleted file mode 100644 index 72d282cf8..000000000 --- a/vendor/github.com/kardianos/osext/osext_windows.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package osext - -import ( - "syscall" - "unicode/utf16" - "unsafe" -) - -var ( - kernel = syscall.MustLoadDLL("kernel32.dll") - getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW") -) - -// GetModuleFileName() with hModule = NULL -func executable() (exePath string, err error) { - return getModuleFileName() -} - -func getModuleFileName() (string, error) { - var n uint32 - b := make([]uint16, syscall.MAX_PATH) - size := uint32(len(b)) - - r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size)) - n = uint32(r0) - if n == 0 { - return "", e1 - } - return string(utf16.Decode(b[0:n])), nil -} diff --git a/vendor/github.com/stretchr/testify/LICENCE.txt b/vendor/github.com/stretchr/testify/LICENCE.txt deleted file mode 100644 index a009ba467..000000000 --- a/vendor/github.com/stretchr/testify/LICENCE.txt +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell - -Please consider promoting this project if you find it useful. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go deleted file mode 100644 index 22e3a6bb9..000000000 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ /dev/null @@ -1,897 +0,0 @@ -package assert - -import ( - "bufio" - "bytes" - "fmt" - "math" - "reflect" - "regexp" - "runtime" - "strings" - "time" - "unicode" - "unicode/utf8" -) - -// TestingT is an interface wrapper around *testing.T -type TestingT interface { - Errorf(format string, args ...interface{}) -} - -// Comparison a custom function that returns true on success and false on failure -type Comparison func() (success bool) - -/* - Helper functions -*/ - -// ObjectsAreEqual determines if two objects are considered equal. -// -// This function does no assertion of any kind. -func ObjectsAreEqual(expected, actual interface{}) bool { - - if expected == nil || actual == nil { - return expected == actual - } - - if reflect.DeepEqual(expected, actual) { - return true - } - - return false - -} - -// ObjectsAreEqualValues gets whether two objects are equal, or if their -// values are equal. -func ObjectsAreEqualValues(expected, actual interface{}) bool { - if ObjectsAreEqual(expected, actual) { - return true - } - - actualType := reflect.TypeOf(actual) - expectedValue := reflect.ValueOf(expected) - if expectedValue.Type().ConvertibleTo(actualType) { - // Attempt comparison after type conversion - if reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) { - return true - } - } - - return false -} - -/* CallerInfo is necessary because the assert functions use the testing object -internally, causing it to print the file:line of the assert method, rather than where -the problem actually occured in calling code.*/ - -// CallerInfo returns an array of strings containing the file and line number -// of each stack frame leading from the current test to the assert call that -// failed. -func CallerInfo() []string { - - pc := uintptr(0) - file := "" - line := 0 - ok := false - name := "" - - callers := []string{} - for i := 0; ; i++ { - pc, file, line, ok = runtime.Caller(i) - if !ok { - return nil - } - - // This is a huge edge case, but it will panic if this is the case, see #180 - if file == "" { - break - } - - parts := strings.Split(file, "/") - dir := parts[len(parts)-2] - file = parts[len(parts)-1] - if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { - callers = append(callers, fmt.Sprintf("%s:%d", file, line)) - } - - f := runtime.FuncForPC(pc) - if f == nil { - break - } - name = f.Name() - // Drop the package - segments := strings.Split(name, ".") - name = segments[len(segments)-1] - if isTest(name, "Test") || - isTest(name, "Benchmark") || - isTest(name, "Example") { - break - } - } - - return callers -} - -// Stolen from the `go test` tool. -// isTest tells whether name looks like a test (or benchmark, according to prefix). -// It is a Test (say) if there is a character after Test that is not a lower-case letter. -// We don't want TesticularCancer. -func isTest(name, prefix string) bool { - if !strings.HasPrefix(name, prefix) { - return false - } - if len(name) == len(prefix) { // "Test" is ok - return true - } - rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) - return !unicode.IsLower(rune) -} - -// getWhitespaceString returns a string that is long enough to overwrite the default -// output from the go testing framework. -func getWhitespaceString() string { - - _, file, line, ok := runtime.Caller(1) - if !ok { - return "" - } - parts := strings.Split(file, "/") - file = parts[len(parts)-1] - - return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line))) - -} - -func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { - if len(msgAndArgs) == 0 || msgAndArgs == nil { - return "" - } - if len(msgAndArgs) == 1 { - return msgAndArgs[0].(string) - } - if len(msgAndArgs) > 1 { - return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) - } - return "" -} - -// Indents all lines of the message by appending a number of tabs to each line, in an output format compatible with Go's -// test printing (see inner comment for specifics) -func indentMessageLines(message string, tabs int) string { - outBuf := new(bytes.Buffer) - - for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { - if i != 0 { - outBuf.WriteRune('\n') - } - for ii := 0; ii < tabs; ii++ { - outBuf.WriteRune('\t') - // Bizarrely, all lines except the first need one fewer tabs prepended, so deliberately advance the counter - // by 1 prematurely. - if ii == 0 && i > 0 { - ii++ - } - } - outBuf.WriteString(scanner.Text()) - } - - return outBuf.String() -} - -// Fail reports a failure through -func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { - - message := messageFromMsgAndArgs(msgAndArgs...) - - errorTrace := strings.Join(CallerInfo(), "\n\r\t\t\t") - if len(message) > 0 { - t.Errorf("\r%s\r\tError Trace:\t%s\n"+ - "\r\tError:%s\n"+ - "\r\tMessages:\t%s\n\r", - getWhitespaceString(), - errorTrace, - indentMessageLines(failureMessage, 2), - message) - } else { - t.Errorf("\r%s\r\tError Trace:\t%s\n"+ - "\r\tError:%s\n\r", - getWhitespaceString(), - errorTrace, - indentMessageLines(failureMessage, 2)) - } - - return false -} - -// Implements asserts that an object is implemented by the specified interface. -// -// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject") -func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { - - interfaceType := reflect.TypeOf(interfaceObject).Elem() - - if !reflect.TypeOf(object).Implements(interfaceType) { - return Fail(t, fmt.Sprintf("Object must implement %v", interfaceType), msgAndArgs...) - } - - return true - -} - -// IsType asserts that the specified objects are of the same type. -func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { - - if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { - return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) - } - - return true -} - -// Equal asserts that two objects are equal. -// -// assert.Equal(t, 123, 123, "123 and 123 should be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { - - if !ObjectsAreEqual(expected, actual) { - return Fail(t, fmt.Sprintf("Not equal: %#v (expected)\n"+ - " != %#v (actual)", expected, actual), msgAndArgs...) - } - - return true - -} - -// EqualValues asserts that two objects are equal or convertable to the same types -// and equal. -// -// assert.EqualValues(t, uint32(123), int32(123), "123 and 123 should be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { - - if !ObjectsAreEqualValues(expected, actual) { - return Fail(t, fmt.Sprintf("Not equal: %#v (expected)\n"+ - " != %#v (actual)", expected, actual), msgAndArgs...) - } - - return true - -} - -// Exactly asserts that two objects are equal is value and type. -// -// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { - - aType := reflect.TypeOf(expected) - bType := reflect.TypeOf(actual) - - if aType != bType { - return Fail(t, "Types expected to match exactly", "%v != %v", aType, bType) - } - - return Equal(t, expected, actual, msgAndArgs...) - -} - -// NotNil asserts that the specified object is not nil. -// -// assert.NotNil(t, err, "err should be something") -// -// Returns whether the assertion was successful (true) or not (false). -func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - if !isNil(object) { - return true - } - return Fail(t, "Expected value not to be nil.", msgAndArgs...) -} - -// isNil checks if a specified object is nil or not, without Failing. -func isNil(object interface{}) bool { - if object == nil { - return true - } - - value := reflect.ValueOf(object) - kind := value.Kind() - if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { - return true - } - - return false -} - -// Nil asserts that the specified object is nil. -// -// assert.Nil(t, err, "err should be nothing") -// -// Returns whether the assertion was successful (true) or not (false). -func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - if isNil(object) { - return true - } - return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) -} - -var numericZeros = []interface{}{ - int(0), - int8(0), - int16(0), - int32(0), - int64(0), - uint(0), - uint8(0), - uint16(0), - uint32(0), - uint64(0), - float32(0), - float64(0), -} - -// isEmpty gets whether the specified object is considered empty or not. -func isEmpty(object interface{}) bool { - - if object == nil { - return true - } else if object == "" { - return true - } else if object == false { - return true - } - - for _, v := range numericZeros { - if object == v { - return true - } - } - - objValue := reflect.ValueOf(object) - - switch objValue.Kind() { - case reflect.Map: - fallthrough - case reflect.Slice, reflect.Chan: - { - return (objValue.Len() == 0) - } - case reflect.Ptr: - { - switch object.(type) { - case *time.Time: - return object.(*time.Time).IsZero() - default: - return false - } - } - } - return false -} - -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. -// -// assert.Empty(t, obj) -// -// Returns whether the assertion was successful (true) or not (false). -func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - - pass := isEmpty(object) - if !pass { - Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) - } - - return pass - -} - -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. -// -// if assert.NotEmpty(t, obj) { -// assert.Equal(t, "two", obj[1]) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - - pass := !isEmpty(object) - if !pass { - Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) - } - - return pass - -} - -// getLen try to get length of object. -// return (false, 0) if impossible. -func getLen(x interface{}) (ok bool, length int) { - v := reflect.ValueOf(x) - defer func() { - if e := recover(); e != nil { - ok = false - } - }() - return true, v.Len() -} - -// Len asserts that the specified object has specific length. -// Len also fails if the object has a type that len() not accept. -// -// assert.Len(t, mySlice, 3, "The size of slice is not 3") -// -// Returns whether the assertion was successful (true) or not (false). -func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool { - ok, l := getLen(object) - if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...) - } - - if l != length { - return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) - } - return true -} - -// True asserts that the specified value is true. -// -// assert.True(t, myBool, "myBool should be true") -// -// Returns whether the assertion was successful (true) or not (false). -func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { - - if value != true { - return Fail(t, "Should be true", msgAndArgs...) - } - - return true - -} - -// False asserts that the specified value is true. -// -// assert.False(t, myBool, "myBool should be false") -// -// Returns whether the assertion was successful (true) or not (false). -func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { - - if value != false { - return Fail(t, "Should be false", msgAndArgs...) - } - - return true - -} - -// NotEqual asserts that the specified values are NOT equal. -// -// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { - - if ObjectsAreEqual(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) - } - - return true - -} - -// containsElement try loop over the list check if the list includes the element. -// return (false, false) if impossible. -// return (true, false) if element was not found. -// return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { - - listValue := reflect.ValueOf(list) - elementValue := reflect.ValueOf(element) - defer func() { - if e := recover(); e != nil { - ok = false - found = false - } - }() - - if reflect.TypeOf(list).Kind() == reflect.String { - return true, strings.Contains(listValue.String(), elementValue.String()) - } - - for i := 0; i < listValue.Len(); i++ { - if ObjectsAreEqual(listValue.Index(i).Interface(), element) { - return true, true - } - } - return true, false - -} - -// Contains asserts that the specified string or list(array, slice...) contains the -// specified substring or element. -// -// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'") -// assert.Contains(t, ["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'") -// -// Returns whether the assertion was successful (true) or not (false). -func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { - - ok, found := includeElement(s, contains) - if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) - } - if !found { - return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...) - } - - return true - -} - -// NotContains asserts that the specified string or list(array, slice...) does NOT contain the -// specified substring or element. -// -// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") -// assert.NotContains(t, ["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'") -// -// Returns whether the assertion was successful (true) or not (false). -func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { - - ok, found := includeElement(s, contains) - if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) - } - if found { - return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) - } - - return true - -} - -// Condition uses a Comparison to assert a complex condition. -func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { - result := comp() - if !result { - Fail(t, "Condition failed!", msgAndArgs...) - } - return result -} - -// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics -// methods, and represents a simple func that takes no arguments, and returns nothing. -type PanicTestFunc func() - -// didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}) { - - didPanic := false - var message interface{} - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - } - }() - - // call the target function - f() - - }() - - return didPanic, message - -} - -// Panics asserts that the code inside the specified PanicTestFunc panics. -// -// assert.Panics(t, func(){ -// GoCrazy() -// }, "Calling GoCrazy() should panic") -// -// Returns whether the assertion was successful (true) or not (false). -func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { - - if funcDidPanic, panicValue := didPanic(f); !funcDidPanic { - return Fail(t, fmt.Sprintf("func %#v should panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) - } - - return true -} - -// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. -// -// assert.NotPanics(t, func(){ -// RemainCalm() -// }, "Calling RemainCalm() should NOT panic") -// -// Returns whether the assertion was successful (true) or not (false). -func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { - - if funcDidPanic, panicValue := didPanic(f); funcDidPanic { - return Fail(t, fmt.Sprintf("func %#v should not panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) - } - - return true -} - -// WithinDuration asserts that the two times are within duration delta of each other. -// -// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") -// -// Returns whether the assertion was successful (true) or not (false). -func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { - - dt := expected.Sub(actual) - if dt < -delta || dt > delta { - return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) - } - - return true -} - -func toFloat(x interface{}) (float64, bool) { - var xf float64 - xok := true - - switch xn := x.(type) { - case uint8: - xf = float64(xn) - case uint16: - xf = float64(xn) - case uint32: - xf = float64(xn) - case uint64: - xf = float64(xn) - case int: - xf = float64(xn) - case int8: - xf = float64(xn) - case int16: - xf = float64(xn) - case int32: - xf = float64(xn) - case int64: - xf = float64(xn) - case float32: - xf = float64(xn) - case float64: - xf = float64(xn) - default: - xok = false - } - - return xf, xok -} - -// InDelta asserts that the two numerals are within delta of each other. -// -// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) -// -// Returns whether the assertion was successful (true) or not (false). -func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { - - af, aok := toFloat(expected) - bf, bok := toFloat(actual) - - if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) - } - - if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Actual must not be NaN"), msgAndArgs...) - } - - if math.IsNaN(bf) { - return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...) - } - - dt := af - bf - if dt < -delta || dt > delta { - return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) - } - - return true -} - -// InDeltaSlice is the same as InDelta, except it compares two slices. -func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { - if expected == nil || actual == nil || - reflect.TypeOf(actual).Kind() != reflect.Slice || - reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) - } - - actualSlice := reflect.ValueOf(actual) - expectedSlice := reflect.ValueOf(expected) - - for i := 0; i < actualSlice.Len(); i++ { - result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta) - if !result { - return result - } - } - - return true -} - -// min(|expected|, |actual|) * epsilon -func calcEpsilonDelta(expected, actual interface{}, epsilon float64) float64 { - af, aok := toFloat(expected) - bf, bok := toFloat(actual) - - if !aok || !bok { - // invalid input - return 0 - } - - if af < 0 { - af = -af - } - if bf < 0 { - bf = -bf - } - var delta float64 - if af < bf { - delta = af * epsilon - } else { - delta = bf * epsilon - } - return delta -} - -// InEpsilon asserts that expected and actual have a relative error less than epsilon -// -// Returns whether the assertion was successful (true) or not (false). -func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { - delta := calcEpsilonDelta(expected, actual, epsilon) - - return InDelta(t, expected, actual, delta, msgAndArgs...) -} - -// InEpsilonSlice is the same as InEpsilon, except it compares two slices. -func InEpsilonSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { - if expected == nil || actual == nil || - reflect.TypeOf(actual).Kind() != reflect.Slice || - reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) - } - - actualSlice := reflect.ValueOf(actual) - expectedSlice := reflect.ValueOf(expected) - - for i := 0; i < actualSlice.Len(); i++ { - result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta) - if !result { - return result - } - } - - return true -} - -/* - Errors -*/ - -// NoError asserts that a function returned no error (i.e. `nil`). -// -// actualObj, err := SomeFunction() -// if assert.NoError(t, err) { -// assert.Equal(t, actualObj, expectedObj) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { - if isNil(err) { - return true - } - - return Fail(t, fmt.Sprintf("No error is expected but got %v", err), msgAndArgs...) -} - -// Error asserts that a function returned an error (i.e. not `nil`). -// -// actualObj, err := SomeFunction() -// if assert.Error(t, err, "An error was expected") { -// assert.Equal(t, err, expectedError) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { - - message := messageFromMsgAndArgs(msgAndArgs...) - return NotNil(t, err, "An error is expected but got nil. %s", message) - -} - -// EqualError asserts that a function returned an error (i.e. not `nil`) -// and that it is equal to the provided error. -// -// actualObj, err := SomeFunction() -// if assert.Error(t, err, "An error was expected") { -// assert.Equal(t, err, expectedError) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { - - message := messageFromMsgAndArgs(msgAndArgs...) - if !NotNil(t, theError, "An error is expected but got nil. %s", message) { - return false - } - s := "An error with value \"%s\" is expected but got \"%s\". %s" - return Equal(t, errString, theError.Error(), - s, errString, theError.Error(), message) -} - -// matchRegexp return true if a specified regexp matches a string. -func matchRegexp(rx interface{}, str interface{}) bool { - - var r *regexp.Regexp - if rr, ok := rx.(*regexp.Regexp); ok { - r = rr - } else { - r = regexp.MustCompile(fmt.Sprint(rx)) - } - - return (r.FindStringIndex(fmt.Sprint(str)) != nil) - -} - -// Regexp asserts that a specified regexp matches a string. -// -// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") -// assert.Regexp(t, "start...$", "it's not starting") -// -// Returns whether the assertion was successful (true) or not (false). -func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { - - match := matchRegexp(rx, str) - - if !match { - Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) - } - - return match -} - -// NotRegexp asserts that a specified regexp does not match a string. -// -// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") -// assert.NotRegexp(t, "^start", "it's not starting") -// -// Returns whether the assertion was successful (true) or not (false). -func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { - match := matchRegexp(rx, str) - - if match { - Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...) - } - - return !match - -} - -// Zero asserts that i is the zero value for its type and returns the truth. -func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { - if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { - return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) - } - return true -} - -// NotZero asserts that i is not the zero value for its type and returns the truth. -func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { - if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { - return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...) - } - return true -} diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go deleted file mode 100644 index c9dccc4d6..000000000 --- a/vendor/github.com/stretchr/testify/assert/doc.go +++ /dev/null @@ -1,45 +0,0 @@ -// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system. -// -// Example Usage -// -// The following is a complete example using assert in a standard test function: -// import ( -// "testing" -// "github.com/stretchr/testify/assert" -// ) -// -// func TestSomething(t *testing.T) { -// -// var a string = "Hello" -// var b string = "Hello" -// -// assert.Equal(t, a, b, "The two words should be the same.") -// -// } -// -// if you assert many times, use the format below: -// -// import ( -// "testing" -// "github.com/stretchr/testify/assert" -// ) -// -// func TestSomething(t *testing.T) { -// assert := assert.New(t) -// -// var a string = "Hello" -// var b string = "Hello" -// -// assert.Equal(a, b, "The two words should be the same.") -// } -// -// Assertions -// -// Assertions allow you to easily write test code, and are global funcs in the `assert` package. -// All assertion functions take, as the first argument, the `*testing.T` object provided by the -// testing framework. This allows the assertion funcs to write the failings and other details to -// the correct place. -// -// Every assertion function also takes an optional string message as the final argument, -// allowing custom error messages to be appended to the message the assertion method outputs. -package assert diff --git a/vendor/github.com/stretchr/testify/assert/errors.go b/vendor/github.com/stretchr/testify/assert/errors.go deleted file mode 100644 index ac9dc9d1d..000000000 --- a/vendor/github.com/stretchr/testify/assert/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package assert - -import ( - "errors" -) - -// AnError is an error instance useful for testing. If the code does not care -// about error specifics, and only needs to return the error for example, this -// error should be used to make the test code more readable. -var AnError = errors.New("assert.AnError general error for testing") diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions.go b/vendor/github.com/stretchr/testify/assert/forward_assertions.go deleted file mode 100644 index dc14771fe..000000000 --- a/vendor/github.com/stretchr/testify/assert/forward_assertions.go +++ /dev/null @@ -1,275 +0,0 @@ -package assert - -import "time" - -// Assertions provides assertion methods around the -// TestingT interface. -type Assertions struct { - t TestingT -} - -// New makes a new Assertions object for the specified TestingT. -func New(t TestingT) *Assertions { - return &Assertions{ - t: t, - } -} - -// Fail reports a failure through -func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool { - return Fail(a.t, failureMessage, msgAndArgs...) -} - -// Implements asserts that an object is implemented by the specified interface. -// -// assert.Implements((*MyInterface)(nil), new(MyObject), "MyObject") -func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { - return Implements(a.t, interfaceObject, object, msgAndArgs...) -} - -// IsType asserts that the specified objects are of the same type. -func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { - return IsType(a.t, expectedType, object, msgAndArgs...) -} - -// Equal asserts that two objects are equal. -// -// assert.Equal(123, 123, "123 and 123 should be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Equal(expected, actual interface{}, msgAndArgs ...interface{}) bool { - return Equal(a.t, expected, actual, msgAndArgs...) -} - -// EqualValues asserts that two objects are equal or convertable to the same types -// and equal. -// -// assert.EqualValues(uint32(123), int32(123), "123 and 123 should be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) EqualValues(expected, actual interface{}, msgAndArgs ...interface{}) bool { - return EqualValues(a.t, expected, actual, msgAndArgs...) -} - -// Exactly asserts that two objects are equal is value and type. -// -// assert.Exactly(int32(123), int64(123), "123 and 123 should NOT be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Exactly(expected, actual interface{}, msgAndArgs ...interface{}) bool { - return Exactly(a.t, expected, actual, msgAndArgs...) -} - -// NotNil asserts that the specified object is not nil. -// -// assert.NotNil(err, "err should be something") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool { - return NotNil(a.t, object, msgAndArgs...) -} - -// Nil asserts that the specified object is nil. -// -// assert.Nil(err, "err should be nothing") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool { - return Nil(a.t, object, msgAndArgs...) -} - -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or a -// slice with len == 0. -// -// assert.Empty(obj) -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { - return Empty(a.t, object, msgAndArgs...) -} - -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or a -// slice with len == 0. -// -// if assert.NotEmpty(obj) { -// assert.Equal("two", obj[1]) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool { - return NotEmpty(a.t, object, msgAndArgs...) -} - -// Len asserts that the specified object has specific length. -// Len also fails if the object has a type that len() not accept. -// -// assert.Len(mySlice, 3, "The size of slice is not 3") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool { - return Len(a.t, object, length, msgAndArgs...) -} - -// True asserts that the specified value is true. -// -// assert.True(myBool, "myBool should be true") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool { - return True(a.t, value, msgAndArgs...) -} - -// False asserts that the specified value is true. -// -// assert.False(myBool, "myBool should be false") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool { - return False(a.t, value, msgAndArgs...) -} - -// NotEqual asserts that the specified values are NOT equal. -// -// assert.NotEqual(obj1, obj2, "two objects shouldn't be equal") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotEqual(expected, actual interface{}, msgAndArgs ...interface{}) bool { - return NotEqual(a.t, expected, actual, msgAndArgs...) -} - -// Contains asserts that the specified string contains the specified substring. -// -// assert.Contains("Hello World", "World", "But 'Hello World' does contain 'World'") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Contains(s, contains interface{}, msgAndArgs ...interface{}) bool { - return Contains(a.t, s, contains, msgAndArgs...) -} - -// NotContains asserts that the specified string does NOT contain the specified substring. -// -// assert.NotContains("Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotContains(s, contains interface{}, msgAndArgs ...interface{}) bool { - return NotContains(a.t, s, contains, msgAndArgs...) -} - -// Condition uses a Comparison to assert a complex condition. -func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool { - return Condition(a.t, comp, msgAndArgs...) -} - -// Panics asserts that the code inside the specified PanicTestFunc panics. -// -// assert.Panics(func(){ -// GoCrazy() -// }, "Calling GoCrazy() should panic") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { - return Panics(a.t, f, msgAndArgs...) -} - -// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. -// -// assert.NotPanics(func(){ -// RemainCalm() -// }, "Calling RemainCalm() should NOT panic") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool { - return NotPanics(a.t, f, msgAndArgs...) -} - -// WithinDuration asserts that the two times are within duration delta of each other. -// -// assert.WithinDuration(time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) WithinDuration(expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { - return WithinDuration(a.t, expected, actual, delta, msgAndArgs...) -} - -// InDelta asserts that the two numerals are within delta of each other. -// -// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) InDelta(expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { - return InDelta(a.t, expected, actual, delta, msgAndArgs...) -} - -// InEpsilon asserts that expected and actual have a relative error less than epsilon -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) InEpsilon(expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { - return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) -} - -// NoError asserts that a function returned no error (i.e. `nil`). -// -// actualObj, err := SomeFunction() -// if assert.NoError(err) { -// assert.Equal(actualObj, expectedObj) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NoError(theError error, msgAndArgs ...interface{}) bool { - return NoError(a.t, theError, msgAndArgs...) -} - -// Error asserts that a function returned an error (i.e. not `nil`). -// -// actualObj, err := SomeFunction() -// if assert.Error(err, "An error was expected") { -// assert.Equal(err, expectedError) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Error(theError error, msgAndArgs ...interface{}) bool { - return Error(a.t, theError, msgAndArgs...) -} - -// EqualError asserts that a function returned an error (i.e. not `nil`) -// and that it is equal to the provided error. -// -// actualObj, err := SomeFunction() -// if assert.Error(err, "An error was expected") { -// assert.Equal(err, expectedError) -// } -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool { - return EqualError(a.t, theError, errString, msgAndArgs...) -} - -// Regexp asserts that a specified regexp matches a string. -// -// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") -// assert.Regexp(t, "start...$", "it's not starting") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { - return Regexp(a.t, rx, str, msgAndArgs...) -} - -// NotRegexp asserts that a specified regexp does not match a string. -// -// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") -// assert.NotRegexp(t, "^start", "it's not starting") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { - return NotRegexp(a.t, rx, str, msgAndArgs...) -} - -// Zero asserts that i is the zero value for its type and returns the truth. -func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool { - return Zero(a.t, i, msgAndArgs...) -} - -// NotZero asserts that i is not the zero value for its type and returns the truth. -func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool { - return NotZero(a.t, i, msgAndArgs...) -} diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions.go b/vendor/github.com/stretchr/testify/assert/http_assertions.go deleted file mode 100644 index 437a86ce4..000000000 --- a/vendor/github.com/stretchr/testify/assert/http_assertions.go +++ /dev/null @@ -1,157 +0,0 @@ -package assert - -import ( - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" -) - -// httpCode is a helper that returns HTTP code of the response. It returns -1 -// if building a new request fails. -func httpCode(handler http.HandlerFunc, method, url string, values url.Values) int { - w := httptest.NewRecorder() - req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) - if err != nil { - return -1 - } - handler(w, req) - return w.Code -} - -// HTTPSuccess asserts that a specified handler returns a success status code. -// -// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) -// -// Returns whether the assertion was successful (true) or not (false). -func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { - code := httpCode(handler, method, url, values) - if code == -1 { - return false - } - return code >= http.StatusOK && code <= http.StatusPartialContent -} - -// HTTPRedirect asserts that a specified handler returns a redirect status code. -// -// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). -func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { - code := httpCode(handler, method, url, values) - if code == -1 { - return false - } - return code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect -} - -// HTTPError asserts that a specified handler returns an error status code. -// -// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). -func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { - code := httpCode(handler, method, url, values) - if code == -1 { - return false - } - return code >= http.StatusBadRequest -} - -// HTTPBody is a helper that returns HTTP body of the response. It returns -// empty string if building a new request fails. -func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { - w := httptest.NewRecorder() - req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) - if err != nil { - return "" - } - handler(w, req) - return w.Body.String() -} - -// HTTPBodyContains asserts that a specified handler returns a -// body that contains a string. -// -// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). -func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { - body := HTTPBody(handler, method, url, values) - - contains := strings.Contains(body, fmt.Sprint(str)) - if !contains { - Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) - } - - return contains -} - -// HTTPBodyNotContains asserts that a specified handler returns a -// body that does not contain a string. -// -// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). -func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { - body := HTTPBody(handler, method, url, values) - - contains := strings.Contains(body, fmt.Sprint(str)) - if contains { - Fail(t, "Expected response body for %s to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body) - } - - return !contains -} - -// -// Assertions Wrappers -// - -// HTTPSuccess asserts that a specified handler returns a success status code. -// -// assert.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method, url string, values url.Values) bool { - return HTTPSuccess(a.t, handler, method, url, values) -} - -// HTTPRedirect asserts that a specified handler returns a redirect status code. -// -// assert.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method, url string, values url.Values) bool { - return HTTPRedirect(a.t, handler, method, url, values) -} - -// HTTPError asserts that a specified handler returns an error status code. -// -// assert.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) HTTPError(handler http.HandlerFunc, method, url string, values url.Values) bool { - return HTTPError(a.t, handler, method, url, values) -} - -// HTTPBodyContains asserts that a specified handler returns a -// body that contains a string. -// -// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { - return HTTPBodyContains(a.t, handler, method, url, values, str) -} - -// HTTPBodyNotContains asserts that a specified handler returns a -// body that does not contain a string. -// -// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). -func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { - return HTTPBodyNotContains(a.t, handler, method, url, values, str) -} diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE deleted file mode 100644 index 6a66aea5e..000000000 --- a/vendor/golang.org/x/net/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS deleted file mode 100644 index 733099041..000000000 --- a/vendor/golang.org/x/net/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the Go project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of Go, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of Go. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of Go or any code incorporated within this -implementation of Go constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of Go -shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/html/atom/atom.go b/vendor/golang.org/x/net/html/atom/atom.go deleted file mode 100644 index 227404bda..000000000 --- a/vendor/golang.org/x/net/html/atom/atom.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package atom provides integer codes (also known as atoms) for a fixed set of -// frequently occurring HTML strings: tag names and attribute keys such as "p" -// and "id". -// -// Sharing an atom's name between all elements with the same tag can result in -// fewer string allocations when tokenizing and parsing HTML. Integer -// comparisons are also generally faster than string comparisons. -// -// The value of an atom's particular code is not guaranteed to stay the same -// between versions of this package. Neither is any ordering guaranteed: -// whether atom.H1 < atom.H2 may also change. The codes are not guaranteed to -// be dense. The only guarantees are that e.g. looking up "div" will yield -// atom.Div, calling atom.Div.String will return "div", and atom.Div != 0. -package atom - -// Atom is an integer code for a string. The zero value maps to "". -type Atom uint32 - -// String returns the atom's name. -func (a Atom) String() string { - start := uint32(a >> 8) - n := uint32(a & 0xff) - if start+n > uint32(len(atomText)) { - return "" - } - return atomText[start : start+n] -} - -func (a Atom) string() string { - return atomText[a>>8 : a>>8+a&0xff] -} - -// fnv computes the FNV hash with an arbitrary starting value h. -func fnv(h uint32, s []byte) uint32 { - for i := range s { - h ^= uint32(s[i]) - h *= 16777619 - } - return h -} - -func match(s string, t []byte) bool { - for i, c := range t { - if s[i] != c { - return false - } - } - return true -} - -// Lookup returns the atom whose name is s. It returns zero if there is no -// such atom. The lookup is case sensitive. -func Lookup(s []byte) Atom { - if len(s) == 0 || len(s) > maxAtomLen { - return 0 - } - h := fnv(hash0, s) - if a := table[h&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { - return a - } - if a := table[(h>>16)&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { - return a - } - return 0 -} - -// String returns a string whose contents are equal to s. In that sense, it is -// equivalent to string(s) but may be more efficient. -func String(s []byte) string { - if a := Lookup(s); a != 0 { - return a.String() - } - return string(s) -} diff --git a/vendor/golang.org/x/net/html/atom/gen.go b/vendor/golang.org/x/net/html/atom/gen.go deleted file mode 100644 index 6bfa86601..000000000 --- a/vendor/golang.org/x/net/html/atom/gen.go +++ /dev/null @@ -1,648 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build ignore - -package main - -// This program generates table.go and table_test.go. -// Invoke as -// -// go run gen.go |gofmt >table.go -// go run gen.go -test |gofmt >table_test.go - -import ( - "flag" - "fmt" - "math/rand" - "os" - "sort" - "strings" -) - -// identifier converts s to a Go exported identifier. -// It converts "div" to "Div" and "accept-charset" to "AcceptCharset". -func identifier(s string) string { - b := make([]byte, 0, len(s)) - cap := true - for _, c := range s { - if c == '-' { - cap = true - continue - } - if cap && 'a' <= c && c <= 'z' { - c -= 'a' - 'A' - } - cap = false - b = append(b, byte(c)) - } - return string(b) -} - -var test = flag.Bool("test", false, "generate table_test.go") - -func main() { - flag.Parse() - - var all []string - all = append(all, elements...) - all = append(all, attributes...) - all = append(all, eventHandlers...) - all = append(all, extra...) - sort.Strings(all) - - if *test { - fmt.Printf("// generated by go run gen.go -test; DO NOT EDIT\n\n") - fmt.Printf("package atom\n\n") - fmt.Printf("var testAtomList = []string{\n") - for _, s := range all { - fmt.Printf("\t%q,\n", s) - } - fmt.Printf("}\n") - return - } - - // uniq - lists have dups - // compute max len too - maxLen := 0 - w := 0 - for _, s := range all { - if w == 0 || all[w-1] != s { - if maxLen < len(s) { - maxLen = len(s) - } - all[w] = s - w++ - } - } - all = all[:w] - - // Find hash that minimizes table size. - var best *table - for i := 0; i < 1000000; i++ { - if best != nil && 1<<(best.k-1) < len(all) { - break - } - h := rand.Uint32() - for k := uint(0); k <= 16; k++ { - if best != nil && k >= best.k { - break - } - var t table - if t.init(h, k, all) { - best = &t - break - } - } - } - if best == nil { - fmt.Fprintf(os.Stderr, "failed to construct string table\n") - os.Exit(1) - } - - // Lay out strings, using overlaps when possible. - layout := append([]string{}, all...) - - // Remove strings that are substrings of other strings - for changed := true; changed; { - changed = false - for i, s := range layout { - if s == "" { - continue - } - for j, t := range layout { - if i != j && t != "" && strings.Contains(s, t) { - changed = true - layout[j] = "" - } - } - } - } - - // Join strings where one suffix matches another prefix. - for { - // Find best i, j, k such that layout[i][len-k:] == layout[j][:k], - // maximizing overlap length k. - besti := -1 - bestj := -1 - bestk := 0 - for i, s := range layout { - if s == "" { - continue - } - for j, t := range layout { - if i == j { - continue - } - for k := bestk + 1; k <= len(s) && k <= len(t); k++ { - if s[len(s)-k:] == t[:k] { - besti = i - bestj = j - bestk = k - } - } - } - } - if bestk > 0 { - layout[besti] += layout[bestj][bestk:] - layout[bestj] = "" - continue - } - break - } - - text := strings.Join(layout, "") - - atom := map[string]uint32{} - for _, s := range all { - off := strings.Index(text, s) - if off < 0 { - panic("lost string " + s) - } - atom[s] = uint32(off<<8 | len(s)) - } - - // Generate the Go code. - fmt.Printf("// generated by go run gen.go; DO NOT EDIT\n\n") - fmt.Printf("package atom\n\nconst (\n") - for _, s := range all { - fmt.Printf("\t%s Atom = %#x\n", identifier(s), atom[s]) - } - fmt.Printf(")\n\n") - - fmt.Printf("const hash0 = %#x\n\n", best.h0) - fmt.Printf("const maxAtomLen = %d\n\n", maxLen) - - fmt.Printf("var table = [1<<%d]Atom{\n", best.k) - for i, s := range best.tab { - if s == "" { - continue - } - fmt.Printf("\t%#x: %#x, // %s\n", i, atom[s], s) - } - fmt.Printf("}\n") - datasize := (1 << best.k) * 4 - - fmt.Printf("const atomText =\n") - textsize := len(text) - for len(text) > 60 { - fmt.Printf("\t%q +\n", text[:60]) - text = text[60:] - } - fmt.Printf("\t%q\n\n", text) - - fmt.Fprintf(os.Stderr, "%d atoms; %d string bytes + %d tables = %d total data\n", len(all), textsize, datasize, textsize+datasize) -} - -type byLen []string - -func (x byLen) Less(i, j int) bool { return len(x[i]) > len(x[j]) } -func (x byLen) Swap(i, j int) { x[i], x[j] = x[j], x[i] } -func (x byLen) Len() int { return len(x) } - -// fnv computes the FNV hash with an arbitrary starting value h. -func fnv(h uint32, s string) uint32 { - for i := 0; i < len(s); i++ { - h ^= uint32(s[i]) - h *= 16777619 - } - return h -} - -// A table represents an attempt at constructing the lookup table. -// The lookup table uses cuckoo hashing, meaning that each string -// can be found in one of two positions. -type table struct { - h0 uint32 - k uint - mask uint32 - tab []string -} - -// hash returns the two hashes for s. -func (t *table) hash(s string) (h1, h2 uint32) { - h := fnv(t.h0, s) - h1 = h & t.mask - h2 = (h >> 16) & t.mask - return -} - -// init initializes the table with the given parameters. -// h0 is the initial hash value, -// k is the number of bits of hash value to use, and -// x is the list of strings to store in the table. -// init returns false if the table cannot be constructed. -func (t *table) init(h0 uint32, k uint, x []string) bool { - t.h0 = h0 - t.k = k - t.tab = make([]string, 1< len(t.tab) { - return false - } - s := t.tab[i] - h1, h2 := t.hash(s) - j := h1 + h2 - i - if t.tab[j] != "" && !t.push(j, depth+1) { - return false - } - t.tab[j] = s - return true -} - -// The lists of element names and attribute keys were taken from -// https://html.spec.whatwg.org/multipage/indices.html#index -// as of the "HTML Living Standard - Last Updated 21 February 2015" version. - -var elements = []string{ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "command", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hgroup", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "map", - "mark", - "menu", - "menuitem", - "meta", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "pre", - "progress", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "template", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", -} - -// https://html.spec.whatwg.org/multipage/indices.html#attributes-3 - -var attributes = []string{ - "abbr", - "accept", - "accept-charset", - "accesskey", - "action", - "alt", - "async", - "autocomplete", - "autofocus", - "autoplay", - "challenge", - "charset", - "checked", - "cite", - "class", - "cols", - "colspan", - "command", - "content", - "contenteditable", - "contextmenu", - "controls", - "coords", - "crossorigin", - "data", - "datetime", - "default", - "defer", - "dir", - "dirname", - "disabled", - "download", - "draggable", - "dropzone", - "enctype", - "for", - "form", - "formaction", - "formenctype", - "formmethod", - "formnovalidate", - "formtarget", - "headers", - "height", - "hidden", - "high", - "href", - "hreflang", - "http-equiv", - "icon", - "id", - "inputmode", - "ismap", - "itemid", - "itemprop", - "itemref", - "itemscope", - "itemtype", - "keytype", - "kind", - "label", - "lang", - "list", - "loop", - "low", - "manifest", - "max", - "maxlength", - "media", - "mediagroup", - "method", - "min", - "minlength", - "multiple", - "muted", - "name", - "novalidate", - "open", - "optimum", - "pattern", - "ping", - "placeholder", - "poster", - "preload", - "radiogroup", - "readonly", - "rel", - "required", - "reversed", - "rows", - "rowspan", - "sandbox", - "spellcheck", - "scope", - "scoped", - "seamless", - "selected", - "shape", - "size", - "sizes", - "sortable", - "sorted", - "span", - "src", - "srcdoc", - "srclang", - "start", - "step", - "style", - "tabindex", - "target", - "title", - "translate", - "type", - "typemustmatch", - "usemap", - "value", - "width", - "wrap", -} - -var eventHandlers = []string{ - "onabort", - "onautocomplete", - "onautocompleteerror", - "onafterprint", - "onbeforeprint", - "onbeforeunload", - "onblur", - "oncancel", - "oncanplay", - "oncanplaythrough", - "onchange", - "onclick", - "onclose", - "oncontextmenu", - "oncuechange", - "ondblclick", - "ondrag", - "ondragend", - "ondragenter", - "ondragleave", - "ondragover", - "ondragstart", - "ondrop", - "ondurationchange", - "onemptied", - "onended", - "onerror", - "onfocus", - "onhashchange", - "oninput", - "oninvalid", - "onkeydown", - "onkeypress", - "onkeyup", - "onlanguagechange", - "onload", - "onloadeddata", - "onloadedmetadata", - "onloadstart", - "onmessage", - "onmousedown", - "onmousemove", - "onmouseout", - "onmouseover", - "onmouseup", - "onmousewheel", - "onoffline", - "ononline", - "onpagehide", - "onpageshow", - "onpause", - "onplay", - "onplaying", - "onpopstate", - "onprogress", - "onratechange", - "onreset", - "onresize", - "onscroll", - "onseeked", - "onseeking", - "onselect", - "onshow", - "onsort", - "onstalled", - "onstorage", - "onsubmit", - "onsuspend", - "ontimeupdate", - "ontoggle", - "onunload", - "onvolumechange", - "onwaiting", -} - -// extra are ad-hoc values not covered by any of the lists above. -var extra = []string{ - "align", - "annotation", - "annotation-xml", - "applet", - "basefont", - "bgsound", - "big", - "blink", - "center", - "color", - "desc", - "face", - "font", - "foreignObject", // HTML is case-insensitive, but SVG-embedded-in-HTML is case-sensitive. - "foreignobject", - "frame", - "frameset", - "image", - "isindex", - "listing", - "malignmark", - "marquee", - "math", - "mglyph", - "mi", - "mn", - "mo", - "ms", - "mtext", - "nobr", - "noembed", - "noframes", - "plaintext", - "prompt", - "public", - "spacer", - "strike", - "svg", - "system", - "tt", - "xmp", -} diff --git a/vendor/golang.org/x/net/html/atom/table.go b/vendor/golang.org/x/net/html/atom/table.go deleted file mode 100644 index 2605ba310..000000000 --- a/vendor/golang.org/x/net/html/atom/table.go +++ /dev/null @@ -1,713 +0,0 @@ -// generated by go run gen.go; DO NOT EDIT - -package atom - -const ( - A Atom = 0x1 - Abbr Atom = 0x4 - Accept Atom = 0x2106 - AcceptCharset Atom = 0x210e - Accesskey Atom = 0x3309 - Action Atom = 0x1f606 - Address Atom = 0x4f307 - Align Atom = 0x1105 - Alt Atom = 0x4503 - Annotation Atom = 0x1670a - AnnotationXml Atom = 0x1670e - Applet Atom = 0x2b306 - Area Atom = 0x2fa04 - Article Atom = 0x38807 - Aside Atom = 0x8305 - Async Atom = 0x7b05 - Audio Atom = 0xa605 - Autocomplete Atom = 0x1fc0c - Autofocus Atom = 0xb309 - Autoplay Atom = 0xce08 - B Atom = 0x101 - Base Atom = 0xd604 - Basefont Atom = 0xd608 - Bdi Atom = 0x1a03 - Bdo Atom = 0xe703 - Bgsound Atom = 0x11807 - Big Atom = 0x12403 - Blink Atom = 0x12705 - Blockquote Atom = 0x12c0a - Body Atom = 0x2f04 - Br Atom = 0x202 - Button Atom = 0x13606 - Canvas Atom = 0x7f06 - Caption Atom = 0x1bb07 - Center Atom = 0x5b506 - Challenge Atom = 0x21f09 - Charset Atom = 0x2807 - Checked Atom = 0x32807 - Cite Atom = 0x3c804 - Class Atom = 0x4de05 - Code Atom = 0x14904 - Col Atom = 0x15003 - Colgroup Atom = 0x15008 - Color Atom = 0x15d05 - Cols Atom = 0x16204 - Colspan Atom = 0x16207 - Command Atom = 0x17507 - Content Atom = 0x42307 - Contenteditable Atom = 0x4230f - Contextmenu Atom = 0x3310b - Controls Atom = 0x18808 - Coords Atom = 0x19406 - Crossorigin Atom = 0x19f0b - Data Atom = 0x44a04 - Datalist Atom = 0x44a08 - Datetime Atom = 0x23c08 - Dd Atom = 0x26702 - Default Atom = 0x8607 - Defer Atom = 0x14b05 - Del Atom = 0x3ef03 - Desc Atom = 0x4db04 - Details Atom = 0x4807 - Dfn Atom = 0x6103 - Dialog Atom = 0x1b06 - Dir Atom = 0x6903 - Dirname Atom = 0x6907 - Disabled Atom = 0x10c08 - Div Atom = 0x11303 - Dl Atom = 0x11e02 - Download Atom = 0x40008 - Draggable Atom = 0x17b09 - Dropzone Atom = 0x39108 - Dt Atom = 0x50902 - Em Atom = 0x6502 - Embed Atom = 0x6505 - Enctype Atom = 0x21107 - Face Atom = 0x5b304 - Fieldset Atom = 0x1b008 - Figcaption Atom = 0x1b80a - Figure Atom = 0x1cc06 - Font Atom = 0xda04 - Footer Atom = 0x8d06 - For Atom = 0x1d803 - ForeignObject Atom = 0x1d80d - Foreignobject Atom = 0x1e50d - Form Atom = 0x1f204 - Formaction Atom = 0x1f20a - Formenctype Atom = 0x20d0b - Formmethod Atom = 0x2280a - Formnovalidate Atom = 0x2320e - Formtarget Atom = 0x2470a - Frame Atom = 0x9a05 - Frameset Atom = 0x9a08 - H1 Atom = 0x26e02 - H2 Atom = 0x29402 - H3 Atom = 0x2a702 - H4 Atom = 0x2e902 - H5 Atom = 0x2f302 - H6 Atom = 0x50b02 - Head Atom = 0x2d504 - Header Atom = 0x2d506 - Headers Atom = 0x2d507 - Height Atom = 0x25106 - Hgroup Atom = 0x25906 - Hidden Atom = 0x26506 - High Atom = 0x26b04 - Hr Atom = 0x27002 - Href Atom = 0x27004 - Hreflang Atom = 0x27008 - Html Atom = 0x25504 - HttpEquiv Atom = 0x2780a - I Atom = 0x601 - Icon Atom = 0x42204 - Id Atom = 0x8502 - Iframe Atom = 0x29606 - Image Atom = 0x29c05 - Img Atom = 0x2a103 - Input Atom = 0x3e805 - Inputmode Atom = 0x3e809 - Ins Atom = 0x1a803 - Isindex Atom = 0x2a907 - Ismap Atom = 0x2b005 - Itemid Atom = 0x33c06 - Itemprop Atom = 0x3c908 - Itemref Atom = 0x5ad07 - Itemscope Atom = 0x2b909 - Itemtype Atom = 0x2c308 - Kbd Atom = 0x1903 - Keygen Atom = 0x3906 - Keytype Atom = 0x53707 - Kind Atom = 0x10904 - Label Atom = 0xf005 - Lang Atom = 0x27404 - Legend Atom = 0x18206 - Li Atom = 0x1202 - Link Atom = 0x12804 - List Atom = 0x44e04 - Listing Atom = 0x44e07 - Loop Atom = 0xf404 - Low Atom = 0x11f03 - Malignmark Atom = 0x100a - Manifest Atom = 0x5f108 - Map Atom = 0x2b203 - Mark Atom = 0x1604 - Marquee Atom = 0x2cb07 - Math Atom = 0x2d204 - Max Atom = 0x2e103 - Maxlength Atom = 0x2e109 - Media Atom = 0x6e05 - Mediagroup Atom = 0x6e0a - Menu Atom = 0x33804 - Menuitem Atom = 0x33808 - Meta Atom = 0x45d04 - Meter Atom = 0x24205 - Method Atom = 0x22c06 - Mglyph Atom = 0x2a206 - Mi Atom = 0x2eb02 - Min Atom = 0x2eb03 - Minlength Atom = 0x2eb09 - Mn Atom = 0x23502 - Mo Atom = 0x3ed02 - Ms Atom = 0x2bc02 - Mtext Atom = 0x2f505 - Multiple Atom = 0x30308 - Muted Atom = 0x30b05 - Name Atom = 0x6c04 - Nav Atom = 0x3e03 - Nobr Atom = 0x5704 - Noembed Atom = 0x6307 - Noframes Atom = 0x9808 - Noscript Atom = 0x3d208 - Novalidate Atom = 0x2360a - Object Atom = 0x1ec06 - Ol Atom = 0xc902 - Onabort Atom = 0x13a07 - Onafterprint Atom = 0x1c00c - Onautocomplete Atom = 0x1fa0e - Onautocompleteerror Atom = 0x1fa13 - Onbeforeprint Atom = 0x6040d - Onbeforeunload Atom = 0x4e70e - Onblur Atom = 0xaa06 - Oncancel Atom = 0xe908 - Oncanplay Atom = 0x28509 - Oncanplaythrough Atom = 0x28510 - Onchange Atom = 0x3a708 - Onclick Atom = 0x31007 - Onclose Atom = 0x31707 - Oncontextmenu Atom = 0x32f0d - Oncuechange Atom = 0x3420b - Ondblclick Atom = 0x34d0a - Ondrag Atom = 0x35706 - Ondragend Atom = 0x35709 - Ondragenter Atom = 0x3600b - Ondragleave Atom = 0x36b0b - Ondragover Atom = 0x3760a - Ondragstart Atom = 0x3800b - Ondrop Atom = 0x38f06 - Ondurationchange Atom = 0x39f10 - Onemptied Atom = 0x39609 - Onended Atom = 0x3af07 - Onerror Atom = 0x3b607 - Onfocus Atom = 0x3bd07 - Onhashchange Atom = 0x3da0c - Oninput Atom = 0x3e607 - Oninvalid Atom = 0x3f209 - Onkeydown Atom = 0x3fb09 - Onkeypress Atom = 0x4080a - Onkeyup Atom = 0x41807 - Onlanguagechange Atom = 0x43210 - Onload Atom = 0x44206 - Onloadeddata Atom = 0x4420c - Onloadedmetadata Atom = 0x45510 - Onloadstart Atom = 0x46b0b - Onmessage Atom = 0x47609 - Onmousedown Atom = 0x47f0b - Onmousemove Atom = 0x48a0b - Onmouseout Atom = 0x4950a - Onmouseover Atom = 0x4a20b - Onmouseup Atom = 0x4ad09 - Onmousewheel Atom = 0x4b60c - Onoffline Atom = 0x4c209 - Ononline Atom = 0x4cb08 - Onpagehide Atom = 0x4d30a - Onpageshow Atom = 0x4fe0a - Onpause Atom = 0x50d07 - Onplay Atom = 0x51706 - Onplaying Atom = 0x51709 - Onpopstate Atom = 0x5200a - Onprogress Atom = 0x52a0a - Onratechange Atom = 0x53e0c - Onreset Atom = 0x54a07 - Onresize Atom = 0x55108 - Onscroll Atom = 0x55f08 - Onseeked Atom = 0x56708 - Onseeking Atom = 0x56f09 - Onselect Atom = 0x57808 - Onshow Atom = 0x58206 - Onsort Atom = 0x58b06 - Onstalled Atom = 0x59509 - Onstorage Atom = 0x59e09 - Onsubmit Atom = 0x5a708 - Onsuspend Atom = 0x5bb09 - Ontimeupdate Atom = 0xdb0c - Ontoggle Atom = 0x5c408 - Onunload Atom = 0x5cc08 - Onvolumechange Atom = 0x5d40e - Onwaiting Atom = 0x5e209 - Open Atom = 0x3cf04 - Optgroup Atom = 0xf608 - Optimum Atom = 0x5eb07 - Option Atom = 0x60006 - Output Atom = 0x49c06 - P Atom = 0xc01 - Param Atom = 0xc05 - Pattern Atom = 0x5107 - Ping Atom = 0x7704 - Placeholder Atom = 0xc30b - Plaintext Atom = 0xfd09 - Poster Atom = 0x15706 - Pre Atom = 0x25e03 - Preload Atom = 0x25e07 - Progress Atom = 0x52c08 - Prompt Atom = 0x5fa06 - Public Atom = 0x41e06 - Q Atom = 0x13101 - Radiogroup Atom = 0x30a - Readonly Atom = 0x2fb08 - Rel Atom = 0x25f03 - Required Atom = 0x1d008 - Reversed Atom = 0x5a08 - Rows Atom = 0x9204 - Rowspan Atom = 0x9207 - Rp Atom = 0x1c602 - Rt Atom = 0x13f02 - Ruby Atom = 0xaf04 - S Atom = 0x2c01 - Samp Atom = 0x4e04 - Sandbox Atom = 0xbb07 - Scope Atom = 0x2bd05 - Scoped Atom = 0x2bd06 - Script Atom = 0x3d406 - Seamless Atom = 0x31c08 - Section Atom = 0x4e207 - Select Atom = 0x57a06 - Selected Atom = 0x57a08 - Shape Atom = 0x4f905 - Size Atom = 0x55504 - Sizes Atom = 0x55505 - Small Atom = 0x18f05 - Sortable Atom = 0x58d08 - Sorted Atom = 0x19906 - Source Atom = 0x1aa06 - Spacer Atom = 0x2db06 - Span Atom = 0x9504 - Spellcheck Atom = 0x3230a - Src Atom = 0x3c303 - Srcdoc Atom = 0x3c306 - Srclang Atom = 0x41107 - Start Atom = 0x38605 - Step Atom = 0x5f704 - Strike Atom = 0x53306 - Strong Atom = 0x55906 - Style Atom = 0x61105 - Sub Atom = 0x5a903 - Summary Atom = 0x61607 - Sup Atom = 0x61d03 - Svg Atom = 0x62003 - System Atom = 0x62306 - Tabindex Atom = 0x46308 - Table Atom = 0x42d05 - Target Atom = 0x24b06 - Tbody Atom = 0x2e05 - Td Atom = 0x4702 - Template Atom = 0x62608 - Textarea Atom = 0x2f608 - Tfoot Atom = 0x8c05 - Th Atom = 0x22e02 - Thead Atom = 0x2d405 - Time Atom = 0xdd04 - Title Atom = 0xa105 - Tr Atom = 0x10502 - Track Atom = 0x10505 - Translate Atom = 0x14009 - Tt Atom = 0x5302 - Type Atom = 0x21404 - Typemustmatch Atom = 0x2140d - U Atom = 0xb01 - Ul Atom = 0x8a02 - Usemap Atom = 0x51106 - Value Atom = 0x4005 - Var Atom = 0x11503 - Video Atom = 0x28105 - Wbr Atom = 0x12103 - Width Atom = 0x50705 - Wrap Atom = 0x58704 - Xmp Atom = 0xc103 -) - -const hash0 = 0xc17da63e - -const maxAtomLen = 19 - -var table = [1 << 9]Atom{ - 0x1: 0x48a0b, // onmousemove - 0x2: 0x5e209, // onwaiting - 0x3: 0x1fa13, // onautocompleteerror - 0x4: 0x5fa06, // prompt - 0x7: 0x5eb07, // optimum - 0x8: 0x1604, // mark - 0xa: 0x5ad07, // itemref - 0xb: 0x4fe0a, // onpageshow - 0xc: 0x57a06, // select - 0xd: 0x17b09, // draggable - 0xe: 0x3e03, // nav - 0xf: 0x17507, // command - 0x11: 0xb01, // u - 0x14: 0x2d507, // headers - 0x15: 0x44a08, // datalist - 0x17: 0x4e04, // samp - 0x1a: 0x3fb09, // onkeydown - 0x1b: 0x55f08, // onscroll - 0x1c: 0x15003, // col - 0x20: 0x3c908, // itemprop - 0x21: 0x2780a, // http-equiv - 0x22: 0x61d03, // sup - 0x24: 0x1d008, // required - 0x2b: 0x25e07, // preload - 0x2c: 0x6040d, // onbeforeprint - 0x2d: 0x3600b, // ondragenter - 0x2e: 0x50902, // dt - 0x2f: 0x5a708, // onsubmit - 0x30: 0x27002, // hr - 0x31: 0x32f0d, // oncontextmenu - 0x33: 0x29c05, // image - 0x34: 0x50d07, // onpause - 0x35: 0x25906, // hgroup - 0x36: 0x7704, // ping - 0x37: 0x57808, // onselect - 0x3a: 0x11303, // div - 0x3b: 0x1fa0e, // onautocomplete - 0x40: 0x2eb02, // mi - 0x41: 0x31c08, // seamless - 0x42: 0x2807, // charset - 0x43: 0x8502, // id - 0x44: 0x5200a, // onpopstate - 0x45: 0x3ef03, // del - 0x46: 0x2cb07, // marquee - 0x47: 0x3309, // accesskey - 0x49: 0x8d06, // footer - 0x4a: 0x44e04, // list - 0x4b: 0x2b005, // ismap - 0x51: 0x33804, // menu - 0x52: 0x2f04, // body - 0x55: 0x9a08, // frameset - 0x56: 0x54a07, // onreset - 0x57: 0x12705, // blink - 0x58: 0xa105, // title - 0x59: 0x38807, // article - 0x5b: 0x22e02, // th - 0x5d: 0x13101, // q - 0x5e: 0x3cf04, // open - 0x5f: 0x2fa04, // area - 0x61: 0x44206, // onload - 0x62: 0xda04, // font - 0x63: 0xd604, // base - 0x64: 0x16207, // colspan - 0x65: 0x53707, // keytype - 0x66: 0x11e02, // dl - 0x68: 0x1b008, // fieldset - 0x6a: 0x2eb03, // min - 0x6b: 0x11503, // var - 0x6f: 0x2d506, // header - 0x70: 0x13f02, // rt - 0x71: 0x15008, // colgroup - 0x72: 0x23502, // mn - 0x74: 0x13a07, // onabort - 0x75: 0x3906, // keygen - 0x76: 0x4c209, // onoffline - 0x77: 0x21f09, // challenge - 0x78: 0x2b203, // map - 0x7a: 0x2e902, // h4 - 0x7b: 0x3b607, // onerror - 0x7c: 0x2e109, // maxlength - 0x7d: 0x2f505, // mtext - 0x7e: 0xbb07, // sandbox - 0x7f: 0x58b06, // onsort - 0x80: 0x100a, // malignmark - 0x81: 0x45d04, // meta - 0x82: 0x7b05, // async - 0x83: 0x2a702, // h3 - 0x84: 0x26702, // dd - 0x85: 0x27004, // href - 0x86: 0x6e0a, // mediagroup - 0x87: 0x19406, // coords - 0x88: 0x41107, // srclang - 0x89: 0x34d0a, // ondblclick - 0x8a: 0x4005, // value - 0x8c: 0xe908, // oncancel - 0x8e: 0x3230a, // spellcheck - 0x8f: 0x9a05, // frame - 0x91: 0x12403, // big - 0x94: 0x1f606, // action - 0x95: 0x6903, // dir - 0x97: 0x2fb08, // readonly - 0x99: 0x42d05, // table - 0x9a: 0x61607, // summary - 0x9b: 0x12103, // wbr - 0x9c: 0x30a, // radiogroup - 0x9d: 0x6c04, // name - 0x9f: 0x62306, // system - 0xa1: 0x15d05, // color - 0xa2: 0x7f06, // canvas - 0xa3: 0x25504, // html - 0xa5: 0x56f09, // onseeking - 0xac: 0x4f905, // shape - 0xad: 0x25f03, // rel - 0xae: 0x28510, // oncanplaythrough - 0xaf: 0x3760a, // ondragover - 0xb0: 0x62608, // template - 0xb1: 0x1d80d, // foreignObject - 0xb3: 0x9204, // rows - 0xb6: 0x44e07, // listing - 0xb7: 0x49c06, // output - 0xb9: 0x3310b, // contextmenu - 0xbb: 0x11f03, // low - 0xbc: 0x1c602, // rp - 0xbd: 0x5bb09, // onsuspend - 0xbe: 0x13606, // button - 0xbf: 0x4db04, // desc - 0xc1: 0x4e207, // section - 0xc2: 0x52a0a, // onprogress - 0xc3: 0x59e09, // onstorage - 0xc4: 0x2d204, // math - 0xc5: 0x4503, // alt - 0xc7: 0x8a02, // ul - 0xc8: 0x5107, // pattern - 0xc9: 0x4b60c, // onmousewheel - 0xca: 0x35709, // ondragend - 0xcb: 0xaf04, // ruby - 0xcc: 0xc01, // p - 0xcd: 0x31707, // onclose - 0xce: 0x24205, // meter - 0xcf: 0x11807, // bgsound - 0xd2: 0x25106, // height - 0xd4: 0x101, // b - 0xd5: 0x2c308, // itemtype - 0xd8: 0x1bb07, // caption - 0xd9: 0x10c08, // disabled - 0xdb: 0x33808, // menuitem - 0xdc: 0x62003, // svg - 0xdd: 0x18f05, // small - 0xde: 0x44a04, // data - 0xe0: 0x4cb08, // ononline - 0xe1: 0x2a206, // mglyph - 0xe3: 0x6505, // embed - 0xe4: 0x10502, // tr - 0xe5: 0x46b0b, // onloadstart - 0xe7: 0x3c306, // srcdoc - 0xeb: 0x5c408, // ontoggle - 0xed: 0xe703, // bdo - 0xee: 0x4702, // td - 0xef: 0x8305, // aside - 0xf0: 0x29402, // h2 - 0xf1: 0x52c08, // progress - 0xf2: 0x12c0a, // blockquote - 0xf4: 0xf005, // label - 0xf5: 0x601, // i - 0xf7: 0x9207, // rowspan - 0xfb: 0x51709, // onplaying - 0xfd: 0x2a103, // img - 0xfe: 0xf608, // optgroup - 0xff: 0x42307, // content - 0x101: 0x53e0c, // onratechange - 0x103: 0x3da0c, // onhashchange - 0x104: 0x4807, // details - 0x106: 0x40008, // download - 0x109: 0x14009, // translate - 0x10b: 0x4230f, // contenteditable - 0x10d: 0x36b0b, // ondragleave - 0x10e: 0x2106, // accept - 0x10f: 0x57a08, // selected - 0x112: 0x1f20a, // formaction - 0x113: 0x5b506, // center - 0x115: 0x45510, // onloadedmetadata - 0x116: 0x12804, // link - 0x117: 0xdd04, // time - 0x118: 0x19f0b, // crossorigin - 0x119: 0x3bd07, // onfocus - 0x11a: 0x58704, // wrap - 0x11b: 0x42204, // icon - 0x11d: 0x28105, // video - 0x11e: 0x4de05, // class - 0x121: 0x5d40e, // onvolumechange - 0x122: 0xaa06, // onblur - 0x123: 0x2b909, // itemscope - 0x124: 0x61105, // style - 0x127: 0x41e06, // public - 0x129: 0x2320e, // formnovalidate - 0x12a: 0x58206, // onshow - 0x12c: 0x51706, // onplay - 0x12d: 0x3c804, // cite - 0x12e: 0x2bc02, // ms - 0x12f: 0xdb0c, // ontimeupdate - 0x130: 0x10904, // kind - 0x131: 0x2470a, // formtarget - 0x135: 0x3af07, // onended - 0x136: 0x26506, // hidden - 0x137: 0x2c01, // s - 0x139: 0x2280a, // formmethod - 0x13a: 0x3e805, // input - 0x13c: 0x50b02, // h6 - 0x13d: 0xc902, // ol - 0x13e: 0x3420b, // oncuechange - 0x13f: 0x1e50d, // foreignobject - 0x143: 0x4e70e, // onbeforeunload - 0x144: 0x2bd05, // scope - 0x145: 0x39609, // onemptied - 0x146: 0x14b05, // defer - 0x147: 0xc103, // xmp - 0x148: 0x39f10, // ondurationchange - 0x149: 0x1903, // kbd - 0x14c: 0x47609, // onmessage - 0x14d: 0x60006, // option - 0x14e: 0x2eb09, // minlength - 0x14f: 0x32807, // checked - 0x150: 0xce08, // autoplay - 0x152: 0x202, // br - 0x153: 0x2360a, // novalidate - 0x156: 0x6307, // noembed - 0x159: 0x31007, // onclick - 0x15a: 0x47f0b, // onmousedown - 0x15b: 0x3a708, // onchange - 0x15e: 0x3f209, // oninvalid - 0x15f: 0x2bd06, // scoped - 0x160: 0x18808, // controls - 0x161: 0x30b05, // muted - 0x162: 0x58d08, // sortable - 0x163: 0x51106, // usemap - 0x164: 0x1b80a, // figcaption - 0x165: 0x35706, // ondrag - 0x166: 0x26b04, // high - 0x168: 0x3c303, // src - 0x169: 0x15706, // poster - 0x16b: 0x1670e, // annotation-xml - 0x16c: 0x5f704, // step - 0x16d: 0x4, // abbr - 0x16e: 0x1b06, // dialog - 0x170: 0x1202, // li - 0x172: 0x3ed02, // mo - 0x175: 0x1d803, // for - 0x176: 0x1a803, // ins - 0x178: 0x55504, // size - 0x179: 0x43210, // onlanguagechange - 0x17a: 0x8607, // default - 0x17b: 0x1a03, // bdi - 0x17c: 0x4d30a, // onpagehide - 0x17d: 0x6907, // dirname - 0x17e: 0x21404, // type - 0x17f: 0x1f204, // form - 0x181: 0x28509, // oncanplay - 0x182: 0x6103, // dfn - 0x183: 0x46308, // tabindex - 0x186: 0x6502, // em - 0x187: 0x27404, // lang - 0x189: 0x39108, // dropzone - 0x18a: 0x4080a, // onkeypress - 0x18b: 0x23c08, // datetime - 0x18c: 0x16204, // cols - 0x18d: 0x1, // a - 0x18e: 0x4420c, // onloadeddata - 0x190: 0xa605, // audio - 0x192: 0x2e05, // tbody - 0x193: 0x22c06, // method - 0x195: 0xf404, // loop - 0x196: 0x29606, // iframe - 0x198: 0x2d504, // head - 0x19e: 0x5f108, // manifest - 0x19f: 0xb309, // autofocus - 0x1a0: 0x14904, // code - 0x1a1: 0x55906, // strong - 0x1a2: 0x30308, // multiple - 0x1a3: 0xc05, // param - 0x1a6: 0x21107, // enctype - 0x1a7: 0x5b304, // face - 0x1a8: 0xfd09, // plaintext - 0x1a9: 0x26e02, // h1 - 0x1aa: 0x59509, // onstalled - 0x1ad: 0x3d406, // script - 0x1ae: 0x2db06, // spacer - 0x1af: 0x55108, // onresize - 0x1b0: 0x4a20b, // onmouseover - 0x1b1: 0x5cc08, // onunload - 0x1b2: 0x56708, // onseeked - 0x1b4: 0x2140d, // typemustmatch - 0x1b5: 0x1cc06, // figure - 0x1b6: 0x4950a, // onmouseout - 0x1b7: 0x25e03, // pre - 0x1b8: 0x50705, // width - 0x1b9: 0x19906, // sorted - 0x1bb: 0x5704, // nobr - 0x1be: 0x5302, // tt - 0x1bf: 0x1105, // align - 0x1c0: 0x3e607, // oninput - 0x1c3: 0x41807, // onkeyup - 0x1c6: 0x1c00c, // onafterprint - 0x1c7: 0x210e, // accept-charset - 0x1c8: 0x33c06, // itemid - 0x1c9: 0x3e809, // inputmode - 0x1cb: 0x53306, // strike - 0x1cc: 0x5a903, // sub - 0x1cd: 0x10505, // track - 0x1ce: 0x38605, // start - 0x1d0: 0xd608, // basefont - 0x1d6: 0x1aa06, // source - 0x1d7: 0x18206, // legend - 0x1d8: 0x2d405, // thead - 0x1da: 0x8c05, // tfoot - 0x1dd: 0x1ec06, // object - 0x1de: 0x6e05, // media - 0x1df: 0x1670a, // annotation - 0x1e0: 0x20d0b, // formenctype - 0x1e2: 0x3d208, // noscript - 0x1e4: 0x55505, // sizes - 0x1e5: 0x1fc0c, // autocomplete - 0x1e6: 0x9504, // span - 0x1e7: 0x9808, // noframes - 0x1e8: 0x24b06, // target - 0x1e9: 0x38f06, // ondrop - 0x1ea: 0x2b306, // applet - 0x1ec: 0x5a08, // reversed - 0x1f0: 0x2a907, // isindex - 0x1f3: 0x27008, // hreflang - 0x1f5: 0x2f302, // h5 - 0x1f6: 0x4f307, // address - 0x1fa: 0x2e103, // max - 0x1fb: 0xc30b, // placeholder - 0x1fc: 0x2f608, // textarea - 0x1fe: 0x4ad09, // onmouseup - 0x1ff: 0x3800b, // ondragstart -} - -const atomText = "abbradiogrouparamalignmarkbdialogaccept-charsetbodyaccesskey" + - "genavaluealtdetailsampatternobreversedfnoembedirnamediagroup" + - "ingasyncanvasidefaultfooterowspanoframesetitleaudionblurubya" + - "utofocusandboxmplaceholderautoplaybasefontimeupdatebdoncance" + - "labelooptgrouplaintextrackindisabledivarbgsoundlowbrbigblink" + - "blockquotebuttonabortranslatecodefercolgroupostercolorcolspa" + - "nnotation-xmlcommandraggablegendcontrolsmallcoordsortedcross" + - "originsourcefieldsetfigcaptionafterprintfigurequiredforeignO" + - "bjectforeignobjectformactionautocompleteerrorformenctypemust" + - "matchallengeformmethodformnovalidatetimeterformtargetheightm" + - "lhgroupreloadhiddenhigh1hreflanghttp-equivideoncanplaythroug" + - "h2iframeimageimglyph3isindexismappletitemscopeditemtypemarqu" + - "eematheaderspacermaxlength4minlength5mtextareadonlymultiplem" + - "utedonclickoncloseamlesspellcheckedoncontextmenuitemidoncuec" + - "hangeondblclickondragendondragenterondragleaveondragoverondr" + - "agstarticleondropzonemptiedondurationchangeonendedonerroronf" + - "ocusrcdocitempropenoscriptonhashchangeoninputmodeloninvalido" + - "nkeydownloadonkeypressrclangonkeyupublicontenteditableonlang" + - "uagechangeonloadeddatalistingonloadedmetadatabindexonloadsta" + - "rtonmessageonmousedownonmousemoveonmouseoutputonmouseoveronm" + - "ouseuponmousewheelonofflineononlineonpagehidesclassectionbef" + - "oreunloaddresshapeonpageshowidth6onpausemaponplayingonpopsta" + - "teonprogresstrikeytypeonratechangeonresetonresizestrongonscr" + - "ollonseekedonseekingonselectedonshowraponsortableonstalledon" + - "storageonsubmitemrefacenteronsuspendontoggleonunloadonvolume" + - "changeonwaitingoptimumanifestepromptoptionbeforeprintstylesu" + - "mmarysupsvgsystemplate" diff --git a/vendor/golang.org/x/net/html/charset/charset.go b/vendor/golang.org/x/net/html/charset/charset.go deleted file mode 100644 index 449607253..000000000 --- a/vendor/golang.org/x/net/html/charset/charset.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package charset provides common text encodings for HTML documents. -// -// The mapping from encoding labels to encodings is defined at -// https://encoding.spec.whatwg.org/. -package charset - -import ( - "bytes" - "fmt" - "io" - "mime" - "strings" - "unicode/utf8" - - "golang.org/x/net/html" - "golang.org/x/text/encoding" - "golang.org/x/text/encoding/charmap" - "golang.org/x/text/encoding/htmlindex" - "golang.org/x/text/transform" -) - -// Lookup returns the encoding with the specified label, and its canonical -// name. It returns nil and the empty string if label is not one of the -// standard encodings for HTML. Matching is case-insensitive and ignores -// leading and trailing whitespace. Encoders will use HTML escape sequences for -// runes that are not supported by the character set. -func Lookup(label string) (e encoding.Encoding, name string) { - e, err := htmlindex.Get(label) - if err != nil { - return nil, "" - } - name, _ = htmlindex.Name(e) - return &htmlEncoding{e}, name -} - -type htmlEncoding struct{ encoding.Encoding } - -func (h *htmlEncoding) NewEncoder() *encoding.Encoder { - // HTML requires a non-terminating legacy encoder. We use HTML escapes to - // substitute unsupported code points. - return encoding.HTMLEscapeUnsupported(h.Encoding.NewEncoder()) -} - -// DetermineEncoding determines the encoding of an HTML document by examining -// up to the first 1024 bytes of content and the declared Content-Type. -// -// See http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#determining-the-character-encoding -func DetermineEncoding(content []byte, contentType string) (e encoding.Encoding, name string, certain bool) { - if len(content) > 1024 { - content = content[:1024] - } - - for _, b := range boms { - if bytes.HasPrefix(content, b.bom) { - e, name = Lookup(b.enc) - return e, name, true - } - } - - if _, params, err := mime.ParseMediaType(contentType); err == nil { - if cs, ok := params["charset"]; ok { - if e, name = Lookup(cs); e != nil { - return e, name, true - } - } - } - - if len(content) > 0 { - e, name = prescan(content) - if e != nil { - return e, name, false - } - } - - // Try to detect UTF-8. - // First eliminate any partial rune at the end. - for i := len(content) - 1; i >= 0 && i > len(content)-4; i-- { - b := content[i] - if b < 0x80 { - break - } - if utf8.RuneStart(b) { - content = content[:i] - break - } - } - hasHighBit := false - for _, c := range content { - if c >= 0x80 { - hasHighBit = true - break - } - } - if hasHighBit && utf8.Valid(content) { - return encoding.Nop, "utf-8", false - } - - // TODO: change default depending on user's locale? - return charmap.Windows1252, "windows-1252", false -} - -// NewReader returns an io.Reader that converts the content of r to UTF-8. -// It calls DetermineEncoding to find out what r's encoding is. -func NewReader(r io.Reader, contentType string) (io.Reader, error) { - preview := make([]byte, 1024) - n, err := io.ReadFull(r, preview) - switch { - case err == io.ErrUnexpectedEOF: - preview = preview[:n] - r = bytes.NewReader(preview) - case err != nil: - return nil, err - default: - r = io.MultiReader(bytes.NewReader(preview), r) - } - - if e, _, _ := DetermineEncoding(preview, contentType); e != encoding.Nop { - r = transform.NewReader(r, e.NewDecoder()) - } - return r, nil -} - -// NewReaderLabel returns a reader that converts from the specified charset to -// UTF-8. It uses Lookup to find the encoding that corresponds to label, and -// returns an error if Lookup returns nil. It is suitable for use as -// encoding/xml.Decoder's CharsetReader function. -func NewReaderLabel(label string, input io.Reader) (io.Reader, error) { - e, _ := Lookup(label) - if e == nil { - return nil, fmt.Errorf("unsupported charset: %q", label) - } - return transform.NewReader(input, e.NewDecoder()), nil -} - -func prescan(content []byte) (e encoding.Encoding, name string) { - z := html.NewTokenizer(bytes.NewReader(content)) - for { - switch z.Next() { - case html.ErrorToken: - return nil, "" - - case html.StartTagToken, html.SelfClosingTagToken: - tagName, hasAttr := z.TagName() - if !bytes.Equal(tagName, []byte("meta")) { - continue - } - attrList := make(map[string]bool) - gotPragma := false - - const ( - dontKnow = iota - doNeedPragma - doNotNeedPragma - ) - needPragma := dontKnow - - name = "" - e = nil - for hasAttr { - var key, val []byte - key, val, hasAttr = z.TagAttr() - ks := string(key) - if attrList[ks] { - continue - } - attrList[ks] = true - for i, c := range val { - if 'A' <= c && c <= 'Z' { - val[i] = c + 0x20 - } - } - - switch ks { - case "http-equiv": - if bytes.Equal(val, []byte("content-type")) { - gotPragma = true - } - - case "content": - if e == nil { - name = fromMetaElement(string(val)) - if name != "" { - e, name = Lookup(name) - if e != nil { - needPragma = doNeedPragma - } - } - } - - case "charset": - e, name = Lookup(string(val)) - needPragma = doNotNeedPragma - } - } - - if needPragma == dontKnow || needPragma == doNeedPragma && !gotPragma { - continue - } - - if strings.HasPrefix(name, "utf-16") { - name = "utf-8" - e = encoding.Nop - } - - if e != nil { - return e, name - } - } - } -} - -func fromMetaElement(s string) string { - for s != "" { - csLoc := strings.Index(s, "charset") - if csLoc == -1 { - return "" - } - s = s[csLoc+len("charset"):] - s = strings.TrimLeft(s, " \t\n\f\r") - if !strings.HasPrefix(s, "=") { - continue - } - s = s[1:] - s = strings.TrimLeft(s, " \t\n\f\r") - if s == "" { - return "" - } - if q := s[0]; q == '"' || q == '\'' { - s = s[1:] - closeQuote := strings.IndexRune(s, rune(q)) - if closeQuote == -1 { - return "" - } - return s[:closeQuote] - } - - end := strings.IndexAny(s, "; \t\n\f\r") - if end == -1 { - end = len(s) - } - return s[:end] - } - return "" -} - -var boms = []struct { - bom []byte - enc string -}{ - {[]byte{0xfe, 0xff}, "utf-16be"}, - {[]byte{0xff, 0xfe}, "utf-16le"}, - {[]byte{0xef, 0xbb, 0xbf}, "utf-8"}, -} diff --git a/vendor/golang.org/x/net/html/const.go b/vendor/golang.org/x/net/html/const.go deleted file mode 100644 index 52f651ff6..000000000 --- a/vendor/golang.org/x/net/html/const.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -// Section 12.2.3.2 of the HTML5 specification says "The following elements -// have varying levels of special parsing rules". -// https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements -var isSpecialElementMap = map[string]bool{ - "address": true, - "applet": true, - "area": true, - "article": true, - "aside": true, - "base": true, - "basefont": true, - "bgsound": true, - "blockquote": true, - "body": true, - "br": true, - "button": true, - "caption": true, - "center": true, - "col": true, - "colgroup": true, - "dd": true, - "details": true, - "dir": true, - "div": true, - "dl": true, - "dt": true, - "embed": true, - "fieldset": true, - "figcaption": true, - "figure": true, - "footer": true, - "form": true, - "frame": true, - "frameset": true, - "h1": true, - "h2": true, - "h3": true, - "h4": true, - "h5": true, - "h6": true, - "head": true, - "header": true, - "hgroup": true, - "hr": true, - "html": true, - "iframe": true, - "img": true, - "input": true, - "isindex": true, - "li": true, - "link": true, - "listing": true, - "marquee": true, - "menu": true, - "meta": true, - "nav": true, - "noembed": true, - "noframes": true, - "noscript": true, - "object": true, - "ol": true, - "p": true, - "param": true, - "plaintext": true, - "pre": true, - "script": true, - "section": true, - "select": true, - "source": true, - "style": true, - "summary": true, - "table": true, - "tbody": true, - "td": true, - "template": true, - "textarea": true, - "tfoot": true, - "th": true, - "thead": true, - "title": true, - "tr": true, - "track": true, - "ul": true, - "wbr": true, - "xmp": true, -} - -func isSpecialElement(element *Node) bool { - switch element.Namespace { - case "", "html": - return isSpecialElementMap[element.Data] - case "svg": - return element.Data == "foreignObject" - } - return false -} diff --git a/vendor/golang.org/x/net/html/doc.go b/vendor/golang.org/x/net/html/doc.go deleted file mode 100644 index b453fe1e4..000000000 --- a/vendor/golang.org/x/net/html/doc.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package html implements an HTML5-compliant tokenizer and parser. - -Tokenization is done by creating a Tokenizer for an io.Reader r. It is the -caller's responsibility to ensure that r provides UTF-8 encoded HTML. - - z := html.NewTokenizer(r) - -Given a Tokenizer z, the HTML is tokenized by repeatedly calling z.Next(), -which parses the next token and returns its type, or an error: - - for { - tt := z.Next() - if tt == html.ErrorToken { - // ... - return ... - } - // Process the current token. - } - -There are two APIs for retrieving the current token. The high-level API is to -call Token; the low-level API is to call Text or TagName / TagAttr. Both APIs -allow optionally calling Raw after Next but before Token, Text, TagName, or -TagAttr. In EBNF notation, the valid call sequence per token is: - - Next {Raw} [ Token | Text | TagName {TagAttr} ] - -Token returns an independent data structure that completely describes a token. -Entities (such as "<") are unescaped, tag names and attribute keys are -lower-cased, and attributes are collected into a []Attribute. For example: - - for { - if z.Next() == html.ErrorToken { - // Returning io.EOF indicates success. - return z.Err() - } - emitToken(z.Token()) - } - -The low-level API performs fewer allocations and copies, but the contents of -the []byte values returned by Text, TagName and TagAttr may change on the next -call to Next. For example, to extract an HTML page's anchor text: - - depth := 0 - for { - tt := z.Next() - switch tt { - case ErrorToken: - return z.Err() - case TextToken: - if depth > 0 { - // emitBytes should copy the []byte it receives, - // if it doesn't process it immediately. - emitBytes(z.Text()) - } - case StartTagToken, EndTagToken: - tn, _ := z.TagName() - if len(tn) == 1 && tn[0] == 'a' { - if tt == StartTagToken { - depth++ - } else { - depth-- - } - } - } - } - -Parsing is done by calling Parse with an io.Reader, which returns the root of -the parse tree (the document element) as a *Node. It is the caller's -responsibility to ensure that the Reader provides UTF-8 encoded HTML. For -example, to process each anchor node in depth-first order: - - doc, err := html.Parse(r) - if err != nil { - // ... - } - var f func(*html.Node) - f = func(n *html.Node) { - if n.Type == html.ElementNode && n.Data == "a" { - // Do something with n... - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - f(c) - } - } - f(doc) - -The relevant specifications include: -https://html.spec.whatwg.org/multipage/syntax.html and -https://html.spec.whatwg.org/multipage/syntax.html#tokenization -*/ -package html - -// The tokenization algorithm implemented by this package is not a line-by-line -// transliteration of the relatively verbose state-machine in the WHATWG -// specification. A more direct approach is used instead, where the program -// counter implies the state, such as whether it is tokenizing a tag or a text -// node. Specification compliance is verified by checking expected and actual -// outputs over a test suite rather than aiming for algorithmic fidelity. - -// TODO(nigeltao): Does a DOM API belong in this package or a separate one? -// TODO(nigeltao): How does parsing interact with a JavaScript engine? diff --git a/vendor/golang.org/x/net/html/doctype.go b/vendor/golang.org/x/net/html/doctype.go deleted file mode 100644 index c484e5a94..000000000 --- a/vendor/golang.org/x/net/html/doctype.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "strings" -) - -// parseDoctype parses the data from a DoctypeToken into a name, -// public identifier, and system identifier. It returns a Node whose Type -// is DoctypeNode, whose Data is the name, and which has attributes -// named "system" and "public" for the two identifiers if they were present. -// quirks is whether the document should be parsed in "quirks mode". -func parseDoctype(s string) (n *Node, quirks bool) { - n = &Node{Type: DoctypeNode} - - // Find the name. - space := strings.IndexAny(s, whitespace) - if space == -1 { - space = len(s) - } - n.Data = s[:space] - // The comparison to "html" is case-sensitive. - if n.Data != "html" { - quirks = true - } - n.Data = strings.ToLower(n.Data) - s = strings.TrimLeft(s[space:], whitespace) - - if len(s) < 6 { - // It can't start with "PUBLIC" or "SYSTEM". - // Ignore the rest of the string. - return n, quirks || s != "" - } - - key := strings.ToLower(s[:6]) - s = s[6:] - for key == "public" || key == "system" { - s = strings.TrimLeft(s, whitespace) - if s == "" { - break - } - quote := s[0] - if quote != '"' && quote != '\'' { - break - } - s = s[1:] - q := strings.IndexRune(s, rune(quote)) - var id string - if q == -1 { - id = s - s = "" - } else { - id = s[:q] - s = s[q+1:] - } - n.Attr = append(n.Attr, Attribute{Key: key, Val: id}) - if key == "public" { - key = "system" - } else { - key = "" - } - } - - if key != "" || s != "" { - quirks = true - } else if len(n.Attr) > 0 { - if n.Attr[0].Key == "public" { - public := strings.ToLower(n.Attr[0].Val) - switch public { - case "-//w3o//dtd w3 html strict 3.0//en//", "-/w3d/dtd html 4.0 transitional/en", "html": - quirks = true - default: - for _, q := range quirkyIDs { - if strings.HasPrefix(public, q) { - quirks = true - break - } - } - } - // The following two public IDs only cause quirks mode if there is no system ID. - if len(n.Attr) == 1 && (strings.HasPrefix(public, "-//w3c//dtd html 4.01 frameset//") || - strings.HasPrefix(public, "-//w3c//dtd html 4.01 transitional//")) { - quirks = true - } - } - if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" && - strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" { - quirks = true - } - } - - return n, quirks -} - -// quirkyIDs is a list of public doctype identifiers that cause a document -// to be interpreted in quirks mode. The identifiers should be in lower case. -var quirkyIDs = []string{ - "+//silmaril//dtd html pro v0r11 19970101//", - "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", - "-//as//dtd html 3.0 aswedit + extensions//", - "-//ietf//dtd html 2.0 level 1//", - "-//ietf//dtd html 2.0 level 2//", - "-//ietf//dtd html 2.0 strict level 1//", - "-//ietf//dtd html 2.0 strict level 2//", - "-//ietf//dtd html 2.0 strict//", - "-//ietf//dtd html 2.0//", - "-//ietf//dtd html 2.1e//", - "-//ietf//dtd html 3.0//", - "-//ietf//dtd html 3.2 final//", - "-//ietf//dtd html 3.2//", - "-//ietf//dtd html 3//", - "-//ietf//dtd html level 0//", - "-//ietf//dtd html level 1//", - "-//ietf//dtd html level 2//", - "-//ietf//dtd html level 3//", - "-//ietf//dtd html strict level 0//", - "-//ietf//dtd html strict level 1//", - "-//ietf//dtd html strict level 2//", - "-//ietf//dtd html strict level 3//", - "-//ietf//dtd html strict//", - "-//ietf//dtd html//", - "-//metrius//dtd metrius presentational//", - "-//microsoft//dtd internet explorer 2.0 html strict//", - "-//microsoft//dtd internet explorer 2.0 html//", - "-//microsoft//dtd internet explorer 2.0 tables//", - "-//microsoft//dtd internet explorer 3.0 html strict//", - "-//microsoft//dtd internet explorer 3.0 html//", - "-//microsoft//dtd internet explorer 3.0 tables//", - "-//netscape comm. corp.//dtd html//", - "-//netscape comm. corp.//dtd strict html//", - "-//o'reilly and associates//dtd html 2.0//", - "-//o'reilly and associates//dtd html extended 1.0//", - "-//o'reilly and associates//dtd html extended relaxed 1.0//", - "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", - "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", - "-//spyglass//dtd html 2.0 extended//", - "-//sq//dtd html 2.0 hotmetal + extensions//", - "-//sun microsystems corp.//dtd hotjava html//", - "-//sun microsystems corp.//dtd hotjava strict html//", - "-//w3c//dtd html 3 1995-03-24//", - "-//w3c//dtd html 3.2 draft//", - "-//w3c//dtd html 3.2 final//", - "-//w3c//dtd html 3.2//", - "-//w3c//dtd html 3.2s draft//", - "-//w3c//dtd html 4.0 frameset//", - "-//w3c//dtd html 4.0 transitional//", - "-//w3c//dtd html experimental 19960712//", - "-//w3c//dtd html experimental 970421//", - "-//w3c//dtd w3 html//", - "-//w3o//dtd w3 html 3.0//", - "-//webtechs//dtd mozilla html 2.0//", - "-//webtechs//dtd mozilla html//", -} diff --git a/vendor/golang.org/x/net/html/entity.go b/vendor/golang.org/x/net/html/entity.go deleted file mode 100644 index a50c04c60..000000000 --- a/vendor/golang.org/x/net/html/entity.go +++ /dev/null @@ -1,2253 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -// All entities that do not end with ';' are 6 or fewer bytes long. -const longestEntityWithoutSemicolon = 6 - -// entity is a map from HTML entity names to their values. The semicolon matters: -// https://html.spec.whatwg.org/multipage/syntax.html#named-character-references -// lists both "amp" and "amp;" as two separate entries. -// -// Note that the HTML5 list is larger than the HTML4 list at -// http://www.w3.org/TR/html4/sgml/entities.html -var entity = map[string]rune{ - "AElig;": '\U000000C6', - "AMP;": '\U00000026', - "Aacute;": '\U000000C1', - "Abreve;": '\U00000102', - "Acirc;": '\U000000C2', - "Acy;": '\U00000410', - "Afr;": '\U0001D504', - "Agrave;": '\U000000C0', - "Alpha;": '\U00000391', - "Amacr;": '\U00000100', - "And;": '\U00002A53', - "Aogon;": '\U00000104', - "Aopf;": '\U0001D538', - "ApplyFunction;": '\U00002061', - "Aring;": '\U000000C5', - "Ascr;": '\U0001D49C', - "Assign;": '\U00002254', - "Atilde;": '\U000000C3', - "Auml;": '\U000000C4', - "Backslash;": '\U00002216', - "Barv;": '\U00002AE7', - "Barwed;": '\U00002306', - "Bcy;": '\U00000411', - "Because;": '\U00002235', - "Bernoullis;": '\U0000212C', - "Beta;": '\U00000392', - "Bfr;": '\U0001D505', - "Bopf;": '\U0001D539', - "Breve;": '\U000002D8', - "Bscr;": '\U0000212C', - "Bumpeq;": '\U0000224E', - "CHcy;": '\U00000427', - "COPY;": '\U000000A9', - "Cacute;": '\U00000106', - "Cap;": '\U000022D2', - "CapitalDifferentialD;": '\U00002145', - "Cayleys;": '\U0000212D', - "Ccaron;": '\U0000010C', - "Ccedil;": '\U000000C7', - "Ccirc;": '\U00000108', - "Cconint;": '\U00002230', - "Cdot;": '\U0000010A', - "Cedilla;": '\U000000B8', - "CenterDot;": '\U000000B7', - "Cfr;": '\U0000212D', - "Chi;": '\U000003A7', - "CircleDot;": '\U00002299', - "CircleMinus;": '\U00002296', - "CirclePlus;": '\U00002295', - "CircleTimes;": '\U00002297', - "ClockwiseContourIntegral;": '\U00002232', - "CloseCurlyDoubleQuote;": '\U0000201D', - "CloseCurlyQuote;": '\U00002019', - "Colon;": '\U00002237', - "Colone;": '\U00002A74', - "Congruent;": '\U00002261', - "Conint;": '\U0000222F', - "ContourIntegral;": '\U0000222E', - "Copf;": '\U00002102', - "Coproduct;": '\U00002210', - "CounterClockwiseContourIntegral;": '\U00002233', - "Cross;": '\U00002A2F', - "Cscr;": '\U0001D49E', - "Cup;": '\U000022D3', - "CupCap;": '\U0000224D', - "DD;": '\U00002145', - "DDotrahd;": '\U00002911', - "DJcy;": '\U00000402', - "DScy;": '\U00000405', - "DZcy;": '\U0000040F', - "Dagger;": '\U00002021', - "Darr;": '\U000021A1', - "Dashv;": '\U00002AE4', - "Dcaron;": '\U0000010E', - "Dcy;": '\U00000414', - "Del;": '\U00002207', - "Delta;": '\U00000394', - "Dfr;": '\U0001D507', - "DiacriticalAcute;": '\U000000B4', - "DiacriticalDot;": '\U000002D9', - "DiacriticalDoubleAcute;": '\U000002DD', - "DiacriticalGrave;": '\U00000060', - "DiacriticalTilde;": '\U000002DC', - "Diamond;": '\U000022C4', - "DifferentialD;": '\U00002146', - "Dopf;": '\U0001D53B', - "Dot;": '\U000000A8', - "DotDot;": '\U000020DC', - "DotEqual;": '\U00002250', - "DoubleContourIntegral;": '\U0000222F', - "DoubleDot;": '\U000000A8', - "DoubleDownArrow;": '\U000021D3', - "DoubleLeftArrow;": '\U000021D0', - "DoubleLeftRightArrow;": '\U000021D4', - "DoubleLeftTee;": '\U00002AE4', - "DoubleLongLeftArrow;": '\U000027F8', - "DoubleLongLeftRightArrow;": '\U000027FA', - "DoubleLongRightArrow;": '\U000027F9', - "DoubleRightArrow;": '\U000021D2', - "DoubleRightTee;": '\U000022A8', - "DoubleUpArrow;": '\U000021D1', - "DoubleUpDownArrow;": '\U000021D5', - "DoubleVerticalBar;": '\U00002225', - "DownArrow;": '\U00002193', - "DownArrowBar;": '\U00002913', - "DownArrowUpArrow;": '\U000021F5', - "DownBreve;": '\U00000311', - "DownLeftRightVector;": '\U00002950', - "DownLeftTeeVector;": '\U0000295E', - "DownLeftVector;": '\U000021BD', - "DownLeftVectorBar;": '\U00002956', - "DownRightTeeVector;": '\U0000295F', - "DownRightVector;": '\U000021C1', - "DownRightVectorBar;": '\U00002957', - "DownTee;": '\U000022A4', - "DownTeeArrow;": '\U000021A7', - "Downarrow;": '\U000021D3', - "Dscr;": '\U0001D49F', - "Dstrok;": '\U00000110', - "ENG;": '\U0000014A', - "ETH;": '\U000000D0', - "Eacute;": '\U000000C9', - "Ecaron;": '\U0000011A', - "Ecirc;": '\U000000CA', - "Ecy;": '\U0000042D', - "Edot;": '\U00000116', - "Efr;": '\U0001D508', - "Egrave;": '\U000000C8', - "Element;": '\U00002208', - "Emacr;": '\U00000112', - "EmptySmallSquare;": '\U000025FB', - "EmptyVerySmallSquare;": '\U000025AB', - "Eogon;": '\U00000118', - "Eopf;": '\U0001D53C', - "Epsilon;": '\U00000395', - "Equal;": '\U00002A75', - "EqualTilde;": '\U00002242', - "Equilibrium;": '\U000021CC', - "Escr;": '\U00002130', - "Esim;": '\U00002A73', - "Eta;": '\U00000397', - "Euml;": '\U000000CB', - "Exists;": '\U00002203', - "ExponentialE;": '\U00002147', - "Fcy;": '\U00000424', - "Ffr;": '\U0001D509', - "FilledSmallSquare;": '\U000025FC', - "FilledVerySmallSquare;": '\U000025AA', - "Fopf;": '\U0001D53D', - "ForAll;": '\U00002200', - "Fouriertrf;": '\U00002131', - "Fscr;": '\U00002131', - "GJcy;": '\U00000403', - "GT;": '\U0000003E', - "Gamma;": '\U00000393', - "Gammad;": '\U000003DC', - "Gbreve;": '\U0000011E', - "Gcedil;": '\U00000122', - "Gcirc;": '\U0000011C', - "Gcy;": '\U00000413', - "Gdot;": '\U00000120', - "Gfr;": '\U0001D50A', - "Gg;": '\U000022D9', - "Gopf;": '\U0001D53E', - "GreaterEqual;": '\U00002265', - "GreaterEqualLess;": '\U000022DB', - "GreaterFullEqual;": '\U00002267', - "GreaterGreater;": '\U00002AA2', - "GreaterLess;": '\U00002277', - "GreaterSlantEqual;": '\U00002A7E', - "GreaterTilde;": '\U00002273', - "Gscr;": '\U0001D4A2', - "Gt;": '\U0000226B', - "HARDcy;": '\U0000042A', - "Hacek;": '\U000002C7', - "Hat;": '\U0000005E', - "Hcirc;": '\U00000124', - "Hfr;": '\U0000210C', - "HilbertSpace;": '\U0000210B', - "Hopf;": '\U0000210D', - "HorizontalLine;": '\U00002500', - "Hscr;": '\U0000210B', - "Hstrok;": '\U00000126', - "HumpDownHump;": '\U0000224E', - "HumpEqual;": '\U0000224F', - "IEcy;": '\U00000415', - "IJlig;": '\U00000132', - "IOcy;": '\U00000401', - "Iacute;": '\U000000CD', - "Icirc;": '\U000000CE', - "Icy;": '\U00000418', - "Idot;": '\U00000130', - "Ifr;": '\U00002111', - "Igrave;": '\U000000CC', - "Im;": '\U00002111', - "Imacr;": '\U0000012A', - "ImaginaryI;": '\U00002148', - "Implies;": '\U000021D2', - "Int;": '\U0000222C', - "Integral;": '\U0000222B', - "Intersection;": '\U000022C2', - "InvisibleComma;": '\U00002063', - "InvisibleTimes;": '\U00002062', - "Iogon;": '\U0000012E', - "Iopf;": '\U0001D540', - "Iota;": '\U00000399', - "Iscr;": '\U00002110', - "Itilde;": '\U00000128', - "Iukcy;": '\U00000406', - "Iuml;": '\U000000CF', - "Jcirc;": '\U00000134', - "Jcy;": '\U00000419', - "Jfr;": '\U0001D50D', - "Jopf;": '\U0001D541', - "Jscr;": '\U0001D4A5', - "Jsercy;": '\U00000408', - "Jukcy;": '\U00000404', - "KHcy;": '\U00000425', - "KJcy;": '\U0000040C', - "Kappa;": '\U0000039A', - "Kcedil;": '\U00000136', - "Kcy;": '\U0000041A', - "Kfr;": '\U0001D50E', - "Kopf;": '\U0001D542', - "Kscr;": '\U0001D4A6', - "LJcy;": '\U00000409', - "LT;": '\U0000003C', - "Lacute;": '\U00000139', - "Lambda;": '\U0000039B', - "Lang;": '\U000027EA', - "Laplacetrf;": '\U00002112', - "Larr;": '\U0000219E', - "Lcaron;": '\U0000013D', - "Lcedil;": '\U0000013B', - "Lcy;": '\U0000041B', - "LeftAngleBracket;": '\U000027E8', - "LeftArrow;": '\U00002190', - "LeftArrowBar;": '\U000021E4', - "LeftArrowRightArrow;": '\U000021C6', - "LeftCeiling;": '\U00002308', - "LeftDoubleBracket;": '\U000027E6', - "LeftDownTeeVector;": '\U00002961', - "LeftDownVector;": '\U000021C3', - "LeftDownVectorBar;": '\U00002959', - "LeftFloor;": '\U0000230A', - "LeftRightArrow;": '\U00002194', - "LeftRightVector;": '\U0000294E', - "LeftTee;": '\U000022A3', - "LeftTeeArrow;": '\U000021A4', - "LeftTeeVector;": '\U0000295A', - "LeftTriangle;": '\U000022B2', - "LeftTriangleBar;": '\U000029CF', - "LeftTriangleEqual;": '\U000022B4', - "LeftUpDownVector;": '\U00002951', - "LeftUpTeeVector;": '\U00002960', - "LeftUpVector;": '\U000021BF', - "LeftUpVectorBar;": '\U00002958', - "LeftVector;": '\U000021BC', - "LeftVectorBar;": '\U00002952', - "Leftarrow;": '\U000021D0', - "Leftrightarrow;": '\U000021D4', - "LessEqualGreater;": '\U000022DA', - "LessFullEqual;": '\U00002266', - "LessGreater;": '\U00002276', - "LessLess;": '\U00002AA1', - "LessSlantEqual;": '\U00002A7D', - "LessTilde;": '\U00002272', - "Lfr;": '\U0001D50F', - "Ll;": '\U000022D8', - "Lleftarrow;": '\U000021DA', - "Lmidot;": '\U0000013F', - "LongLeftArrow;": '\U000027F5', - "LongLeftRightArrow;": '\U000027F7', - "LongRightArrow;": '\U000027F6', - "Longleftarrow;": '\U000027F8', - "Longleftrightarrow;": '\U000027FA', - "Longrightarrow;": '\U000027F9', - "Lopf;": '\U0001D543', - "LowerLeftArrow;": '\U00002199', - "LowerRightArrow;": '\U00002198', - "Lscr;": '\U00002112', - "Lsh;": '\U000021B0', - "Lstrok;": '\U00000141', - "Lt;": '\U0000226A', - "Map;": '\U00002905', - "Mcy;": '\U0000041C', - "MediumSpace;": '\U0000205F', - "Mellintrf;": '\U00002133', - "Mfr;": '\U0001D510', - "MinusPlus;": '\U00002213', - "Mopf;": '\U0001D544', - "Mscr;": '\U00002133', - "Mu;": '\U0000039C', - "NJcy;": '\U0000040A', - "Nacute;": '\U00000143', - "Ncaron;": '\U00000147', - "Ncedil;": '\U00000145', - "Ncy;": '\U0000041D', - "NegativeMediumSpace;": '\U0000200B', - "NegativeThickSpace;": '\U0000200B', - "NegativeThinSpace;": '\U0000200B', - "NegativeVeryThinSpace;": '\U0000200B', - "NestedGreaterGreater;": '\U0000226B', - "NestedLessLess;": '\U0000226A', - "NewLine;": '\U0000000A', - "Nfr;": '\U0001D511', - "NoBreak;": '\U00002060', - "NonBreakingSpace;": '\U000000A0', - "Nopf;": '\U00002115', - "Not;": '\U00002AEC', - "NotCongruent;": '\U00002262', - "NotCupCap;": '\U0000226D', - "NotDoubleVerticalBar;": '\U00002226', - "NotElement;": '\U00002209', - "NotEqual;": '\U00002260', - "NotExists;": '\U00002204', - "NotGreater;": '\U0000226F', - "NotGreaterEqual;": '\U00002271', - "NotGreaterLess;": '\U00002279', - "NotGreaterTilde;": '\U00002275', - "NotLeftTriangle;": '\U000022EA', - "NotLeftTriangleEqual;": '\U000022EC', - "NotLess;": '\U0000226E', - "NotLessEqual;": '\U00002270', - "NotLessGreater;": '\U00002278', - "NotLessTilde;": '\U00002274', - "NotPrecedes;": '\U00002280', - "NotPrecedesSlantEqual;": '\U000022E0', - "NotReverseElement;": '\U0000220C', - "NotRightTriangle;": '\U000022EB', - "NotRightTriangleEqual;": '\U000022ED', - "NotSquareSubsetEqual;": '\U000022E2', - "NotSquareSupersetEqual;": '\U000022E3', - "NotSubsetEqual;": '\U00002288', - "NotSucceeds;": '\U00002281', - "NotSucceedsSlantEqual;": '\U000022E1', - "NotSupersetEqual;": '\U00002289', - "NotTilde;": '\U00002241', - "NotTildeEqual;": '\U00002244', - "NotTildeFullEqual;": '\U00002247', - "NotTildeTilde;": '\U00002249', - "NotVerticalBar;": '\U00002224', - "Nscr;": '\U0001D4A9', - "Ntilde;": '\U000000D1', - "Nu;": '\U0000039D', - "OElig;": '\U00000152', - "Oacute;": '\U000000D3', - "Ocirc;": '\U000000D4', - "Ocy;": '\U0000041E', - "Odblac;": '\U00000150', - "Ofr;": '\U0001D512', - "Ograve;": '\U000000D2', - "Omacr;": '\U0000014C', - "Omega;": '\U000003A9', - "Omicron;": '\U0000039F', - "Oopf;": '\U0001D546', - "OpenCurlyDoubleQuote;": '\U0000201C', - "OpenCurlyQuote;": '\U00002018', - "Or;": '\U00002A54', - "Oscr;": '\U0001D4AA', - "Oslash;": '\U000000D8', - "Otilde;": '\U000000D5', - "Otimes;": '\U00002A37', - "Ouml;": '\U000000D6', - "OverBar;": '\U0000203E', - "OverBrace;": '\U000023DE', - "OverBracket;": '\U000023B4', - "OverParenthesis;": '\U000023DC', - "PartialD;": '\U00002202', - "Pcy;": '\U0000041F', - "Pfr;": '\U0001D513', - "Phi;": '\U000003A6', - "Pi;": '\U000003A0', - "PlusMinus;": '\U000000B1', - "Poincareplane;": '\U0000210C', - "Popf;": '\U00002119', - "Pr;": '\U00002ABB', - "Precedes;": '\U0000227A', - "PrecedesEqual;": '\U00002AAF', - "PrecedesSlantEqual;": '\U0000227C', - "PrecedesTilde;": '\U0000227E', - "Prime;": '\U00002033', - "Product;": '\U0000220F', - "Proportion;": '\U00002237', - "Proportional;": '\U0000221D', - "Pscr;": '\U0001D4AB', - "Psi;": '\U000003A8', - "QUOT;": '\U00000022', - "Qfr;": '\U0001D514', - "Qopf;": '\U0000211A', - "Qscr;": '\U0001D4AC', - "RBarr;": '\U00002910', - "REG;": '\U000000AE', - "Racute;": '\U00000154', - "Rang;": '\U000027EB', - "Rarr;": '\U000021A0', - "Rarrtl;": '\U00002916', - "Rcaron;": '\U00000158', - "Rcedil;": '\U00000156', - "Rcy;": '\U00000420', - "Re;": '\U0000211C', - "ReverseElement;": '\U0000220B', - "ReverseEquilibrium;": '\U000021CB', - "ReverseUpEquilibrium;": '\U0000296F', - "Rfr;": '\U0000211C', - "Rho;": '\U000003A1', - "RightAngleBracket;": '\U000027E9', - "RightArrow;": '\U00002192', - "RightArrowBar;": '\U000021E5', - "RightArrowLeftArrow;": '\U000021C4', - "RightCeiling;": '\U00002309', - "RightDoubleBracket;": '\U000027E7', - "RightDownTeeVector;": '\U0000295D', - "RightDownVector;": '\U000021C2', - "RightDownVectorBar;": '\U00002955', - "RightFloor;": '\U0000230B', - "RightTee;": '\U000022A2', - "RightTeeArrow;": '\U000021A6', - "RightTeeVector;": '\U0000295B', - "RightTriangle;": '\U000022B3', - "RightTriangleBar;": '\U000029D0', - "RightTriangleEqual;": '\U000022B5', - "RightUpDownVector;": '\U0000294F', - "RightUpTeeVector;": '\U0000295C', - "RightUpVector;": '\U000021BE', - "RightUpVectorBar;": '\U00002954', - "RightVector;": '\U000021C0', - "RightVectorBar;": '\U00002953', - "Rightarrow;": '\U000021D2', - "Ropf;": '\U0000211D', - "RoundImplies;": '\U00002970', - "Rrightarrow;": '\U000021DB', - "Rscr;": '\U0000211B', - "Rsh;": '\U000021B1', - "RuleDelayed;": '\U000029F4', - "SHCHcy;": '\U00000429', - "SHcy;": '\U00000428', - "SOFTcy;": '\U0000042C', - "Sacute;": '\U0000015A', - "Sc;": '\U00002ABC', - "Scaron;": '\U00000160', - "Scedil;": '\U0000015E', - "Scirc;": '\U0000015C', - "Scy;": '\U00000421', - "Sfr;": '\U0001D516', - "ShortDownArrow;": '\U00002193', - "ShortLeftArrow;": '\U00002190', - "ShortRightArrow;": '\U00002192', - "ShortUpArrow;": '\U00002191', - "Sigma;": '\U000003A3', - "SmallCircle;": '\U00002218', - "Sopf;": '\U0001D54A', - "Sqrt;": '\U0000221A', - "Square;": '\U000025A1', - "SquareIntersection;": '\U00002293', - "SquareSubset;": '\U0000228F', - "SquareSubsetEqual;": '\U00002291', - "SquareSuperset;": '\U00002290', - "SquareSupersetEqual;": '\U00002292', - "SquareUnion;": '\U00002294', - "Sscr;": '\U0001D4AE', - "Star;": '\U000022C6', - "Sub;": '\U000022D0', - "Subset;": '\U000022D0', - "SubsetEqual;": '\U00002286', - "Succeeds;": '\U0000227B', - "SucceedsEqual;": '\U00002AB0', - "SucceedsSlantEqual;": '\U0000227D', - "SucceedsTilde;": '\U0000227F', - "SuchThat;": '\U0000220B', - "Sum;": '\U00002211', - "Sup;": '\U000022D1', - "Superset;": '\U00002283', - "SupersetEqual;": '\U00002287', - "Supset;": '\U000022D1', - "THORN;": '\U000000DE', - "TRADE;": '\U00002122', - "TSHcy;": '\U0000040B', - "TScy;": '\U00000426', - "Tab;": '\U00000009', - "Tau;": '\U000003A4', - "Tcaron;": '\U00000164', - "Tcedil;": '\U00000162', - "Tcy;": '\U00000422', - "Tfr;": '\U0001D517', - "Therefore;": '\U00002234', - "Theta;": '\U00000398', - "ThinSpace;": '\U00002009', - "Tilde;": '\U0000223C', - "TildeEqual;": '\U00002243', - "TildeFullEqual;": '\U00002245', - "TildeTilde;": '\U00002248', - "Topf;": '\U0001D54B', - "TripleDot;": '\U000020DB', - "Tscr;": '\U0001D4AF', - "Tstrok;": '\U00000166', - "Uacute;": '\U000000DA', - "Uarr;": '\U0000219F', - "Uarrocir;": '\U00002949', - "Ubrcy;": '\U0000040E', - "Ubreve;": '\U0000016C', - "Ucirc;": '\U000000DB', - "Ucy;": '\U00000423', - "Udblac;": '\U00000170', - "Ufr;": '\U0001D518', - "Ugrave;": '\U000000D9', - "Umacr;": '\U0000016A', - "UnderBar;": '\U0000005F', - "UnderBrace;": '\U000023DF', - "UnderBracket;": '\U000023B5', - "UnderParenthesis;": '\U000023DD', - "Union;": '\U000022C3', - "UnionPlus;": '\U0000228E', - "Uogon;": '\U00000172', - "Uopf;": '\U0001D54C', - "UpArrow;": '\U00002191', - "UpArrowBar;": '\U00002912', - "UpArrowDownArrow;": '\U000021C5', - "UpDownArrow;": '\U00002195', - "UpEquilibrium;": '\U0000296E', - "UpTee;": '\U000022A5', - "UpTeeArrow;": '\U000021A5', - "Uparrow;": '\U000021D1', - "Updownarrow;": '\U000021D5', - "UpperLeftArrow;": '\U00002196', - "UpperRightArrow;": '\U00002197', - "Upsi;": '\U000003D2', - "Upsilon;": '\U000003A5', - "Uring;": '\U0000016E', - "Uscr;": '\U0001D4B0', - "Utilde;": '\U00000168', - "Uuml;": '\U000000DC', - "VDash;": '\U000022AB', - "Vbar;": '\U00002AEB', - "Vcy;": '\U00000412', - "Vdash;": '\U000022A9', - "Vdashl;": '\U00002AE6', - "Vee;": '\U000022C1', - "Verbar;": '\U00002016', - "Vert;": '\U00002016', - "VerticalBar;": '\U00002223', - "VerticalLine;": '\U0000007C', - "VerticalSeparator;": '\U00002758', - "VerticalTilde;": '\U00002240', - "VeryThinSpace;": '\U0000200A', - "Vfr;": '\U0001D519', - "Vopf;": '\U0001D54D', - "Vscr;": '\U0001D4B1', - "Vvdash;": '\U000022AA', - "Wcirc;": '\U00000174', - "Wedge;": '\U000022C0', - "Wfr;": '\U0001D51A', - "Wopf;": '\U0001D54E', - "Wscr;": '\U0001D4B2', - "Xfr;": '\U0001D51B', - "Xi;": '\U0000039E', - "Xopf;": '\U0001D54F', - "Xscr;": '\U0001D4B3', - "YAcy;": '\U0000042F', - "YIcy;": '\U00000407', - "YUcy;": '\U0000042E', - "Yacute;": '\U000000DD', - "Ycirc;": '\U00000176', - "Ycy;": '\U0000042B', - "Yfr;": '\U0001D51C', - "Yopf;": '\U0001D550', - "Yscr;": '\U0001D4B4', - "Yuml;": '\U00000178', - "ZHcy;": '\U00000416', - "Zacute;": '\U00000179', - "Zcaron;": '\U0000017D', - "Zcy;": '\U00000417', - "Zdot;": '\U0000017B', - "ZeroWidthSpace;": '\U0000200B', - "Zeta;": '\U00000396', - "Zfr;": '\U00002128', - "Zopf;": '\U00002124', - "Zscr;": '\U0001D4B5', - "aacute;": '\U000000E1', - "abreve;": '\U00000103', - "ac;": '\U0000223E', - "acd;": '\U0000223F', - "acirc;": '\U000000E2', - "acute;": '\U000000B4', - "acy;": '\U00000430', - "aelig;": '\U000000E6', - "af;": '\U00002061', - "afr;": '\U0001D51E', - "agrave;": '\U000000E0', - "alefsym;": '\U00002135', - "aleph;": '\U00002135', - "alpha;": '\U000003B1', - "amacr;": '\U00000101', - "amalg;": '\U00002A3F', - "amp;": '\U00000026', - "and;": '\U00002227', - "andand;": '\U00002A55', - "andd;": '\U00002A5C', - "andslope;": '\U00002A58', - "andv;": '\U00002A5A', - "ang;": '\U00002220', - "ange;": '\U000029A4', - "angle;": '\U00002220', - "angmsd;": '\U00002221', - "angmsdaa;": '\U000029A8', - "angmsdab;": '\U000029A9', - "angmsdac;": '\U000029AA', - "angmsdad;": '\U000029AB', - "angmsdae;": '\U000029AC', - "angmsdaf;": '\U000029AD', - "angmsdag;": '\U000029AE', - "angmsdah;": '\U000029AF', - "angrt;": '\U0000221F', - "angrtvb;": '\U000022BE', - "angrtvbd;": '\U0000299D', - "angsph;": '\U00002222', - "angst;": '\U000000C5', - "angzarr;": '\U0000237C', - "aogon;": '\U00000105', - "aopf;": '\U0001D552', - "ap;": '\U00002248', - "apE;": '\U00002A70', - "apacir;": '\U00002A6F', - "ape;": '\U0000224A', - "apid;": '\U0000224B', - "apos;": '\U00000027', - "approx;": '\U00002248', - "approxeq;": '\U0000224A', - "aring;": '\U000000E5', - "ascr;": '\U0001D4B6', - "ast;": '\U0000002A', - "asymp;": '\U00002248', - "asympeq;": '\U0000224D', - "atilde;": '\U000000E3', - "auml;": '\U000000E4', - "awconint;": '\U00002233', - "awint;": '\U00002A11', - "bNot;": '\U00002AED', - "backcong;": '\U0000224C', - "backepsilon;": '\U000003F6', - "backprime;": '\U00002035', - "backsim;": '\U0000223D', - "backsimeq;": '\U000022CD', - "barvee;": '\U000022BD', - "barwed;": '\U00002305', - "barwedge;": '\U00002305', - "bbrk;": '\U000023B5', - "bbrktbrk;": '\U000023B6', - "bcong;": '\U0000224C', - "bcy;": '\U00000431', - "bdquo;": '\U0000201E', - "becaus;": '\U00002235', - "because;": '\U00002235', - "bemptyv;": '\U000029B0', - "bepsi;": '\U000003F6', - "bernou;": '\U0000212C', - "beta;": '\U000003B2', - "beth;": '\U00002136', - "between;": '\U0000226C', - "bfr;": '\U0001D51F', - "bigcap;": '\U000022C2', - "bigcirc;": '\U000025EF', - "bigcup;": '\U000022C3', - "bigodot;": '\U00002A00', - "bigoplus;": '\U00002A01', - "bigotimes;": '\U00002A02', - "bigsqcup;": '\U00002A06', - "bigstar;": '\U00002605', - "bigtriangledown;": '\U000025BD', - "bigtriangleup;": '\U000025B3', - "biguplus;": '\U00002A04', - "bigvee;": '\U000022C1', - "bigwedge;": '\U000022C0', - "bkarow;": '\U0000290D', - "blacklozenge;": '\U000029EB', - "blacksquare;": '\U000025AA', - "blacktriangle;": '\U000025B4', - "blacktriangledown;": '\U000025BE', - "blacktriangleleft;": '\U000025C2', - "blacktriangleright;": '\U000025B8', - "blank;": '\U00002423', - "blk12;": '\U00002592', - "blk14;": '\U00002591', - "blk34;": '\U00002593', - "block;": '\U00002588', - "bnot;": '\U00002310', - "bopf;": '\U0001D553', - "bot;": '\U000022A5', - "bottom;": '\U000022A5', - "bowtie;": '\U000022C8', - "boxDL;": '\U00002557', - "boxDR;": '\U00002554', - "boxDl;": '\U00002556', - "boxDr;": '\U00002553', - "boxH;": '\U00002550', - "boxHD;": '\U00002566', - "boxHU;": '\U00002569', - "boxHd;": '\U00002564', - "boxHu;": '\U00002567', - "boxUL;": '\U0000255D', - "boxUR;": '\U0000255A', - "boxUl;": '\U0000255C', - "boxUr;": '\U00002559', - "boxV;": '\U00002551', - "boxVH;": '\U0000256C', - "boxVL;": '\U00002563', - "boxVR;": '\U00002560', - "boxVh;": '\U0000256B', - "boxVl;": '\U00002562', - "boxVr;": '\U0000255F', - "boxbox;": '\U000029C9', - "boxdL;": '\U00002555', - "boxdR;": '\U00002552', - "boxdl;": '\U00002510', - "boxdr;": '\U0000250C', - "boxh;": '\U00002500', - "boxhD;": '\U00002565', - "boxhU;": '\U00002568', - "boxhd;": '\U0000252C', - "boxhu;": '\U00002534', - "boxminus;": '\U0000229F', - "boxplus;": '\U0000229E', - "boxtimes;": '\U000022A0', - "boxuL;": '\U0000255B', - "boxuR;": '\U00002558', - "boxul;": '\U00002518', - "boxur;": '\U00002514', - "boxv;": '\U00002502', - "boxvH;": '\U0000256A', - "boxvL;": '\U00002561', - "boxvR;": '\U0000255E', - "boxvh;": '\U0000253C', - "boxvl;": '\U00002524', - "boxvr;": '\U0000251C', - "bprime;": '\U00002035', - "breve;": '\U000002D8', - "brvbar;": '\U000000A6', - "bscr;": '\U0001D4B7', - "bsemi;": '\U0000204F', - "bsim;": '\U0000223D', - "bsime;": '\U000022CD', - "bsol;": '\U0000005C', - "bsolb;": '\U000029C5', - "bsolhsub;": '\U000027C8', - "bull;": '\U00002022', - "bullet;": '\U00002022', - "bump;": '\U0000224E', - "bumpE;": '\U00002AAE', - "bumpe;": '\U0000224F', - "bumpeq;": '\U0000224F', - "cacute;": '\U00000107', - "cap;": '\U00002229', - "capand;": '\U00002A44', - "capbrcup;": '\U00002A49', - "capcap;": '\U00002A4B', - "capcup;": '\U00002A47', - "capdot;": '\U00002A40', - "caret;": '\U00002041', - "caron;": '\U000002C7', - "ccaps;": '\U00002A4D', - "ccaron;": '\U0000010D', - "ccedil;": '\U000000E7', - "ccirc;": '\U00000109', - "ccups;": '\U00002A4C', - "ccupssm;": '\U00002A50', - "cdot;": '\U0000010B', - "cedil;": '\U000000B8', - "cemptyv;": '\U000029B2', - "cent;": '\U000000A2', - "centerdot;": '\U000000B7', - "cfr;": '\U0001D520', - "chcy;": '\U00000447', - "check;": '\U00002713', - "checkmark;": '\U00002713', - "chi;": '\U000003C7', - "cir;": '\U000025CB', - "cirE;": '\U000029C3', - "circ;": '\U000002C6', - "circeq;": '\U00002257', - "circlearrowleft;": '\U000021BA', - "circlearrowright;": '\U000021BB', - "circledR;": '\U000000AE', - "circledS;": '\U000024C8', - "circledast;": '\U0000229B', - "circledcirc;": '\U0000229A', - "circleddash;": '\U0000229D', - "cire;": '\U00002257', - "cirfnint;": '\U00002A10', - "cirmid;": '\U00002AEF', - "cirscir;": '\U000029C2', - "clubs;": '\U00002663', - "clubsuit;": '\U00002663', - "colon;": '\U0000003A', - "colone;": '\U00002254', - "coloneq;": '\U00002254', - "comma;": '\U0000002C', - "commat;": '\U00000040', - "comp;": '\U00002201', - "compfn;": '\U00002218', - "complement;": '\U00002201', - "complexes;": '\U00002102', - "cong;": '\U00002245', - "congdot;": '\U00002A6D', - "conint;": '\U0000222E', - "copf;": '\U0001D554', - "coprod;": '\U00002210', - "copy;": '\U000000A9', - "copysr;": '\U00002117', - "crarr;": '\U000021B5', - "cross;": '\U00002717', - "cscr;": '\U0001D4B8', - "csub;": '\U00002ACF', - "csube;": '\U00002AD1', - "csup;": '\U00002AD0', - "csupe;": '\U00002AD2', - "ctdot;": '\U000022EF', - "cudarrl;": '\U00002938', - "cudarrr;": '\U00002935', - "cuepr;": '\U000022DE', - "cuesc;": '\U000022DF', - "cularr;": '\U000021B6', - "cularrp;": '\U0000293D', - "cup;": '\U0000222A', - "cupbrcap;": '\U00002A48', - "cupcap;": '\U00002A46', - "cupcup;": '\U00002A4A', - "cupdot;": '\U0000228D', - "cupor;": '\U00002A45', - "curarr;": '\U000021B7', - "curarrm;": '\U0000293C', - "curlyeqprec;": '\U000022DE', - "curlyeqsucc;": '\U000022DF', - "curlyvee;": '\U000022CE', - "curlywedge;": '\U000022CF', - "curren;": '\U000000A4', - "curvearrowleft;": '\U000021B6', - "curvearrowright;": '\U000021B7', - "cuvee;": '\U000022CE', - "cuwed;": '\U000022CF', - "cwconint;": '\U00002232', - "cwint;": '\U00002231', - "cylcty;": '\U0000232D', - "dArr;": '\U000021D3', - "dHar;": '\U00002965', - "dagger;": '\U00002020', - "daleth;": '\U00002138', - "darr;": '\U00002193', - "dash;": '\U00002010', - "dashv;": '\U000022A3', - "dbkarow;": '\U0000290F', - "dblac;": '\U000002DD', - "dcaron;": '\U0000010F', - "dcy;": '\U00000434', - "dd;": '\U00002146', - "ddagger;": '\U00002021', - "ddarr;": '\U000021CA', - "ddotseq;": '\U00002A77', - "deg;": '\U000000B0', - "delta;": '\U000003B4', - "demptyv;": '\U000029B1', - "dfisht;": '\U0000297F', - "dfr;": '\U0001D521', - "dharl;": '\U000021C3', - "dharr;": '\U000021C2', - "diam;": '\U000022C4', - "diamond;": '\U000022C4', - "diamondsuit;": '\U00002666', - "diams;": '\U00002666', - "die;": '\U000000A8', - "digamma;": '\U000003DD', - "disin;": '\U000022F2', - "div;": '\U000000F7', - "divide;": '\U000000F7', - "divideontimes;": '\U000022C7', - "divonx;": '\U000022C7', - "djcy;": '\U00000452', - "dlcorn;": '\U0000231E', - "dlcrop;": '\U0000230D', - "dollar;": '\U00000024', - "dopf;": '\U0001D555', - "dot;": '\U000002D9', - "doteq;": '\U00002250', - "doteqdot;": '\U00002251', - "dotminus;": '\U00002238', - "dotplus;": '\U00002214', - "dotsquare;": '\U000022A1', - "doublebarwedge;": '\U00002306', - "downarrow;": '\U00002193', - "downdownarrows;": '\U000021CA', - "downharpoonleft;": '\U000021C3', - "downharpoonright;": '\U000021C2', - "drbkarow;": '\U00002910', - "drcorn;": '\U0000231F', - "drcrop;": '\U0000230C', - "dscr;": '\U0001D4B9', - "dscy;": '\U00000455', - "dsol;": '\U000029F6', - "dstrok;": '\U00000111', - "dtdot;": '\U000022F1', - "dtri;": '\U000025BF', - "dtrif;": '\U000025BE', - "duarr;": '\U000021F5', - "duhar;": '\U0000296F', - "dwangle;": '\U000029A6', - "dzcy;": '\U0000045F', - "dzigrarr;": '\U000027FF', - "eDDot;": '\U00002A77', - "eDot;": '\U00002251', - "eacute;": '\U000000E9', - "easter;": '\U00002A6E', - "ecaron;": '\U0000011B', - "ecir;": '\U00002256', - "ecirc;": '\U000000EA', - "ecolon;": '\U00002255', - "ecy;": '\U0000044D', - "edot;": '\U00000117', - "ee;": '\U00002147', - "efDot;": '\U00002252', - "efr;": '\U0001D522', - "eg;": '\U00002A9A', - "egrave;": '\U000000E8', - "egs;": '\U00002A96', - "egsdot;": '\U00002A98', - "el;": '\U00002A99', - "elinters;": '\U000023E7', - "ell;": '\U00002113', - "els;": '\U00002A95', - "elsdot;": '\U00002A97', - "emacr;": '\U00000113', - "empty;": '\U00002205', - "emptyset;": '\U00002205', - "emptyv;": '\U00002205', - "emsp;": '\U00002003', - "emsp13;": '\U00002004', - "emsp14;": '\U00002005', - "eng;": '\U0000014B', - "ensp;": '\U00002002', - "eogon;": '\U00000119', - "eopf;": '\U0001D556', - "epar;": '\U000022D5', - "eparsl;": '\U000029E3', - "eplus;": '\U00002A71', - "epsi;": '\U000003B5', - "epsilon;": '\U000003B5', - "epsiv;": '\U000003F5', - "eqcirc;": '\U00002256', - "eqcolon;": '\U00002255', - "eqsim;": '\U00002242', - "eqslantgtr;": '\U00002A96', - "eqslantless;": '\U00002A95', - "equals;": '\U0000003D', - "equest;": '\U0000225F', - "equiv;": '\U00002261', - "equivDD;": '\U00002A78', - "eqvparsl;": '\U000029E5', - "erDot;": '\U00002253', - "erarr;": '\U00002971', - "escr;": '\U0000212F', - "esdot;": '\U00002250', - "esim;": '\U00002242', - "eta;": '\U000003B7', - "eth;": '\U000000F0', - "euml;": '\U000000EB', - "euro;": '\U000020AC', - "excl;": '\U00000021', - "exist;": '\U00002203', - "expectation;": '\U00002130', - "exponentiale;": '\U00002147', - "fallingdotseq;": '\U00002252', - "fcy;": '\U00000444', - "female;": '\U00002640', - "ffilig;": '\U0000FB03', - "fflig;": '\U0000FB00', - "ffllig;": '\U0000FB04', - "ffr;": '\U0001D523', - "filig;": '\U0000FB01', - "flat;": '\U0000266D', - "fllig;": '\U0000FB02', - "fltns;": '\U000025B1', - "fnof;": '\U00000192', - "fopf;": '\U0001D557', - "forall;": '\U00002200', - "fork;": '\U000022D4', - "forkv;": '\U00002AD9', - "fpartint;": '\U00002A0D', - "frac12;": '\U000000BD', - "frac13;": '\U00002153', - "frac14;": '\U000000BC', - "frac15;": '\U00002155', - "frac16;": '\U00002159', - "frac18;": '\U0000215B', - "frac23;": '\U00002154', - "frac25;": '\U00002156', - "frac34;": '\U000000BE', - "frac35;": '\U00002157', - "frac38;": '\U0000215C', - "frac45;": '\U00002158', - "frac56;": '\U0000215A', - "frac58;": '\U0000215D', - "frac78;": '\U0000215E', - "frasl;": '\U00002044', - "frown;": '\U00002322', - "fscr;": '\U0001D4BB', - "gE;": '\U00002267', - "gEl;": '\U00002A8C', - "gacute;": '\U000001F5', - "gamma;": '\U000003B3', - "gammad;": '\U000003DD', - "gap;": '\U00002A86', - "gbreve;": '\U0000011F', - "gcirc;": '\U0000011D', - "gcy;": '\U00000433', - "gdot;": '\U00000121', - "ge;": '\U00002265', - "gel;": '\U000022DB', - "geq;": '\U00002265', - "geqq;": '\U00002267', - "geqslant;": '\U00002A7E', - "ges;": '\U00002A7E', - "gescc;": '\U00002AA9', - "gesdot;": '\U00002A80', - "gesdoto;": '\U00002A82', - "gesdotol;": '\U00002A84', - "gesles;": '\U00002A94', - "gfr;": '\U0001D524', - "gg;": '\U0000226B', - "ggg;": '\U000022D9', - "gimel;": '\U00002137', - "gjcy;": '\U00000453', - "gl;": '\U00002277', - "glE;": '\U00002A92', - "gla;": '\U00002AA5', - "glj;": '\U00002AA4', - "gnE;": '\U00002269', - "gnap;": '\U00002A8A', - "gnapprox;": '\U00002A8A', - "gne;": '\U00002A88', - "gneq;": '\U00002A88', - "gneqq;": '\U00002269', - "gnsim;": '\U000022E7', - "gopf;": '\U0001D558', - "grave;": '\U00000060', - "gscr;": '\U0000210A', - "gsim;": '\U00002273', - "gsime;": '\U00002A8E', - "gsiml;": '\U00002A90', - "gt;": '\U0000003E', - "gtcc;": '\U00002AA7', - "gtcir;": '\U00002A7A', - "gtdot;": '\U000022D7', - "gtlPar;": '\U00002995', - "gtquest;": '\U00002A7C', - "gtrapprox;": '\U00002A86', - "gtrarr;": '\U00002978', - "gtrdot;": '\U000022D7', - "gtreqless;": '\U000022DB', - "gtreqqless;": '\U00002A8C', - "gtrless;": '\U00002277', - "gtrsim;": '\U00002273', - "hArr;": '\U000021D4', - "hairsp;": '\U0000200A', - "half;": '\U000000BD', - "hamilt;": '\U0000210B', - "hardcy;": '\U0000044A', - "harr;": '\U00002194', - "harrcir;": '\U00002948', - "harrw;": '\U000021AD', - "hbar;": '\U0000210F', - "hcirc;": '\U00000125', - "hearts;": '\U00002665', - "heartsuit;": '\U00002665', - "hellip;": '\U00002026', - "hercon;": '\U000022B9', - "hfr;": '\U0001D525', - "hksearow;": '\U00002925', - "hkswarow;": '\U00002926', - "hoarr;": '\U000021FF', - "homtht;": '\U0000223B', - "hookleftarrow;": '\U000021A9', - "hookrightarrow;": '\U000021AA', - "hopf;": '\U0001D559', - "horbar;": '\U00002015', - "hscr;": '\U0001D4BD', - "hslash;": '\U0000210F', - "hstrok;": '\U00000127', - "hybull;": '\U00002043', - "hyphen;": '\U00002010', - "iacute;": '\U000000ED', - "ic;": '\U00002063', - "icirc;": '\U000000EE', - "icy;": '\U00000438', - "iecy;": '\U00000435', - "iexcl;": '\U000000A1', - "iff;": '\U000021D4', - "ifr;": '\U0001D526', - "igrave;": '\U000000EC', - "ii;": '\U00002148', - "iiiint;": '\U00002A0C', - "iiint;": '\U0000222D', - "iinfin;": '\U000029DC', - "iiota;": '\U00002129', - "ijlig;": '\U00000133', - "imacr;": '\U0000012B', - "image;": '\U00002111', - "imagline;": '\U00002110', - "imagpart;": '\U00002111', - "imath;": '\U00000131', - "imof;": '\U000022B7', - "imped;": '\U000001B5', - "in;": '\U00002208', - "incare;": '\U00002105', - "infin;": '\U0000221E', - "infintie;": '\U000029DD', - "inodot;": '\U00000131', - "int;": '\U0000222B', - "intcal;": '\U000022BA', - "integers;": '\U00002124', - "intercal;": '\U000022BA', - "intlarhk;": '\U00002A17', - "intprod;": '\U00002A3C', - "iocy;": '\U00000451', - "iogon;": '\U0000012F', - "iopf;": '\U0001D55A', - "iota;": '\U000003B9', - "iprod;": '\U00002A3C', - "iquest;": '\U000000BF', - "iscr;": '\U0001D4BE', - "isin;": '\U00002208', - "isinE;": '\U000022F9', - "isindot;": '\U000022F5', - "isins;": '\U000022F4', - "isinsv;": '\U000022F3', - "isinv;": '\U00002208', - "it;": '\U00002062', - "itilde;": '\U00000129', - "iukcy;": '\U00000456', - "iuml;": '\U000000EF', - "jcirc;": '\U00000135', - "jcy;": '\U00000439', - "jfr;": '\U0001D527', - "jmath;": '\U00000237', - "jopf;": '\U0001D55B', - "jscr;": '\U0001D4BF', - "jsercy;": '\U00000458', - "jukcy;": '\U00000454', - "kappa;": '\U000003BA', - "kappav;": '\U000003F0', - "kcedil;": '\U00000137', - "kcy;": '\U0000043A', - "kfr;": '\U0001D528', - "kgreen;": '\U00000138', - "khcy;": '\U00000445', - "kjcy;": '\U0000045C', - "kopf;": '\U0001D55C', - "kscr;": '\U0001D4C0', - "lAarr;": '\U000021DA', - "lArr;": '\U000021D0', - "lAtail;": '\U0000291B', - "lBarr;": '\U0000290E', - "lE;": '\U00002266', - "lEg;": '\U00002A8B', - "lHar;": '\U00002962', - "lacute;": '\U0000013A', - "laemptyv;": '\U000029B4', - "lagran;": '\U00002112', - "lambda;": '\U000003BB', - "lang;": '\U000027E8', - "langd;": '\U00002991', - "langle;": '\U000027E8', - "lap;": '\U00002A85', - "laquo;": '\U000000AB', - "larr;": '\U00002190', - "larrb;": '\U000021E4', - "larrbfs;": '\U0000291F', - "larrfs;": '\U0000291D', - "larrhk;": '\U000021A9', - "larrlp;": '\U000021AB', - "larrpl;": '\U00002939', - "larrsim;": '\U00002973', - "larrtl;": '\U000021A2', - "lat;": '\U00002AAB', - "latail;": '\U00002919', - "late;": '\U00002AAD', - "lbarr;": '\U0000290C', - "lbbrk;": '\U00002772', - "lbrace;": '\U0000007B', - "lbrack;": '\U0000005B', - "lbrke;": '\U0000298B', - "lbrksld;": '\U0000298F', - "lbrkslu;": '\U0000298D', - "lcaron;": '\U0000013E', - "lcedil;": '\U0000013C', - "lceil;": '\U00002308', - "lcub;": '\U0000007B', - "lcy;": '\U0000043B', - "ldca;": '\U00002936', - "ldquo;": '\U0000201C', - "ldquor;": '\U0000201E', - "ldrdhar;": '\U00002967', - "ldrushar;": '\U0000294B', - "ldsh;": '\U000021B2', - "le;": '\U00002264', - "leftarrow;": '\U00002190', - "leftarrowtail;": '\U000021A2', - "leftharpoondown;": '\U000021BD', - "leftharpoonup;": '\U000021BC', - "leftleftarrows;": '\U000021C7', - "leftrightarrow;": '\U00002194', - "leftrightarrows;": '\U000021C6', - "leftrightharpoons;": '\U000021CB', - "leftrightsquigarrow;": '\U000021AD', - "leftthreetimes;": '\U000022CB', - "leg;": '\U000022DA', - "leq;": '\U00002264', - "leqq;": '\U00002266', - "leqslant;": '\U00002A7D', - "les;": '\U00002A7D', - "lescc;": '\U00002AA8', - "lesdot;": '\U00002A7F', - "lesdoto;": '\U00002A81', - "lesdotor;": '\U00002A83', - "lesges;": '\U00002A93', - "lessapprox;": '\U00002A85', - "lessdot;": '\U000022D6', - "lesseqgtr;": '\U000022DA', - "lesseqqgtr;": '\U00002A8B', - "lessgtr;": '\U00002276', - "lesssim;": '\U00002272', - "lfisht;": '\U0000297C', - "lfloor;": '\U0000230A', - "lfr;": '\U0001D529', - "lg;": '\U00002276', - "lgE;": '\U00002A91', - "lhard;": '\U000021BD', - "lharu;": '\U000021BC', - "lharul;": '\U0000296A', - "lhblk;": '\U00002584', - "ljcy;": '\U00000459', - "ll;": '\U0000226A', - "llarr;": '\U000021C7', - "llcorner;": '\U0000231E', - "llhard;": '\U0000296B', - "lltri;": '\U000025FA', - "lmidot;": '\U00000140', - "lmoust;": '\U000023B0', - "lmoustache;": '\U000023B0', - "lnE;": '\U00002268', - "lnap;": '\U00002A89', - "lnapprox;": '\U00002A89', - "lne;": '\U00002A87', - "lneq;": '\U00002A87', - "lneqq;": '\U00002268', - "lnsim;": '\U000022E6', - "loang;": '\U000027EC', - "loarr;": '\U000021FD', - "lobrk;": '\U000027E6', - "longleftarrow;": '\U000027F5', - "longleftrightarrow;": '\U000027F7', - "longmapsto;": '\U000027FC', - "longrightarrow;": '\U000027F6', - "looparrowleft;": '\U000021AB', - "looparrowright;": '\U000021AC', - "lopar;": '\U00002985', - "lopf;": '\U0001D55D', - "loplus;": '\U00002A2D', - "lotimes;": '\U00002A34', - "lowast;": '\U00002217', - "lowbar;": '\U0000005F', - "loz;": '\U000025CA', - "lozenge;": '\U000025CA', - "lozf;": '\U000029EB', - "lpar;": '\U00000028', - "lparlt;": '\U00002993', - "lrarr;": '\U000021C6', - "lrcorner;": '\U0000231F', - "lrhar;": '\U000021CB', - "lrhard;": '\U0000296D', - "lrm;": '\U0000200E', - "lrtri;": '\U000022BF', - "lsaquo;": '\U00002039', - "lscr;": '\U0001D4C1', - "lsh;": '\U000021B0', - "lsim;": '\U00002272', - "lsime;": '\U00002A8D', - "lsimg;": '\U00002A8F', - "lsqb;": '\U0000005B', - "lsquo;": '\U00002018', - "lsquor;": '\U0000201A', - "lstrok;": '\U00000142', - "lt;": '\U0000003C', - "ltcc;": '\U00002AA6', - "ltcir;": '\U00002A79', - "ltdot;": '\U000022D6', - "lthree;": '\U000022CB', - "ltimes;": '\U000022C9', - "ltlarr;": '\U00002976', - "ltquest;": '\U00002A7B', - "ltrPar;": '\U00002996', - "ltri;": '\U000025C3', - "ltrie;": '\U000022B4', - "ltrif;": '\U000025C2', - "lurdshar;": '\U0000294A', - "luruhar;": '\U00002966', - "mDDot;": '\U0000223A', - "macr;": '\U000000AF', - "male;": '\U00002642', - "malt;": '\U00002720', - "maltese;": '\U00002720', - "map;": '\U000021A6', - "mapsto;": '\U000021A6', - "mapstodown;": '\U000021A7', - "mapstoleft;": '\U000021A4', - "mapstoup;": '\U000021A5', - "marker;": '\U000025AE', - "mcomma;": '\U00002A29', - "mcy;": '\U0000043C', - "mdash;": '\U00002014', - "measuredangle;": '\U00002221', - "mfr;": '\U0001D52A', - "mho;": '\U00002127', - "micro;": '\U000000B5', - "mid;": '\U00002223', - "midast;": '\U0000002A', - "midcir;": '\U00002AF0', - "middot;": '\U000000B7', - "minus;": '\U00002212', - "minusb;": '\U0000229F', - "minusd;": '\U00002238', - "minusdu;": '\U00002A2A', - "mlcp;": '\U00002ADB', - "mldr;": '\U00002026', - "mnplus;": '\U00002213', - "models;": '\U000022A7', - "mopf;": '\U0001D55E', - "mp;": '\U00002213', - "mscr;": '\U0001D4C2', - "mstpos;": '\U0000223E', - "mu;": '\U000003BC', - "multimap;": '\U000022B8', - "mumap;": '\U000022B8', - "nLeftarrow;": '\U000021CD', - "nLeftrightarrow;": '\U000021CE', - "nRightarrow;": '\U000021CF', - "nVDash;": '\U000022AF', - "nVdash;": '\U000022AE', - "nabla;": '\U00002207', - "nacute;": '\U00000144', - "nap;": '\U00002249', - "napos;": '\U00000149', - "napprox;": '\U00002249', - "natur;": '\U0000266E', - "natural;": '\U0000266E', - "naturals;": '\U00002115', - "nbsp;": '\U000000A0', - "ncap;": '\U00002A43', - "ncaron;": '\U00000148', - "ncedil;": '\U00000146', - "ncong;": '\U00002247', - "ncup;": '\U00002A42', - "ncy;": '\U0000043D', - "ndash;": '\U00002013', - "ne;": '\U00002260', - "neArr;": '\U000021D7', - "nearhk;": '\U00002924', - "nearr;": '\U00002197', - "nearrow;": '\U00002197', - "nequiv;": '\U00002262', - "nesear;": '\U00002928', - "nexist;": '\U00002204', - "nexists;": '\U00002204', - "nfr;": '\U0001D52B', - "nge;": '\U00002271', - "ngeq;": '\U00002271', - "ngsim;": '\U00002275', - "ngt;": '\U0000226F', - "ngtr;": '\U0000226F', - "nhArr;": '\U000021CE', - "nharr;": '\U000021AE', - "nhpar;": '\U00002AF2', - "ni;": '\U0000220B', - "nis;": '\U000022FC', - "nisd;": '\U000022FA', - "niv;": '\U0000220B', - "njcy;": '\U0000045A', - "nlArr;": '\U000021CD', - "nlarr;": '\U0000219A', - "nldr;": '\U00002025', - "nle;": '\U00002270', - "nleftarrow;": '\U0000219A', - "nleftrightarrow;": '\U000021AE', - "nleq;": '\U00002270', - "nless;": '\U0000226E', - "nlsim;": '\U00002274', - "nlt;": '\U0000226E', - "nltri;": '\U000022EA', - "nltrie;": '\U000022EC', - "nmid;": '\U00002224', - "nopf;": '\U0001D55F', - "not;": '\U000000AC', - "notin;": '\U00002209', - "notinva;": '\U00002209', - "notinvb;": '\U000022F7', - "notinvc;": '\U000022F6', - "notni;": '\U0000220C', - "notniva;": '\U0000220C', - "notnivb;": '\U000022FE', - "notnivc;": '\U000022FD', - "npar;": '\U00002226', - "nparallel;": '\U00002226', - "npolint;": '\U00002A14', - "npr;": '\U00002280', - "nprcue;": '\U000022E0', - "nprec;": '\U00002280', - "nrArr;": '\U000021CF', - "nrarr;": '\U0000219B', - "nrightarrow;": '\U0000219B', - "nrtri;": '\U000022EB', - "nrtrie;": '\U000022ED', - "nsc;": '\U00002281', - "nsccue;": '\U000022E1', - "nscr;": '\U0001D4C3', - "nshortmid;": '\U00002224', - "nshortparallel;": '\U00002226', - "nsim;": '\U00002241', - "nsime;": '\U00002244', - "nsimeq;": '\U00002244', - "nsmid;": '\U00002224', - "nspar;": '\U00002226', - "nsqsube;": '\U000022E2', - "nsqsupe;": '\U000022E3', - "nsub;": '\U00002284', - "nsube;": '\U00002288', - "nsubseteq;": '\U00002288', - "nsucc;": '\U00002281', - "nsup;": '\U00002285', - "nsupe;": '\U00002289', - "nsupseteq;": '\U00002289', - "ntgl;": '\U00002279', - "ntilde;": '\U000000F1', - "ntlg;": '\U00002278', - "ntriangleleft;": '\U000022EA', - "ntrianglelefteq;": '\U000022EC', - "ntriangleright;": '\U000022EB', - "ntrianglerighteq;": '\U000022ED', - "nu;": '\U000003BD', - "num;": '\U00000023', - "numero;": '\U00002116', - "numsp;": '\U00002007', - "nvDash;": '\U000022AD', - "nvHarr;": '\U00002904', - "nvdash;": '\U000022AC', - "nvinfin;": '\U000029DE', - "nvlArr;": '\U00002902', - "nvrArr;": '\U00002903', - "nwArr;": '\U000021D6', - "nwarhk;": '\U00002923', - "nwarr;": '\U00002196', - "nwarrow;": '\U00002196', - "nwnear;": '\U00002927', - "oS;": '\U000024C8', - "oacute;": '\U000000F3', - "oast;": '\U0000229B', - "ocir;": '\U0000229A', - "ocirc;": '\U000000F4', - "ocy;": '\U0000043E', - "odash;": '\U0000229D', - "odblac;": '\U00000151', - "odiv;": '\U00002A38', - "odot;": '\U00002299', - "odsold;": '\U000029BC', - "oelig;": '\U00000153', - "ofcir;": '\U000029BF', - "ofr;": '\U0001D52C', - "ogon;": '\U000002DB', - "ograve;": '\U000000F2', - "ogt;": '\U000029C1', - "ohbar;": '\U000029B5', - "ohm;": '\U000003A9', - "oint;": '\U0000222E', - "olarr;": '\U000021BA', - "olcir;": '\U000029BE', - "olcross;": '\U000029BB', - "oline;": '\U0000203E', - "olt;": '\U000029C0', - "omacr;": '\U0000014D', - "omega;": '\U000003C9', - "omicron;": '\U000003BF', - "omid;": '\U000029B6', - "ominus;": '\U00002296', - "oopf;": '\U0001D560', - "opar;": '\U000029B7', - "operp;": '\U000029B9', - "oplus;": '\U00002295', - "or;": '\U00002228', - "orarr;": '\U000021BB', - "ord;": '\U00002A5D', - "order;": '\U00002134', - "orderof;": '\U00002134', - "ordf;": '\U000000AA', - "ordm;": '\U000000BA', - "origof;": '\U000022B6', - "oror;": '\U00002A56', - "orslope;": '\U00002A57', - "orv;": '\U00002A5B', - "oscr;": '\U00002134', - "oslash;": '\U000000F8', - "osol;": '\U00002298', - "otilde;": '\U000000F5', - "otimes;": '\U00002297', - "otimesas;": '\U00002A36', - "ouml;": '\U000000F6', - "ovbar;": '\U0000233D', - "par;": '\U00002225', - "para;": '\U000000B6', - "parallel;": '\U00002225', - "parsim;": '\U00002AF3', - "parsl;": '\U00002AFD', - "part;": '\U00002202', - "pcy;": '\U0000043F', - "percnt;": '\U00000025', - "period;": '\U0000002E', - "permil;": '\U00002030', - "perp;": '\U000022A5', - "pertenk;": '\U00002031', - "pfr;": '\U0001D52D', - "phi;": '\U000003C6', - "phiv;": '\U000003D5', - "phmmat;": '\U00002133', - "phone;": '\U0000260E', - "pi;": '\U000003C0', - "pitchfork;": '\U000022D4', - "piv;": '\U000003D6', - "planck;": '\U0000210F', - "planckh;": '\U0000210E', - "plankv;": '\U0000210F', - "plus;": '\U0000002B', - "plusacir;": '\U00002A23', - "plusb;": '\U0000229E', - "pluscir;": '\U00002A22', - "plusdo;": '\U00002214', - "plusdu;": '\U00002A25', - "pluse;": '\U00002A72', - "plusmn;": '\U000000B1', - "plussim;": '\U00002A26', - "plustwo;": '\U00002A27', - "pm;": '\U000000B1', - "pointint;": '\U00002A15', - "popf;": '\U0001D561', - "pound;": '\U000000A3', - "pr;": '\U0000227A', - "prE;": '\U00002AB3', - "prap;": '\U00002AB7', - "prcue;": '\U0000227C', - "pre;": '\U00002AAF', - "prec;": '\U0000227A', - "precapprox;": '\U00002AB7', - "preccurlyeq;": '\U0000227C', - "preceq;": '\U00002AAF', - "precnapprox;": '\U00002AB9', - "precneqq;": '\U00002AB5', - "precnsim;": '\U000022E8', - "precsim;": '\U0000227E', - "prime;": '\U00002032', - "primes;": '\U00002119', - "prnE;": '\U00002AB5', - "prnap;": '\U00002AB9', - "prnsim;": '\U000022E8', - "prod;": '\U0000220F', - "profalar;": '\U0000232E', - "profline;": '\U00002312', - "profsurf;": '\U00002313', - "prop;": '\U0000221D', - "propto;": '\U0000221D', - "prsim;": '\U0000227E', - "prurel;": '\U000022B0', - "pscr;": '\U0001D4C5', - "psi;": '\U000003C8', - "puncsp;": '\U00002008', - "qfr;": '\U0001D52E', - "qint;": '\U00002A0C', - "qopf;": '\U0001D562', - "qprime;": '\U00002057', - "qscr;": '\U0001D4C6', - "quaternions;": '\U0000210D', - "quatint;": '\U00002A16', - "quest;": '\U0000003F', - "questeq;": '\U0000225F', - "quot;": '\U00000022', - "rAarr;": '\U000021DB', - "rArr;": '\U000021D2', - "rAtail;": '\U0000291C', - "rBarr;": '\U0000290F', - "rHar;": '\U00002964', - "racute;": '\U00000155', - "radic;": '\U0000221A', - "raemptyv;": '\U000029B3', - "rang;": '\U000027E9', - "rangd;": '\U00002992', - "range;": '\U000029A5', - "rangle;": '\U000027E9', - "raquo;": '\U000000BB', - "rarr;": '\U00002192', - "rarrap;": '\U00002975', - "rarrb;": '\U000021E5', - "rarrbfs;": '\U00002920', - "rarrc;": '\U00002933', - "rarrfs;": '\U0000291E', - "rarrhk;": '\U000021AA', - "rarrlp;": '\U000021AC', - "rarrpl;": '\U00002945', - "rarrsim;": '\U00002974', - "rarrtl;": '\U000021A3', - "rarrw;": '\U0000219D', - "ratail;": '\U0000291A', - "ratio;": '\U00002236', - "rationals;": '\U0000211A', - "rbarr;": '\U0000290D', - "rbbrk;": '\U00002773', - "rbrace;": '\U0000007D', - "rbrack;": '\U0000005D', - "rbrke;": '\U0000298C', - "rbrksld;": '\U0000298E', - "rbrkslu;": '\U00002990', - "rcaron;": '\U00000159', - "rcedil;": '\U00000157', - "rceil;": '\U00002309', - "rcub;": '\U0000007D', - "rcy;": '\U00000440', - "rdca;": '\U00002937', - "rdldhar;": '\U00002969', - "rdquo;": '\U0000201D', - "rdquor;": '\U0000201D', - "rdsh;": '\U000021B3', - "real;": '\U0000211C', - "realine;": '\U0000211B', - "realpart;": '\U0000211C', - "reals;": '\U0000211D', - "rect;": '\U000025AD', - "reg;": '\U000000AE', - "rfisht;": '\U0000297D', - "rfloor;": '\U0000230B', - "rfr;": '\U0001D52F', - "rhard;": '\U000021C1', - "rharu;": '\U000021C0', - "rharul;": '\U0000296C', - "rho;": '\U000003C1', - "rhov;": '\U000003F1', - "rightarrow;": '\U00002192', - "rightarrowtail;": '\U000021A3', - "rightharpoondown;": '\U000021C1', - "rightharpoonup;": '\U000021C0', - "rightleftarrows;": '\U000021C4', - "rightleftharpoons;": '\U000021CC', - "rightrightarrows;": '\U000021C9', - "rightsquigarrow;": '\U0000219D', - "rightthreetimes;": '\U000022CC', - "ring;": '\U000002DA', - "risingdotseq;": '\U00002253', - "rlarr;": '\U000021C4', - "rlhar;": '\U000021CC', - "rlm;": '\U0000200F', - "rmoust;": '\U000023B1', - "rmoustache;": '\U000023B1', - "rnmid;": '\U00002AEE', - "roang;": '\U000027ED', - "roarr;": '\U000021FE', - "robrk;": '\U000027E7', - "ropar;": '\U00002986', - "ropf;": '\U0001D563', - "roplus;": '\U00002A2E', - "rotimes;": '\U00002A35', - "rpar;": '\U00000029', - "rpargt;": '\U00002994', - "rppolint;": '\U00002A12', - "rrarr;": '\U000021C9', - "rsaquo;": '\U0000203A', - "rscr;": '\U0001D4C7', - "rsh;": '\U000021B1', - "rsqb;": '\U0000005D', - "rsquo;": '\U00002019', - "rsquor;": '\U00002019', - "rthree;": '\U000022CC', - "rtimes;": '\U000022CA', - "rtri;": '\U000025B9', - "rtrie;": '\U000022B5', - "rtrif;": '\U000025B8', - "rtriltri;": '\U000029CE', - "ruluhar;": '\U00002968', - "rx;": '\U0000211E', - "sacute;": '\U0000015B', - "sbquo;": '\U0000201A', - "sc;": '\U0000227B', - "scE;": '\U00002AB4', - "scap;": '\U00002AB8', - "scaron;": '\U00000161', - "sccue;": '\U0000227D', - "sce;": '\U00002AB0', - "scedil;": '\U0000015F', - "scirc;": '\U0000015D', - "scnE;": '\U00002AB6', - "scnap;": '\U00002ABA', - "scnsim;": '\U000022E9', - "scpolint;": '\U00002A13', - "scsim;": '\U0000227F', - "scy;": '\U00000441', - "sdot;": '\U000022C5', - "sdotb;": '\U000022A1', - "sdote;": '\U00002A66', - "seArr;": '\U000021D8', - "searhk;": '\U00002925', - "searr;": '\U00002198', - "searrow;": '\U00002198', - "sect;": '\U000000A7', - "semi;": '\U0000003B', - "seswar;": '\U00002929', - "setminus;": '\U00002216', - "setmn;": '\U00002216', - "sext;": '\U00002736', - "sfr;": '\U0001D530', - "sfrown;": '\U00002322', - "sharp;": '\U0000266F', - "shchcy;": '\U00000449', - "shcy;": '\U00000448', - "shortmid;": '\U00002223', - "shortparallel;": '\U00002225', - "shy;": '\U000000AD', - "sigma;": '\U000003C3', - "sigmaf;": '\U000003C2', - "sigmav;": '\U000003C2', - "sim;": '\U0000223C', - "simdot;": '\U00002A6A', - "sime;": '\U00002243', - "simeq;": '\U00002243', - "simg;": '\U00002A9E', - "simgE;": '\U00002AA0', - "siml;": '\U00002A9D', - "simlE;": '\U00002A9F', - "simne;": '\U00002246', - "simplus;": '\U00002A24', - "simrarr;": '\U00002972', - "slarr;": '\U00002190', - "smallsetminus;": '\U00002216', - "smashp;": '\U00002A33', - "smeparsl;": '\U000029E4', - "smid;": '\U00002223', - "smile;": '\U00002323', - "smt;": '\U00002AAA', - "smte;": '\U00002AAC', - "softcy;": '\U0000044C', - "sol;": '\U0000002F', - "solb;": '\U000029C4', - "solbar;": '\U0000233F', - "sopf;": '\U0001D564', - "spades;": '\U00002660', - "spadesuit;": '\U00002660', - "spar;": '\U00002225', - "sqcap;": '\U00002293', - "sqcup;": '\U00002294', - "sqsub;": '\U0000228F', - "sqsube;": '\U00002291', - "sqsubset;": '\U0000228F', - "sqsubseteq;": '\U00002291', - "sqsup;": '\U00002290', - "sqsupe;": '\U00002292', - "sqsupset;": '\U00002290', - "sqsupseteq;": '\U00002292', - "squ;": '\U000025A1', - "square;": '\U000025A1', - "squarf;": '\U000025AA', - "squf;": '\U000025AA', - "srarr;": '\U00002192', - "sscr;": '\U0001D4C8', - "ssetmn;": '\U00002216', - "ssmile;": '\U00002323', - "sstarf;": '\U000022C6', - "star;": '\U00002606', - "starf;": '\U00002605', - "straightepsilon;": '\U000003F5', - "straightphi;": '\U000003D5', - "strns;": '\U000000AF', - "sub;": '\U00002282', - "subE;": '\U00002AC5', - "subdot;": '\U00002ABD', - "sube;": '\U00002286', - "subedot;": '\U00002AC3', - "submult;": '\U00002AC1', - "subnE;": '\U00002ACB', - "subne;": '\U0000228A', - "subplus;": '\U00002ABF', - "subrarr;": '\U00002979', - "subset;": '\U00002282', - "subseteq;": '\U00002286', - "subseteqq;": '\U00002AC5', - "subsetneq;": '\U0000228A', - "subsetneqq;": '\U00002ACB', - "subsim;": '\U00002AC7', - "subsub;": '\U00002AD5', - "subsup;": '\U00002AD3', - "succ;": '\U0000227B', - "succapprox;": '\U00002AB8', - "succcurlyeq;": '\U0000227D', - "succeq;": '\U00002AB0', - "succnapprox;": '\U00002ABA', - "succneqq;": '\U00002AB6', - "succnsim;": '\U000022E9', - "succsim;": '\U0000227F', - "sum;": '\U00002211', - "sung;": '\U0000266A', - "sup;": '\U00002283', - "sup1;": '\U000000B9', - "sup2;": '\U000000B2', - "sup3;": '\U000000B3', - "supE;": '\U00002AC6', - "supdot;": '\U00002ABE', - "supdsub;": '\U00002AD8', - "supe;": '\U00002287', - "supedot;": '\U00002AC4', - "suphsol;": '\U000027C9', - "suphsub;": '\U00002AD7', - "suplarr;": '\U0000297B', - "supmult;": '\U00002AC2', - "supnE;": '\U00002ACC', - "supne;": '\U0000228B', - "supplus;": '\U00002AC0', - "supset;": '\U00002283', - "supseteq;": '\U00002287', - "supseteqq;": '\U00002AC6', - "supsetneq;": '\U0000228B', - "supsetneqq;": '\U00002ACC', - "supsim;": '\U00002AC8', - "supsub;": '\U00002AD4', - "supsup;": '\U00002AD6', - "swArr;": '\U000021D9', - "swarhk;": '\U00002926', - "swarr;": '\U00002199', - "swarrow;": '\U00002199', - "swnwar;": '\U0000292A', - "szlig;": '\U000000DF', - "target;": '\U00002316', - "tau;": '\U000003C4', - "tbrk;": '\U000023B4', - "tcaron;": '\U00000165', - "tcedil;": '\U00000163', - "tcy;": '\U00000442', - "tdot;": '\U000020DB', - "telrec;": '\U00002315', - "tfr;": '\U0001D531', - "there4;": '\U00002234', - "therefore;": '\U00002234', - "theta;": '\U000003B8', - "thetasym;": '\U000003D1', - "thetav;": '\U000003D1', - "thickapprox;": '\U00002248', - "thicksim;": '\U0000223C', - "thinsp;": '\U00002009', - "thkap;": '\U00002248', - "thksim;": '\U0000223C', - "thorn;": '\U000000FE', - "tilde;": '\U000002DC', - "times;": '\U000000D7', - "timesb;": '\U000022A0', - "timesbar;": '\U00002A31', - "timesd;": '\U00002A30', - "tint;": '\U0000222D', - "toea;": '\U00002928', - "top;": '\U000022A4', - "topbot;": '\U00002336', - "topcir;": '\U00002AF1', - "topf;": '\U0001D565', - "topfork;": '\U00002ADA', - "tosa;": '\U00002929', - "tprime;": '\U00002034', - "trade;": '\U00002122', - "triangle;": '\U000025B5', - "triangledown;": '\U000025BF', - "triangleleft;": '\U000025C3', - "trianglelefteq;": '\U000022B4', - "triangleq;": '\U0000225C', - "triangleright;": '\U000025B9', - "trianglerighteq;": '\U000022B5', - "tridot;": '\U000025EC', - "trie;": '\U0000225C', - "triminus;": '\U00002A3A', - "triplus;": '\U00002A39', - "trisb;": '\U000029CD', - "tritime;": '\U00002A3B', - "trpezium;": '\U000023E2', - "tscr;": '\U0001D4C9', - "tscy;": '\U00000446', - "tshcy;": '\U0000045B', - "tstrok;": '\U00000167', - "twixt;": '\U0000226C', - "twoheadleftarrow;": '\U0000219E', - "twoheadrightarrow;": '\U000021A0', - "uArr;": '\U000021D1', - "uHar;": '\U00002963', - "uacute;": '\U000000FA', - "uarr;": '\U00002191', - "ubrcy;": '\U0000045E', - "ubreve;": '\U0000016D', - "ucirc;": '\U000000FB', - "ucy;": '\U00000443', - "udarr;": '\U000021C5', - "udblac;": '\U00000171', - "udhar;": '\U0000296E', - "ufisht;": '\U0000297E', - "ufr;": '\U0001D532', - "ugrave;": '\U000000F9', - "uharl;": '\U000021BF', - "uharr;": '\U000021BE', - "uhblk;": '\U00002580', - "ulcorn;": '\U0000231C', - "ulcorner;": '\U0000231C', - "ulcrop;": '\U0000230F', - "ultri;": '\U000025F8', - "umacr;": '\U0000016B', - "uml;": '\U000000A8', - "uogon;": '\U00000173', - "uopf;": '\U0001D566', - "uparrow;": '\U00002191', - "updownarrow;": '\U00002195', - "upharpoonleft;": '\U000021BF', - "upharpoonright;": '\U000021BE', - "uplus;": '\U0000228E', - "upsi;": '\U000003C5', - "upsih;": '\U000003D2', - "upsilon;": '\U000003C5', - "upuparrows;": '\U000021C8', - "urcorn;": '\U0000231D', - "urcorner;": '\U0000231D', - "urcrop;": '\U0000230E', - "uring;": '\U0000016F', - "urtri;": '\U000025F9', - "uscr;": '\U0001D4CA', - "utdot;": '\U000022F0', - "utilde;": '\U00000169', - "utri;": '\U000025B5', - "utrif;": '\U000025B4', - "uuarr;": '\U000021C8', - "uuml;": '\U000000FC', - "uwangle;": '\U000029A7', - "vArr;": '\U000021D5', - "vBar;": '\U00002AE8', - "vBarv;": '\U00002AE9', - "vDash;": '\U000022A8', - "vangrt;": '\U0000299C', - "varepsilon;": '\U000003F5', - "varkappa;": '\U000003F0', - "varnothing;": '\U00002205', - "varphi;": '\U000003D5', - "varpi;": '\U000003D6', - "varpropto;": '\U0000221D', - "varr;": '\U00002195', - "varrho;": '\U000003F1', - "varsigma;": '\U000003C2', - "vartheta;": '\U000003D1', - "vartriangleleft;": '\U000022B2', - "vartriangleright;": '\U000022B3', - "vcy;": '\U00000432', - "vdash;": '\U000022A2', - "vee;": '\U00002228', - "veebar;": '\U000022BB', - "veeeq;": '\U0000225A', - "vellip;": '\U000022EE', - "verbar;": '\U0000007C', - "vert;": '\U0000007C', - "vfr;": '\U0001D533', - "vltri;": '\U000022B2', - "vopf;": '\U0001D567', - "vprop;": '\U0000221D', - "vrtri;": '\U000022B3', - "vscr;": '\U0001D4CB', - "vzigzag;": '\U0000299A', - "wcirc;": '\U00000175', - "wedbar;": '\U00002A5F', - "wedge;": '\U00002227', - "wedgeq;": '\U00002259', - "weierp;": '\U00002118', - "wfr;": '\U0001D534', - "wopf;": '\U0001D568', - "wp;": '\U00002118', - "wr;": '\U00002240', - "wreath;": '\U00002240', - "wscr;": '\U0001D4CC', - "xcap;": '\U000022C2', - "xcirc;": '\U000025EF', - "xcup;": '\U000022C3', - "xdtri;": '\U000025BD', - "xfr;": '\U0001D535', - "xhArr;": '\U000027FA', - "xharr;": '\U000027F7', - "xi;": '\U000003BE', - "xlArr;": '\U000027F8', - "xlarr;": '\U000027F5', - "xmap;": '\U000027FC', - "xnis;": '\U000022FB', - "xodot;": '\U00002A00', - "xopf;": '\U0001D569', - "xoplus;": '\U00002A01', - "xotime;": '\U00002A02', - "xrArr;": '\U000027F9', - "xrarr;": '\U000027F6', - "xscr;": '\U0001D4CD', - "xsqcup;": '\U00002A06', - "xuplus;": '\U00002A04', - "xutri;": '\U000025B3', - "xvee;": '\U000022C1', - "xwedge;": '\U000022C0', - "yacute;": '\U000000FD', - "yacy;": '\U0000044F', - "ycirc;": '\U00000177', - "ycy;": '\U0000044B', - "yen;": '\U000000A5', - "yfr;": '\U0001D536', - "yicy;": '\U00000457', - "yopf;": '\U0001D56A', - "yscr;": '\U0001D4CE', - "yucy;": '\U0000044E', - "yuml;": '\U000000FF', - "zacute;": '\U0000017A', - "zcaron;": '\U0000017E', - "zcy;": '\U00000437', - "zdot;": '\U0000017C', - "zeetrf;": '\U00002128', - "zeta;": '\U000003B6', - "zfr;": '\U0001D537', - "zhcy;": '\U00000436', - "zigrarr;": '\U000021DD', - "zopf;": '\U0001D56B', - "zscr;": '\U0001D4CF', - "zwj;": '\U0000200D', - "zwnj;": '\U0000200C', - "AElig": '\U000000C6', - "AMP": '\U00000026', - "Aacute": '\U000000C1', - "Acirc": '\U000000C2', - "Agrave": '\U000000C0', - "Aring": '\U000000C5', - "Atilde": '\U000000C3', - "Auml": '\U000000C4', - "COPY": '\U000000A9', - "Ccedil": '\U000000C7', - "ETH": '\U000000D0', - "Eacute": '\U000000C9', - "Ecirc": '\U000000CA', - "Egrave": '\U000000C8', - "Euml": '\U000000CB', - "GT": '\U0000003E', - "Iacute": '\U000000CD', - "Icirc": '\U000000CE', - "Igrave": '\U000000CC', - "Iuml": '\U000000CF', - "LT": '\U0000003C', - "Ntilde": '\U000000D1', - "Oacute": '\U000000D3', - "Ocirc": '\U000000D4', - "Ograve": '\U000000D2', - "Oslash": '\U000000D8', - "Otilde": '\U000000D5', - "Ouml": '\U000000D6', - "QUOT": '\U00000022', - "REG": '\U000000AE', - "THORN": '\U000000DE', - "Uacute": '\U000000DA', - "Ucirc": '\U000000DB', - "Ugrave": '\U000000D9', - "Uuml": '\U000000DC', - "Yacute": '\U000000DD', - "aacute": '\U000000E1', - "acirc": '\U000000E2', - "acute": '\U000000B4', - "aelig": '\U000000E6', - "agrave": '\U000000E0', - "amp": '\U00000026', - "aring": '\U000000E5', - "atilde": '\U000000E3', - "auml": '\U000000E4', - "brvbar": '\U000000A6', - "ccedil": '\U000000E7', - "cedil": '\U000000B8', - "cent": '\U000000A2', - "copy": '\U000000A9', - "curren": '\U000000A4', - "deg": '\U000000B0', - "divide": '\U000000F7', - "eacute": '\U000000E9', - "ecirc": '\U000000EA', - "egrave": '\U000000E8', - "eth": '\U000000F0', - "euml": '\U000000EB', - "frac12": '\U000000BD', - "frac14": '\U000000BC', - "frac34": '\U000000BE', - "gt": '\U0000003E', - "iacute": '\U000000ED', - "icirc": '\U000000EE', - "iexcl": '\U000000A1', - "igrave": '\U000000EC', - "iquest": '\U000000BF', - "iuml": '\U000000EF', - "laquo": '\U000000AB', - "lt": '\U0000003C', - "macr": '\U000000AF', - "micro": '\U000000B5', - "middot": '\U000000B7', - "nbsp": '\U000000A0', - "not": '\U000000AC', - "ntilde": '\U000000F1', - "oacute": '\U000000F3', - "ocirc": '\U000000F4', - "ograve": '\U000000F2', - "ordf": '\U000000AA', - "ordm": '\U000000BA', - "oslash": '\U000000F8', - "otilde": '\U000000F5', - "ouml": '\U000000F6', - "para": '\U000000B6', - "plusmn": '\U000000B1', - "pound": '\U000000A3', - "quot": '\U00000022', - "raquo": '\U000000BB', - "reg": '\U000000AE', - "sect": '\U000000A7', - "shy": '\U000000AD', - "sup1": '\U000000B9', - "sup2": '\U000000B2', - "sup3": '\U000000B3', - "szlig": '\U000000DF', - "thorn": '\U000000FE', - "times": '\U000000D7', - "uacute": '\U000000FA', - "ucirc": '\U000000FB', - "ugrave": '\U000000F9', - "uml": '\U000000A8', - "uuml": '\U000000FC', - "yacute": '\U000000FD', - "yen": '\U000000A5', - "yuml": '\U000000FF', -} - -// HTML entities that are two unicode codepoints. -var entity2 = map[string][2]rune{ - // TODO(nigeltao): Handle replacements that are wider than their names. - // "nLt;": {'\u226A', '\u20D2'}, - // "nGt;": {'\u226B', '\u20D2'}, - "NotEqualTilde;": {'\u2242', '\u0338'}, - "NotGreaterFullEqual;": {'\u2267', '\u0338'}, - "NotGreaterGreater;": {'\u226B', '\u0338'}, - "NotGreaterSlantEqual;": {'\u2A7E', '\u0338'}, - "NotHumpDownHump;": {'\u224E', '\u0338'}, - "NotHumpEqual;": {'\u224F', '\u0338'}, - "NotLeftTriangleBar;": {'\u29CF', '\u0338'}, - "NotLessLess;": {'\u226A', '\u0338'}, - "NotLessSlantEqual;": {'\u2A7D', '\u0338'}, - "NotNestedGreaterGreater;": {'\u2AA2', '\u0338'}, - "NotNestedLessLess;": {'\u2AA1', '\u0338'}, - "NotPrecedesEqual;": {'\u2AAF', '\u0338'}, - "NotRightTriangleBar;": {'\u29D0', '\u0338'}, - "NotSquareSubset;": {'\u228F', '\u0338'}, - "NotSquareSuperset;": {'\u2290', '\u0338'}, - "NotSubset;": {'\u2282', '\u20D2'}, - "NotSucceedsEqual;": {'\u2AB0', '\u0338'}, - "NotSucceedsTilde;": {'\u227F', '\u0338'}, - "NotSuperset;": {'\u2283', '\u20D2'}, - "ThickSpace;": {'\u205F', '\u200A'}, - "acE;": {'\u223E', '\u0333'}, - "bne;": {'\u003D', '\u20E5'}, - "bnequiv;": {'\u2261', '\u20E5'}, - "caps;": {'\u2229', '\uFE00'}, - "cups;": {'\u222A', '\uFE00'}, - "fjlig;": {'\u0066', '\u006A'}, - "gesl;": {'\u22DB', '\uFE00'}, - "gvertneqq;": {'\u2269', '\uFE00'}, - "gvnE;": {'\u2269', '\uFE00'}, - "lates;": {'\u2AAD', '\uFE00'}, - "lesg;": {'\u22DA', '\uFE00'}, - "lvertneqq;": {'\u2268', '\uFE00'}, - "lvnE;": {'\u2268', '\uFE00'}, - "nGg;": {'\u22D9', '\u0338'}, - "nGtv;": {'\u226B', '\u0338'}, - "nLl;": {'\u22D8', '\u0338'}, - "nLtv;": {'\u226A', '\u0338'}, - "nang;": {'\u2220', '\u20D2'}, - "napE;": {'\u2A70', '\u0338'}, - "napid;": {'\u224B', '\u0338'}, - "nbump;": {'\u224E', '\u0338'}, - "nbumpe;": {'\u224F', '\u0338'}, - "ncongdot;": {'\u2A6D', '\u0338'}, - "nedot;": {'\u2250', '\u0338'}, - "nesim;": {'\u2242', '\u0338'}, - "ngE;": {'\u2267', '\u0338'}, - "ngeqq;": {'\u2267', '\u0338'}, - "ngeqslant;": {'\u2A7E', '\u0338'}, - "nges;": {'\u2A7E', '\u0338'}, - "nlE;": {'\u2266', '\u0338'}, - "nleqq;": {'\u2266', '\u0338'}, - "nleqslant;": {'\u2A7D', '\u0338'}, - "nles;": {'\u2A7D', '\u0338'}, - "notinE;": {'\u22F9', '\u0338'}, - "notindot;": {'\u22F5', '\u0338'}, - "nparsl;": {'\u2AFD', '\u20E5'}, - "npart;": {'\u2202', '\u0338'}, - "npre;": {'\u2AAF', '\u0338'}, - "npreceq;": {'\u2AAF', '\u0338'}, - "nrarrc;": {'\u2933', '\u0338'}, - "nrarrw;": {'\u219D', '\u0338'}, - "nsce;": {'\u2AB0', '\u0338'}, - "nsubE;": {'\u2AC5', '\u0338'}, - "nsubset;": {'\u2282', '\u20D2'}, - "nsubseteqq;": {'\u2AC5', '\u0338'}, - "nsucceq;": {'\u2AB0', '\u0338'}, - "nsupE;": {'\u2AC6', '\u0338'}, - "nsupset;": {'\u2283', '\u20D2'}, - "nsupseteqq;": {'\u2AC6', '\u0338'}, - "nvap;": {'\u224D', '\u20D2'}, - "nvge;": {'\u2265', '\u20D2'}, - "nvgt;": {'\u003E', '\u20D2'}, - "nvle;": {'\u2264', '\u20D2'}, - "nvlt;": {'\u003C', '\u20D2'}, - "nvltrie;": {'\u22B4', '\u20D2'}, - "nvrtrie;": {'\u22B5', '\u20D2'}, - "nvsim;": {'\u223C', '\u20D2'}, - "race;": {'\u223D', '\u0331'}, - "smtes;": {'\u2AAC', '\uFE00'}, - "sqcaps;": {'\u2293', '\uFE00'}, - "sqcups;": {'\u2294', '\uFE00'}, - "varsubsetneq;": {'\u228A', '\uFE00'}, - "varsubsetneqq;": {'\u2ACB', '\uFE00'}, - "varsupsetneq;": {'\u228B', '\uFE00'}, - "varsupsetneqq;": {'\u2ACC', '\uFE00'}, - "vnsub;": {'\u2282', '\u20D2'}, - "vnsup;": {'\u2283', '\u20D2'}, - "vsubnE;": {'\u2ACB', '\uFE00'}, - "vsubne;": {'\u228A', '\uFE00'}, - "vsupnE;": {'\u2ACC', '\uFE00'}, - "vsupne;": {'\u228B', '\uFE00'}, -} diff --git a/vendor/golang.org/x/net/html/escape.go b/vendor/golang.org/x/net/html/escape.go deleted file mode 100644 index d85613962..000000000 --- a/vendor/golang.org/x/net/html/escape.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "bytes" - "strings" - "unicode/utf8" -) - -// These replacements permit compatibility with old numeric entities that -// assumed Windows-1252 encoding. -// https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference -var replacementTable = [...]rune{ - '\u20AC', // First entry is what 0x80 should be replaced with. - '\u0081', - '\u201A', - '\u0192', - '\u201E', - '\u2026', - '\u2020', - '\u2021', - '\u02C6', - '\u2030', - '\u0160', - '\u2039', - '\u0152', - '\u008D', - '\u017D', - '\u008F', - '\u0090', - '\u2018', - '\u2019', - '\u201C', - '\u201D', - '\u2022', - '\u2013', - '\u2014', - '\u02DC', - '\u2122', - '\u0161', - '\u203A', - '\u0153', - '\u009D', - '\u017E', - '\u0178', // Last entry is 0x9F. - // 0x00->'\uFFFD' is handled programmatically. - // 0x0D->'\u000D' is a no-op. -} - -// unescapeEntity reads an entity like "<" from b[src:] and writes the -// corresponding "<" to b[dst:], returning the incremented dst and src cursors. -// Precondition: b[src] == '&' && dst <= src. -// attribute should be true if parsing an attribute value. -func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { - // https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference - - // i starts at 1 because we already know that s[0] == '&'. - i, s := 1, b[src:] - - if len(s) <= 1 { - b[dst] = b[src] - return dst + 1, src + 1 - } - - if s[i] == '#' { - if len(s) <= 3 { // We need to have at least "&#.". - b[dst] = b[src] - return dst + 1, src + 1 - } - i++ - c := s[i] - hex := false - if c == 'x' || c == 'X' { - hex = true - i++ - } - - x := '\x00' - for i < len(s) { - c = s[i] - i++ - if hex { - if '0' <= c && c <= '9' { - x = 16*x + rune(c) - '0' - continue - } else if 'a' <= c && c <= 'f' { - x = 16*x + rune(c) - 'a' + 10 - continue - } else if 'A' <= c && c <= 'F' { - x = 16*x + rune(c) - 'A' + 10 - continue - } - } else if '0' <= c && c <= '9' { - x = 10*x + rune(c) - '0' - continue - } - if c != ';' { - i-- - } - break - } - - if i <= 3 { // No characters matched. - b[dst] = b[src] - return dst + 1, src + 1 - } - - if 0x80 <= x && x <= 0x9F { - // Replace characters from Windows-1252 with UTF-8 equivalents. - x = replacementTable[x-0x80] - } else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF { - // Replace invalid characters with the replacement character. - x = '\uFFFD' - } - - return dst + utf8.EncodeRune(b[dst:], x), src + i - } - - // Consume the maximum number of characters possible, with the - // consumed characters matching one of the named references. - - for i < len(s) { - c := s[i] - i++ - // Lower-cased characters are more common in entities, so we check for them first. - if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { - continue - } - if c != ';' { - i-- - } - break - } - - entityName := string(s[1:i]) - if entityName == "" { - // No-op. - } else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' { - // No-op. - } else if x := entity[entityName]; x != 0 { - return dst + utf8.EncodeRune(b[dst:], x), src + i - } else if x := entity2[entityName]; x[0] != 0 { - dst1 := dst + utf8.EncodeRune(b[dst:], x[0]) - return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i - } else if !attribute { - maxLen := len(entityName) - 1 - if maxLen > longestEntityWithoutSemicolon { - maxLen = longestEntityWithoutSemicolon - } - for j := maxLen; j > 1; j-- { - if x := entity[entityName[:j]]; x != 0 { - return dst + utf8.EncodeRune(b[dst:], x), src + j + 1 - } - } - } - - dst1, src1 = dst+i, src+i - copy(b[dst:dst1], b[src:src1]) - return dst1, src1 -} - -// unescape unescapes b's entities in-place, so that "a<b" becomes "a': - esc = ">" - case '"': - // """ is shorter than """. - esc = """ - case '\r': - esc = " " - default: - panic("unrecognized escape character") - } - s = s[i+1:] - if _, err := w.WriteString(esc); err != nil { - return err - } - i = strings.IndexAny(s, escapedChars) - } - _, err := w.WriteString(s) - return err -} - -// EscapeString escapes special characters like "<" to become "<". It -// escapes only five such characters: <, >, &, ' and ". -// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't -// always true. -func EscapeString(s string) string { - if strings.IndexAny(s, escapedChars) == -1 { - return s - } - var buf bytes.Buffer - escape(&buf, s) - return buf.String() -} - -// UnescapeString unescapes entities like "<" to become "<". It unescapes a -// larger range of entities than EscapeString escapes. For example, "á" -// unescapes to "á", as does "á" and "&xE1;". -// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't -// always true. -func UnescapeString(s string) string { - for _, c := range s { - if c == '&' { - return string(unescape([]byte(s), false)) - } - } - return s -} diff --git a/vendor/golang.org/x/net/html/foreign.go b/vendor/golang.org/x/net/html/foreign.go deleted file mode 100644 index d3b384409..000000000 --- a/vendor/golang.org/x/net/html/foreign.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "strings" -) - -func adjustAttributeNames(aa []Attribute, nameMap map[string]string) { - for i := range aa { - if newName, ok := nameMap[aa[i].Key]; ok { - aa[i].Key = newName - } - } -} - -func adjustForeignAttributes(aa []Attribute) { - for i, a := range aa { - if a.Key == "" || a.Key[0] != 'x' { - continue - } - switch a.Key { - case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", - "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink": - j := strings.Index(a.Key, ":") - aa[i].Namespace = a.Key[:j] - aa[i].Key = a.Key[j+1:] - } - } -} - -func htmlIntegrationPoint(n *Node) bool { - if n.Type != ElementNode { - return false - } - switch n.Namespace { - case "math": - if n.Data == "annotation-xml" { - for _, a := range n.Attr { - if a.Key == "encoding" { - val := strings.ToLower(a.Val) - if val == "text/html" || val == "application/xhtml+xml" { - return true - } - } - } - } - case "svg": - switch n.Data { - case "desc", "foreignObject", "title": - return true - } - } - return false -} - -func mathMLTextIntegrationPoint(n *Node) bool { - if n.Namespace != "math" { - return false - } - switch n.Data { - case "mi", "mo", "mn", "ms", "mtext": - return true - } - return false -} - -// Section 12.2.5.5. -var breakout = map[string]bool{ - "b": true, - "big": true, - "blockquote": true, - "body": true, - "br": true, - "center": true, - "code": true, - "dd": true, - "div": true, - "dl": true, - "dt": true, - "em": true, - "embed": true, - "h1": true, - "h2": true, - "h3": true, - "h4": true, - "h5": true, - "h6": true, - "head": true, - "hr": true, - "i": true, - "img": true, - "li": true, - "listing": true, - "menu": true, - "meta": true, - "nobr": true, - "ol": true, - "p": true, - "pre": true, - "ruby": true, - "s": true, - "small": true, - "span": true, - "strong": true, - "strike": true, - "sub": true, - "sup": true, - "table": true, - "tt": true, - "u": true, - "ul": true, - "var": true, -} - -// Section 12.2.5.5. -var svgTagNameAdjustments = map[string]string{ - "altglyph": "altGlyph", - "altglyphdef": "altGlyphDef", - "altglyphitem": "altGlyphItem", - "animatecolor": "animateColor", - "animatemotion": "animateMotion", - "animatetransform": "animateTransform", - "clippath": "clipPath", - "feblend": "feBlend", - "fecolormatrix": "feColorMatrix", - "fecomponenttransfer": "feComponentTransfer", - "fecomposite": "feComposite", - "feconvolvematrix": "feConvolveMatrix", - "fediffuselighting": "feDiffuseLighting", - "fedisplacementmap": "feDisplacementMap", - "fedistantlight": "feDistantLight", - "feflood": "feFlood", - "fefunca": "feFuncA", - "fefuncb": "feFuncB", - "fefuncg": "feFuncG", - "fefuncr": "feFuncR", - "fegaussianblur": "feGaussianBlur", - "feimage": "feImage", - "femerge": "feMerge", - "femergenode": "feMergeNode", - "femorphology": "feMorphology", - "feoffset": "feOffset", - "fepointlight": "fePointLight", - "fespecularlighting": "feSpecularLighting", - "fespotlight": "feSpotLight", - "fetile": "feTile", - "feturbulence": "feTurbulence", - "foreignobject": "foreignObject", - "glyphref": "glyphRef", - "lineargradient": "linearGradient", - "radialgradient": "radialGradient", - "textpath": "textPath", -} - -// Section 12.2.5.1 -var mathMLAttributeAdjustments = map[string]string{ - "definitionurl": "definitionURL", -} - -var svgAttributeAdjustments = map[string]string{ - "attributename": "attributeName", - "attributetype": "attributeType", - "basefrequency": "baseFrequency", - "baseprofile": "baseProfile", - "calcmode": "calcMode", - "clippathunits": "clipPathUnits", - "contentscripttype": "contentScriptType", - "contentstyletype": "contentStyleType", - "diffuseconstant": "diffuseConstant", - "edgemode": "edgeMode", - "externalresourcesrequired": "externalResourcesRequired", - "filterres": "filterRes", - "filterunits": "filterUnits", - "glyphref": "glyphRef", - "gradienttransform": "gradientTransform", - "gradientunits": "gradientUnits", - "kernelmatrix": "kernelMatrix", - "kernelunitlength": "kernelUnitLength", - "keypoints": "keyPoints", - "keysplines": "keySplines", - "keytimes": "keyTimes", - "lengthadjust": "lengthAdjust", - "limitingconeangle": "limitingConeAngle", - "markerheight": "markerHeight", - "markerunits": "markerUnits", - "markerwidth": "markerWidth", - "maskcontentunits": "maskContentUnits", - "maskunits": "maskUnits", - "numoctaves": "numOctaves", - "pathlength": "pathLength", - "patterncontentunits": "patternContentUnits", - "patterntransform": "patternTransform", - "patternunits": "patternUnits", - "pointsatx": "pointsAtX", - "pointsaty": "pointsAtY", - "pointsatz": "pointsAtZ", - "preservealpha": "preserveAlpha", - "preserveaspectratio": "preserveAspectRatio", - "primitiveunits": "primitiveUnits", - "refx": "refX", - "refy": "refY", - "repeatcount": "repeatCount", - "repeatdur": "repeatDur", - "requiredextensions": "requiredExtensions", - "requiredfeatures": "requiredFeatures", - "specularconstant": "specularConstant", - "specularexponent": "specularExponent", - "spreadmethod": "spreadMethod", - "startoffset": "startOffset", - "stddeviation": "stdDeviation", - "stitchtiles": "stitchTiles", - "surfacescale": "surfaceScale", - "systemlanguage": "systemLanguage", - "tablevalues": "tableValues", - "targetx": "targetX", - "targety": "targetY", - "textlength": "textLength", - "viewbox": "viewBox", - "viewtarget": "viewTarget", - "xchannelselector": "xChannelSelector", - "ychannelselector": "yChannelSelector", - "zoomandpan": "zoomAndPan", -} diff --git a/vendor/golang.org/x/net/html/node.go b/vendor/golang.org/x/net/html/node.go deleted file mode 100644 index 26b657aec..000000000 --- a/vendor/golang.org/x/net/html/node.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "golang.org/x/net/html/atom" -) - -// A NodeType is the type of a Node. -type NodeType uint32 - -const ( - ErrorNode NodeType = iota - TextNode - DocumentNode - ElementNode - CommentNode - DoctypeNode - scopeMarkerNode -) - -// Section 12.2.3.3 says "scope markers are inserted when entering applet -// elements, buttons, object elements, marquees, table cells, and table -// captions, and are used to prevent formatting from 'leaking'". -var scopeMarker = Node{Type: scopeMarkerNode} - -// A Node consists of a NodeType and some Data (tag name for element nodes, -// content for text) and are part of a tree of Nodes. Element nodes may also -// have a Namespace and contain a slice of Attributes. Data is unescaped, so -// that it looks like "a 0 { - return (*s)[i-1] - } - return nil -} - -// index returns the index of the top-most occurrence of n in the stack, or -1 -// if n is not present. -func (s *nodeStack) index(n *Node) int { - for i := len(*s) - 1; i >= 0; i-- { - if (*s)[i] == n { - return i - } - } - return -1 -} - -// insert inserts a node at the given index. -func (s *nodeStack) insert(i int, n *Node) { - (*s) = append(*s, nil) - copy((*s)[i+1:], (*s)[i:]) - (*s)[i] = n -} - -// remove removes a node from the stack. It is a no-op if n is not present. -func (s *nodeStack) remove(n *Node) { - i := s.index(n) - if i == -1 { - return - } - copy((*s)[i:], (*s)[i+1:]) - j := len(*s) - 1 - (*s)[j] = nil - *s = (*s)[:j] -} diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go deleted file mode 100644 index be4b2bf5a..000000000 --- a/vendor/golang.org/x/net/html/parse.go +++ /dev/null @@ -1,2094 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package html - -import ( - "errors" - "fmt" - "io" - "strings" - - a "golang.org/x/net/html/atom" -) - -// A parser implements the HTML5 parsing algorithm: -// https://html.spec.whatwg.org/multipage/syntax.html#tree-construction -type parser struct { - // tokenizer provides the tokens for the parser. - tokenizer *Tokenizer - // tok is the most recently read token. - tok Token - // Self-closing tags like
are treated as start tags, except that - // hasSelfClosingToken is set while they are being processed. - hasSelfClosingToken bool - // doc is the document root element. - doc *Node - // The stack of open elements (section 12.2.3.2) and active formatting - // elements (section 12.2.3.3). - oe, afe nodeStack - // Element pointers (section 12.2.3.4). - head, form *Node - // Other parsing state flags (section 12.2.3.5). - scripting, framesetOK bool - // im is the current insertion mode. - im insertionMode - // originalIM is the insertion mode to go back to after completing a text - // or inTableText insertion mode. - originalIM insertionMode - // fosterParenting is whether new elements should be inserted according to - // the foster parenting rules (section 12.2.5.3). - fosterParenting bool - // quirks is whether the parser is operating in "quirks mode." - quirks bool - // fragment is whether the parser is parsing an HTML fragment. - fragment bool - // context is the context element when parsing an HTML fragment - // (section 12.4). - context *Node -} - -func (p *parser) top() *Node { - if n := p.oe.top(); n != nil { - return n - } - return p.doc -} - -// Stop tags for use in popUntil. These come from section 12.2.3.2. -var ( - defaultScopeStopTags = map[string][]a.Atom{ - "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template}, - "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, - "svg": {a.Desc, a.ForeignObject, a.Title}, - } -) - -type scope int - -const ( - defaultScope scope = iota - listItemScope - buttonScope - tableScope - tableRowScope - tableBodyScope - selectScope -) - -// popUntil pops the stack of open elements at the highest element whose tag -// is in matchTags, provided there is no higher element in the scope's stop -// tags (as defined in section 12.2.3.2). It returns whether or not there was -// such an element. If there was not, popUntil leaves the stack unchanged. -// -// For example, the set of stop tags for table scope is: "html", "table". If -// the stack was: -// ["html", "body", "font", "table", "b", "i", "u"] -// then popUntil(tableScope, "font") would return false, but -// popUntil(tableScope, "i") would return true and the stack would become: -// ["html", "body", "font", "table", "b"] -// -// If an element's tag is in both the stop tags and matchTags, then the stack -// will be popped and the function returns true (provided, of course, there was -// no higher element in the stack that was also in the stop tags). For example, -// popUntil(tableScope, "table") returns true and leaves: -// ["html", "body", "font"] -func (p *parser) popUntil(s scope, matchTags ...a.Atom) bool { - if i := p.indexOfElementInScope(s, matchTags...); i != -1 { - p.oe = p.oe[:i] - return true - } - return false -} - -// indexOfElementInScope returns the index in p.oe of the highest element whose -// tag is in matchTags that is in scope. If no matching element is in scope, it -// returns -1. -func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int { - for i := len(p.oe) - 1; i >= 0; i-- { - tagAtom := p.oe[i].DataAtom - if p.oe[i].Namespace == "" { - for _, t := range matchTags { - if t == tagAtom { - return i - } - } - switch s { - case defaultScope: - // No-op. - case listItemScope: - if tagAtom == a.Ol || tagAtom == a.Ul { - return -1 - } - case buttonScope: - if tagAtom == a.Button { - return -1 - } - case tableScope: - if tagAtom == a.Html || tagAtom == a.Table { - return -1 - } - case selectScope: - if tagAtom != a.Optgroup && tagAtom != a.Option { - return -1 - } - default: - panic("unreachable") - } - } - switch s { - case defaultScope, listItemScope, buttonScope: - for _, t := range defaultScopeStopTags[p.oe[i].Namespace] { - if t == tagAtom { - return -1 - } - } - } - } - return -1 -} - -// elementInScope is like popUntil, except that it doesn't modify the stack of -// open elements. -func (p *parser) elementInScope(s scope, matchTags ...a.Atom) bool { - return p.indexOfElementInScope(s, matchTags...) != -1 -} - -// clearStackToContext pops elements off the stack of open elements until a -// scope-defined element is found. -func (p *parser) clearStackToContext(s scope) { - for i := len(p.oe) - 1; i >= 0; i-- { - tagAtom := p.oe[i].DataAtom - switch s { - case tableScope: - if tagAtom == a.Html || tagAtom == a.Table { - p.oe = p.oe[:i+1] - return - } - case tableRowScope: - if tagAtom == a.Html || tagAtom == a.Tr { - p.oe = p.oe[:i+1] - return - } - case tableBodyScope: - if tagAtom == a.Html || tagAtom == a.Tbody || tagAtom == a.Tfoot || tagAtom == a.Thead { - p.oe = p.oe[:i+1] - return - } - default: - panic("unreachable") - } - } -} - -// generateImpliedEndTags pops nodes off the stack of open elements as long as -// the top node has a tag name of dd, dt, li, option, optgroup, p, rp, or rt. -// If exceptions are specified, nodes with that name will not be popped off. -func (p *parser) generateImpliedEndTags(exceptions ...string) { - var i int -loop: - for i = len(p.oe) - 1; i >= 0; i-- { - n := p.oe[i] - if n.Type == ElementNode { - switch n.DataAtom { - case a.Dd, a.Dt, a.Li, a.Option, a.Optgroup, a.P, a.Rp, a.Rt: - for _, except := range exceptions { - if n.Data == except { - break loop - } - } - continue - } - } - break - } - - p.oe = p.oe[:i+1] -} - -// addChild adds a child node n to the top element, and pushes n onto the stack -// of open elements if it is an element node. -func (p *parser) addChild(n *Node) { - if p.shouldFosterParent() { - p.fosterParent(n) - } else { - p.top().AppendChild(n) - } - - if n.Type == ElementNode { - p.oe = append(p.oe, n) - } -} - -// shouldFosterParent returns whether the next node to be added should be -// foster parented. -func (p *parser) shouldFosterParent() bool { - if p.fosterParenting { - switch p.top().DataAtom { - case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: - return true - } - } - return false -} - -// fosterParent adds a child node according to the foster parenting rules. -// Section 12.2.5.3, "foster parenting". -func (p *parser) fosterParent(n *Node) { - var table, parent, prev *Node - var i int - for i = len(p.oe) - 1; i >= 0; i-- { - if p.oe[i].DataAtom == a.Table { - table = p.oe[i] - break - } - } - - if table == nil { - // The foster parent is the html element. - parent = p.oe[0] - } else { - parent = table.Parent - } - if parent == nil { - parent = p.oe[i-1] - } - - if table != nil { - prev = table.PrevSibling - } else { - prev = parent.LastChild - } - if prev != nil && prev.Type == TextNode && n.Type == TextNode { - prev.Data += n.Data - return - } - - parent.InsertBefore(n, table) -} - -// addText adds text to the preceding node if it is a text node, or else it -// calls addChild with a new text node. -func (p *parser) addText(text string) { - if text == "" { - return - } - - if p.shouldFosterParent() { - p.fosterParent(&Node{ - Type: TextNode, - Data: text, - }) - return - } - - t := p.top() - if n := t.LastChild; n != nil && n.Type == TextNode { - n.Data += text - return - } - p.addChild(&Node{ - Type: TextNode, - Data: text, - }) -} - -// addElement adds a child element based on the current token. -func (p *parser) addElement() { - p.addChild(&Node{ - Type: ElementNode, - DataAtom: p.tok.DataAtom, - Data: p.tok.Data, - Attr: p.tok.Attr, - }) -} - -// Section 12.2.3.3. -func (p *parser) addFormattingElement() { - tagAtom, attr := p.tok.DataAtom, p.tok.Attr - p.addElement() - - // Implement the Noah's Ark clause, but with three per family instead of two. - identicalElements := 0 -findIdenticalElements: - for i := len(p.afe) - 1; i >= 0; i-- { - n := p.afe[i] - if n.Type == scopeMarkerNode { - break - } - if n.Type != ElementNode { - continue - } - if n.Namespace != "" { - continue - } - if n.DataAtom != tagAtom { - continue - } - if len(n.Attr) != len(attr) { - continue - } - compareAttributes: - for _, t0 := range n.Attr { - for _, t1 := range attr { - if t0.Key == t1.Key && t0.Namespace == t1.Namespace && t0.Val == t1.Val { - // Found a match for this attribute, continue with the next attribute. - continue compareAttributes - } - } - // If we get here, there is no attribute that matches a. - // Therefore the element is not identical to the new one. - continue findIdenticalElements - } - - identicalElements++ - if identicalElements >= 3 { - p.afe.remove(n) - } - } - - p.afe = append(p.afe, p.top()) -} - -// Section 12.2.3.3. -func (p *parser) clearActiveFormattingElements() { - for { - n := p.afe.pop() - if len(p.afe) == 0 || n.Type == scopeMarkerNode { - return - } - } -} - -// Section 12.2.3.3. -func (p *parser) reconstructActiveFormattingElements() { - n := p.afe.top() - if n == nil { - return - } - if n.Type == scopeMarkerNode || p.oe.index(n) != -1 { - return - } - i := len(p.afe) - 1 - for n.Type != scopeMarkerNode && p.oe.index(n) == -1 { - if i == 0 { - i = -1 - break - } - i-- - n = p.afe[i] - } - for { - i++ - clone := p.afe[i].clone() - p.addChild(clone) - p.afe[i] = clone - if i == len(p.afe)-1 { - break - } - } -} - -// Section 12.2.4. -func (p *parser) acknowledgeSelfClosingTag() { - p.hasSelfClosingToken = false -} - -// An insertion mode (section 12.2.3.1) is the state transition function from -// a particular state in the HTML5 parser's state machine. It updates the -// parser's fields depending on parser.tok (where ErrorToken means EOF). -// It returns whether the token was consumed. -type insertionMode func(*parser) bool - -// setOriginalIM sets the insertion mode to return to after completing a text or -// inTableText insertion mode. -// Section 12.2.3.1, "using the rules for". -func (p *parser) setOriginalIM() { - if p.originalIM != nil { - panic("html: bad parser state: originalIM was set twice") - } - p.originalIM = p.im -} - -// Section 12.2.3.1, "reset the insertion mode". -func (p *parser) resetInsertionMode() { - for i := len(p.oe) - 1; i >= 0; i-- { - n := p.oe[i] - if i == 0 && p.context != nil { - n = p.context - } - - switch n.DataAtom { - case a.Select: - p.im = inSelectIM - case a.Td, a.Th: - p.im = inCellIM - case a.Tr: - p.im = inRowIM - case a.Tbody, a.Thead, a.Tfoot: - p.im = inTableBodyIM - case a.Caption: - p.im = inCaptionIM - case a.Colgroup: - p.im = inColumnGroupIM - case a.Table: - p.im = inTableIM - case a.Head: - p.im = inBodyIM - case a.Body: - p.im = inBodyIM - case a.Frameset: - p.im = inFramesetIM - case a.Html: - p.im = beforeHeadIM - default: - continue - } - return - } - p.im = inBodyIM -} - -const whitespace = " \t\r\n\f" - -// Section 12.2.5.4.1. -func initialIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case CommentToken: - p.doc.AppendChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - n, quirks := parseDoctype(p.tok.Data) - p.doc.AppendChild(n) - p.quirks = quirks - p.im = beforeHTMLIM - return true - } - p.quirks = true - p.im = beforeHTMLIM - return false -} - -// Section 12.2.5.4.2. -func beforeHTMLIM(p *parser) bool { - switch p.tok.Type { - case DoctypeToken: - // Ignore the token. - return true - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case StartTagToken: - if p.tok.DataAtom == a.Html { - p.addElement() - p.im = beforeHeadIM - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head, a.Body, a.Html, a.Br: - p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.doc.AppendChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - } - p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) - return false -} - -// Section 12.2.5.4.3. -func beforeHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) - if len(p.tok.Data) == 0 { - // It was all whitespace, so ignore it. - return true - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Head: - p.addElement() - p.head = p.top() - p.im = inHeadIM - return true - case a.Html: - return inBodyIM(p) - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head, a.Body, a.Html, a.Br: - p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) - return false -} - -// Section 12.2.5.4.4. -func inHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - s := strings.TrimLeft(p.tok.Data, whitespace) - if len(s) < len(p.tok.Data) { - // Add the initial whitespace to the current node. - p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) - if s == "" { - return true - } - p.tok.Data = s - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - return inBodyIM(p) - case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta: - p.addElement() - p.oe.pop() - p.acknowledgeSelfClosingTag() - return true - case a.Script, a.Title, a.Noscript, a.Noframes, a.Style: - p.addElement() - p.setOriginalIM() - p.im = textIM - return true - case a.Head: - // Ignore the token. - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Head: - n := p.oe.pop() - if n.DataAtom != a.Head { - panic("html: bad parser state: element not found, in the in-head insertion mode") - } - p.im = afterHeadIM - return true - case a.Body, a.Html, a.Br: - p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) - return false - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) - return false -} - -// Section 12.2.5.4.6. -func afterHeadIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - s := strings.TrimLeft(p.tok.Data, whitespace) - if len(s) < len(p.tok.Data) { - // Add the initial whitespace to the current node. - p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) - if s == "" { - return true - } - p.tok.Data = s - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - return inBodyIM(p) - case a.Body: - p.addElement() - p.framesetOK = false - p.im = inBodyIM - return true - case a.Frameset: - p.addElement() - p.im = inFramesetIM - return true - case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title: - p.oe = append(p.oe, p.head) - defer p.oe.remove(p.head) - return inHeadIM(p) - case a.Head: - // Ignore the token. - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Body, a.Html, a.Br: - // Drop down to creating an implied tag. - default: - // Ignore the token. - return true - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - return true - case DoctypeToken: - // Ignore the token. - return true - } - - p.parseImpliedToken(StartTagToken, a.Body, a.Body.String()) - p.framesetOK = true - return false -} - -// copyAttributes copies attributes of src not found on dst to dst. -func copyAttributes(dst *Node, src Token) { - if len(src.Attr) == 0 { - return - } - attr := map[string]string{} - for _, t := range dst.Attr { - attr[t.Key] = t.Val - } - for _, t := range src.Attr { - if _, ok := attr[t.Key]; !ok { - dst.Attr = append(dst.Attr, t) - attr[t.Key] = t.Val - } - } -} - -// Section 12.2.5.4.7. -func inBodyIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - d := p.tok.Data - switch n := p.oe.top(); n.DataAtom { - case a.Pre, a.Listing: - if n.FirstChild == nil { - // Ignore a newline at the start of a
 block.
-				if d != "" && d[0] == '\r' {
-					d = d[1:]
-				}
-				if d != "" && d[0] == '\n' {
-					d = d[1:]
-				}
-			}
-		}
-		d = strings.Replace(d, "\x00", "", -1)
-		if d == "" {
-			return true
-		}
-		p.reconstructActiveFormattingElements()
-		p.addText(d)
-		if p.framesetOK && strings.TrimLeft(d, whitespace) != "" {
-			// There were non-whitespace characters inserted.
-			p.framesetOK = false
-		}
-	case StartTagToken:
-		switch p.tok.DataAtom {
-		case a.Html:
-			copyAttributes(p.oe[0], p.tok)
-		case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title:
-			return inHeadIM(p)
-		case a.Body:
-			if len(p.oe) >= 2 {
-				body := p.oe[1]
-				if body.Type == ElementNode && body.DataAtom == a.Body {
-					p.framesetOK = false
-					copyAttributes(body, p.tok)
-				}
-			}
-		case a.Frameset:
-			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body {
-				// Ignore the token.
-				return true
-			}
-			body := p.oe[1]
-			if body.Parent != nil {
-				body.Parent.RemoveChild(body)
-			}
-			p.oe = p.oe[:1]
-			p.addElement()
-			p.im = inFramesetIM
-			return true
-		case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-			p.popUntil(buttonScope, a.P)
-			switch n := p.top(); n.DataAtom {
-			case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-				p.oe.pop()
-			}
-			p.addElement()
-		case a.Pre, a.Listing:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-			// The newline, if any, will be dealt with by the TextToken case.
-			p.framesetOK = false
-		case a.Form:
-			if p.form == nil {
-				p.popUntil(buttonScope, a.P)
-				p.addElement()
-				p.form = p.top()
-			}
-		case a.Li:
-			p.framesetOK = false
-			for i := len(p.oe) - 1; i >= 0; i-- {
-				node := p.oe[i]
-				switch node.DataAtom {
-				case a.Li:
-					p.oe = p.oe[:i]
-				case a.Address, a.Div, a.P:
-					continue
-				default:
-					if !isSpecialElement(node) {
-						continue
-					}
-				}
-				break
-			}
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Dd, a.Dt:
-			p.framesetOK = false
-			for i := len(p.oe) - 1; i >= 0; i-- {
-				node := p.oe[i]
-				switch node.DataAtom {
-				case a.Dd, a.Dt:
-					p.oe = p.oe[:i]
-				case a.Address, a.Div, a.P:
-					continue
-				default:
-					if !isSpecialElement(node) {
-						continue
-					}
-				}
-				break
-			}
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Plaintext:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-		case a.Button:
-			p.popUntil(defaultScope, a.Button)
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-		case a.A:
-			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
-				if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A {
-					p.inBodyEndTagFormatting(a.A)
-					p.oe.remove(n)
-					p.afe.remove(n)
-					break
-				}
-			}
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement()
-		case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement()
-		case a.Nobr:
-			p.reconstructActiveFormattingElements()
-			if p.elementInScope(defaultScope, a.Nobr) {
-				p.inBodyEndTagFormatting(a.Nobr)
-				p.reconstructActiveFormattingElements()
-			}
-			p.addFormattingElement()
-		case a.Applet, a.Marquee, a.Object:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.afe = append(p.afe, &scopeMarker)
-			p.framesetOK = false
-		case a.Table:
-			if !p.quirks {
-				p.popUntil(buttonScope, a.P)
-			}
-			p.addElement()
-			p.framesetOK = false
-			p.im = inTableIM
-			return true
-		case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-			if p.tok.DataAtom == a.Input {
-				for _, t := range p.tok.Attr {
-					if t.Key == "type" {
-						if strings.ToLower(t.Val) == "hidden" {
-							// Skip setting framesetOK = false
-							return true
-						}
-					}
-				}
-			}
-			p.framesetOK = false
-		case a.Param, a.Source, a.Track:
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-		case a.Hr:
-			p.popUntil(buttonScope, a.P)
-			p.addElement()
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-			p.framesetOK = false
-		case a.Image:
-			p.tok.DataAtom = a.Img
-			p.tok.Data = a.Img.String()
-			return false
-		case a.Isindex:
-			if p.form != nil {
-				// Ignore the token.
-				return true
-			}
-			action := ""
-			prompt := "This is a searchable index. Enter search keywords: "
-			attr := []Attribute{{Key: "name", Val: "isindex"}}
-			for _, t := range p.tok.Attr {
-				switch t.Key {
-				case "action":
-					action = t.Val
-				case "name":
-					// Ignore the attribute.
-				case "prompt":
-					prompt = t.Val
-				default:
-					attr = append(attr, t)
-				}
-			}
-			p.acknowledgeSelfClosingTag()
-			p.popUntil(buttonScope, a.P)
-			p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
-			if action != "" {
-				p.form.Attr = []Attribute{{Key: "action", Val: action}}
-			}
-			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
-			p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
-			p.addText(prompt)
-			p.addChild(&Node{
-				Type:     ElementNode,
-				DataAtom: a.Input,
-				Data:     a.Input.String(),
-				Attr:     attr,
-			})
-			p.oe.pop()
-			p.parseImpliedToken(EndTagToken, a.Label, a.Label.String())
-			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
-			p.parseImpliedToken(EndTagToken, a.Form, a.Form.String())
-		case a.Textarea:
-			p.addElement()
-			p.setOriginalIM()
-			p.framesetOK = false
-			p.im = textIM
-		case a.Xmp:
-			p.popUntil(buttonScope, a.P)
-			p.reconstructActiveFormattingElements()
-			p.framesetOK = false
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Iframe:
-			p.framesetOK = false
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Noembed, a.Noscript:
-			p.addElement()
-			p.setOriginalIM()
-			p.im = textIM
-		case a.Select:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-			p.im = inSelectIM
-			return true
-		case a.Optgroup, a.Option:
-			if p.top().DataAtom == a.Option {
-				p.oe.pop()
-			}
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-		case a.Rp, a.Rt:
-			if p.elementInScope(defaultScope, a.Ruby) {
-				p.generateImpliedEndTags()
-			}
-			p.addElement()
-		case a.Math, a.Svg:
-			p.reconstructActiveFormattingElements()
-			if p.tok.DataAtom == a.Math {
-				adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
-			} else {
-				adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
-			}
-			adjustForeignAttributes(p.tok.Attr)
-			p.addElement()
-			p.top().Namespace = p.tok.Data
-			if p.hasSelfClosingToken {
-				p.oe.pop()
-				p.acknowledgeSelfClosingTag()
-			}
-			return true
-		case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
-			// Ignore the token.
-		default:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-		}
-	case EndTagToken:
-		switch p.tok.DataAtom {
-		case a.Body:
-			if p.elementInScope(defaultScope, a.Body) {
-				p.im = afterBodyIM
-			}
-		case a.Html:
-			if p.elementInScope(defaultScope, a.Body) {
-				p.parseImpliedToken(EndTagToken, a.Body, a.Body.String())
-				return false
-			}
-			return true
-		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
-			p.popUntil(defaultScope, p.tok.DataAtom)
-		case a.Form:
-			node := p.form
-			p.form = nil
-			i := p.indexOfElementInScope(defaultScope, a.Form)
-			if node == nil || i == -1 || p.oe[i] != node {
-				// Ignore the token.
-				return true
-			}
-			p.generateImpliedEndTags()
-			p.oe.remove(node)
-		case a.P:
-			if !p.elementInScope(buttonScope, a.P) {
-				p.parseImpliedToken(StartTagToken, a.P, a.P.String())
-			}
-			p.popUntil(buttonScope, a.P)
-		case a.Li:
-			p.popUntil(listItemScope, a.Li)
-		case a.Dd, a.Dt:
-			p.popUntil(defaultScope, p.tok.DataAtom)
-		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
-			p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6)
-		case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
-			p.inBodyEndTagFormatting(p.tok.DataAtom)
-		case a.Applet, a.Marquee, a.Object:
-			if p.popUntil(defaultScope, p.tok.DataAtom) {
-				p.clearActiveFormattingElements()
-			}
-		case a.Br:
-			p.tok.Type = StartTagToken
-			return false
-		default:
-			p.inBodyEndTagOther(p.tok.DataAtom)
-		}
-	case CommentToken:
-		p.addChild(&Node{
-			Type: CommentNode,
-			Data: p.tok.Data,
-		})
-	}
-
-	return true
-}
-
-func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
-	// This is the "adoption agency" algorithm, described at
-	// https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency
-
-	// TODO: this is a fairly literal line-by-line translation of that algorithm.
-	// Once the code successfully parses the comprehensive test suite, we should
-	// refactor this code to be more idiomatic.
-
-	// Steps 1-4. The outer loop.
-	for i := 0; i < 8; i++ {
-		// Step 5. Find the formatting element.
-		var formattingElement *Node
-		for j := len(p.afe) - 1; j >= 0; j-- {
-			if p.afe[j].Type == scopeMarkerNode {
-				break
-			}
-			if p.afe[j].DataAtom == tagAtom {
-				formattingElement = p.afe[j]
-				break
-			}
-		}
-		if formattingElement == nil {
-			p.inBodyEndTagOther(tagAtom)
-			return
-		}
-		feIndex := p.oe.index(formattingElement)
-		if feIndex == -1 {
-			p.afe.remove(formattingElement)
-			return
-		}
-		if !p.elementInScope(defaultScope, tagAtom) {
-			// Ignore the tag.
-			return
-		}
-
-		// Steps 9-10. Find the furthest block.
-		var furthestBlock *Node
-		for _, e := range p.oe[feIndex:] {
-			if isSpecialElement(e) {
-				furthestBlock = e
-				break
-			}
-		}
-		if furthestBlock == nil {
-			e := p.oe.pop()
-			for e != formattingElement {
-				e = p.oe.pop()
-			}
-			p.afe.remove(e)
-			return
-		}
-
-		// Steps 11-12. Find the common ancestor and bookmark node.
-		commonAncestor := p.oe[feIndex-1]
-		bookmark := p.afe.index(formattingElement)
-
-		// Step 13. The inner loop. Find the lastNode to reparent.
-		lastNode := furthestBlock
-		node := furthestBlock
-		x := p.oe.index(node)
-		// Steps 13.1-13.2
-		for j := 0; j < 3; j++ {
-			// Step 13.3.
-			x--
-			node = p.oe[x]
-			// Step 13.4 - 13.5.
-			if p.afe.index(node) == -1 {
-				p.oe.remove(node)
-				continue
-			}
-			// Step 13.6.
-			if node == formattingElement {
-				break
-			}
-			// Step 13.7.
-			clone := node.clone()
-			p.afe[p.afe.index(node)] = clone
-			p.oe[p.oe.index(node)] = clone
-			node = clone
-			// Step 13.8.
-			if lastNode == furthestBlock {
-				bookmark = p.afe.index(node) + 1
-			}
-			// Step 13.9.
-			if lastNode.Parent != nil {
-				lastNode.Parent.RemoveChild(lastNode)
-			}
-			node.AppendChild(lastNode)
-			// Step 13.10.
-			lastNode = node
-		}
-
-		// Step 14. Reparent lastNode to the common ancestor,
-		// or for misnested table nodes, to the foster parent.
-		if lastNode.Parent != nil {
-			lastNode.Parent.RemoveChild(lastNode)
-		}
-		switch commonAncestor.DataAtom {
-		case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
-			p.fosterParent(lastNode)
-		default:
-			commonAncestor.AppendChild(lastNode)
-		}
-
-		// Steps 15-17. Reparent nodes from the furthest block's children
-		// to a clone of the formatting element.
-		clone := formattingElement.clone()
-		reparentChildren(clone, furthestBlock)
-		furthestBlock.AppendChild(clone)
-
-		// Step 18. Fix up the list of active formatting elements.
-		if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark {
-			// Move the bookmark with the rest of the list.
-			bookmark--
-		}
-		p.afe.remove(formattingElement)
-		p.afe.insert(bookmark, clone)
-
-		// Step 19. Fix up the stack of open elements.
-		p.oe.remove(formattingElement)
-		p.oe.insert(p.oe.index(furthestBlock)+1, clone)
-	}
-}
-
-// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
-// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content
-// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign
-func (p *parser) inBodyEndTagOther(tagAtom a.Atom) {
-	for i := len(p.oe) - 1; i >= 0; i-- {
-		if p.oe[i].DataAtom == tagAtom {
-			p.oe = p.oe[:i]
-			break
-		}
-		if isSpecialElement(p.oe[i]) {
-			break
-		}
-	}
-}
-
-// Section 12.2.5.4.8.
-func textIM(p *parser) bool {
-	switch p.tok.Type {
-	case ErrorToken:
-		p.oe.pop()
-	case TextToken:
-		d := p.tok.Data
-		if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil {
-			// Ignore a newline at the start of a