├── .github ├── release-drafter.yml └── workflows │ ├── pull-request.yaml │ ├── release-drafter.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── codecov.yml ├── entrypoint.sh ├── function ├── data │ ├── README-group.tpl │ ├── README-with-metadata.tpl │ ├── README.tpl │ ├── item-2022.yaml │ ├── item-ignore.yaml │ ├── item.yaml │ ├── linuxsuren-bot.json │ ├── linuxsuren.json │ ├── repos.json │ ├── yaml-readme-contributors.txt │ └── yaml-readme.json ├── github.go ├── github_test.go ├── link.go └── link_test.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── plugins └── vscode └── yaml-readme ├── .eslintrc.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── command.js ├── dist └── extension.js ├── extension.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── server.proto ├── test ├── runTest.js └── suite │ ├── extension.test.js │ └── index.js └── vsc-extension-quickstart.md /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 3 | tag-template: 'v$NEXT_PATCH_VERSION' 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - 'kind/feature' 12 | - title: '🐛 Bug Fixes' 13 | labels: 14 | - 'fix' 15 | - 'bugfix' 16 | - 'bug' 17 | - 'regression' 18 | - 'kind/bug' 19 | - title: 📝 Documentation updates 20 | labels: 21 | - documentation 22 | - 'kind/doc' 23 | - title: 👻 Maintenance 24 | labels: 25 | - chore 26 | - dependencies 27 | - 'kind/chore' 28 | - 'kind/dep' 29 | - title: 🚦 Tests 30 | labels: 31 | - test 32 | - tests 33 | exclude-labels: 34 | - reverted 35 | - no-changelog 36 | - skip-changelog 37 | - invalid 38 | change-template: '* $TITLE (#$NUMBER) @$AUTHOR' 39 | template: | 40 | ## What’s Changed 41 | 42 | $CHANGES 43 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - name: Set up Go 1.18 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | id: go 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v3.0.0 23 | - name: Test 24 | run: | 25 | go test ./... -coverprofile coverage.out 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v1 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | files: coverage.out 31 | flags: unittests 32 | name: codecov-umbrella 33 | fail_ci_if_error: true 34 | - name: Run GoReleaser 35 | uses: goreleaser/goreleaser-action@v2.9.1 36 | with: 37 | version: latest 38 | args: release --skip-publish --rm-dist 39 | 40 | pluginBuild: 41 | runs-on: ubuntu-20.04 42 | steps: 43 | - uses: actions/setup-node@v3 44 | with: 45 | node-version: 16 46 | - name: Check out code 47 | uses: actions/checkout@v3.0.0 48 | - name: Set up Go 1.18 49 | uses: actions/setup-go@v3 50 | with: 51 | go-version: 1.18 52 | id: go 53 | - name: Build tool 54 | run: | 55 | make copy 56 | - name: Test 57 | run: | 58 | cd plugins/vscode/yaml-readme && npm install && npm run pretest && node ./test/suite/extension.test.js 59 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | UpdateReleaseDraft: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3.0.0 14 | - name: Unshallow 15 | run: git fetch --prune --unshallow 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v2.9.1 22 | with: 23 | version: latest 24 | args: release --rm-dist 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | items/ 2 | dist/ 3 | bin/ 4 | .idea/ 5 | .vscode/ 6 | coverage.out 7 | plugins/vscode/yaml-readme/node_modules/ 8 | *.vsix 9 | */**/.DS_Store 10 | .DS_Store 11 | .vscode-test 12 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | archives: 17 | - name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}" 18 | format_overrides: 19 | - goos: windows 20 | format: zip 21 | files: 22 | - README.md 23 | checksum: 24 | name_template: 'checksums.txt' 25 | snapshot: 26 | name_template: "{{ incpatch .Version }}-next" 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - '^docs:' 32 | - '^test:' 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 as builder 2 | 3 | WORKDIR /workspace 4 | COPY . . 5 | RUN go mod download 6 | RUN CGO_ENABLE=0 go build -ldflags "-w -s" -o yaml-readme 7 | 8 | FROM alpine:3.10 9 | 10 | LABEL "com.github.actions.name"="README helper" 11 | LABEL "com.github.actions.description"="README helper" 12 | LABEL "com.github.actions.icon"="home" 13 | LABEL "com.github.actions.color"="red" 14 | 15 | LABEL "repository"="https://github.com/linuxsuren/yaml-readme" 16 | LABEL "homepage"="https://github.com/linuxsuren/yaml-readme" 17 | LABEL "maintainer"="Rick " 18 | 19 | LABEL "Name"="README helper" 20 | 21 | ENV LC_ALL C.UTF-8 22 | ENV LANG en_US.UTF-8 23 | ENV LANGUAGE en_US.UTF-8 24 | 25 | RUN apk add --no-cache \ 26 | git \ 27 | openssh-client \ 28 | libc6-compat \ 29 | libstdc++ \ 30 | curl 31 | 32 | RUN curl -L https://github.com/LinuxSuRen/http-downloader/releases/download/v0.0.67/hd-linux-amd64.tar.gz | tar xzv hd && \ 33 | mv hd /usr/bin/hd && \ 34 | hd fetch --reset 35 | 36 | COPY entrypoint.sh /entrypoint.sh 37 | COPY --from=builder /workspace/yaml-readme /usr/bin/yaml-readme 38 | 39 | ENTRYPOINT ["/entrypoint.sh"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mkdir -p bin 3 | go build -o bin/yaml-readme . 4 | copy: build 5 | cp bin/yaml-readme /usr/local/bin 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/LinuxSuRen/yaml-readme/branch/master/graph/badge.svg?token=mnFyeD2IQ7)](https://codecov.io/gh/LinuxSuRen/yaml-readme) 2 | [![vscode](https://vsmarketplacebadge.apphb.com/version/linuxsuren.yaml-readme.svg)](https://marketplace.visualstudio.com/items?itemName=linuxsuren.yaml-readme) 3 | 4 | A helper to generate the READE file automatically. 5 | 6 | ## Get started 7 | 8 | Install it via [hd](https://github.com/LinuxSuRen/http-downloader/): 9 | 10 | ```shell 11 | hd i yaml-readme 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```shell 17 | # yaml-readme -h 18 | Usage: 19 | yaml-readme [flags] 20 | 21 | Flags: 22 | -h, --help help for yaml-readme 23 | -p, --pattern string The glob pattern with Golang spec to find files (default "items/*.yaml") 24 | -t, --template string The template file which should follow Golang template spec (default "README.tpl") 25 | ``` 26 | 27 | ### Available variables: 28 | 29 | | Name | Usage | 30 | |--------------|-------------------------------------------------------------------------------------------------| 31 | | `filename` | The filename of a particular item file. For example, `items/good.yaml`, the filename is `good`. | 32 | | `parentname` | The parent directory name. For example, `items/good.yaml`, the parent name is `items`. | 33 | | `fullpath` | The related file path of each items. | 34 | 35 | ### Available functions 36 | 37 | | Name | Usage | Description | 38 | |---------------------|----------------------------------------------------|-------------------------------------------------------------------------| 39 | | `printHelp` | `{{printHelp 'hd'}}` | Print the help text of a command | 40 | | `printToc` | `{{printToc}}` | Print the [TOC](https://en.wikipedia.org/wiki/TOC) of the template file | 41 | | `printContributors` | `{{printContributors "linuxsuren" "yaml-readme"}}` | Print all the contributors of an repository | 42 | | `printStarHistory` | `{{printStarHistory "linuxsuren" "yaml-readme"}}` | Print the star history of an repository | 43 | | `printVisitorCount` | `{{printVisitorCount "repo-id"}}` | Print the visitor count chart of an repository | 44 | | `printPages` | `{{printPages "linuxsuren"}}` | Print all the repositories that pages enabled | 45 | | `render` | `{{render true}}` | Make the value be readable, turn `true` to `:white_check_mark:` | 46 | | `gh` | `{{gh "linuxsuren" true}}` | Render a GitHub user to be a link | 47 | | `ghs` | `{{ghs "linuxsuren, linuxsuren" ","}}` | Render multiple GitHub users to be links | 48 | | `link` | `{{link "text" "link"}}` | Print a Markdown style link | 49 | | `linkOrEmpty` | `{{linkOrEmpty "text" "link"}}` | Print a Markdown style link or empty if text is none | 50 | | `ghEmoji` | `{{ghEmoji "linuxsuren"}}` | Print a Markdown style link with Emoji | 51 | 52 | > Want to use more powerful functions? Please feel free to see also [Sprig](http://masterminds.github.io/sprig/). 53 | > You could use all functions from both built-in and Sprig. 54 | 55 | ### Ignore particular items 56 | 57 | In case you want to ignore some particular items, you can put a key `ignore` with value `true`. Let's see the following sample: 58 | 59 | ```yaml 60 | name: rick 61 | ignore: true 62 | ``` 63 | 64 | ## Use in GitHub actions 65 | 66 | You could copy the following sample YAML, and change some variables according to your needs. 67 | ```yaml 68 | name: generator 69 | 70 | on: 71 | push: 72 | branches: [ master ] 73 | 74 | workflow_dispatch: 75 | 76 | jobs: 77 | build: 78 | runs-on: ubuntu-latest 79 | if: "!contains(github.event.head_commit.message, 'ci skip')" 80 | 81 | steps: 82 | - uses: actions/checkout@v3 83 | - name: Update readme 84 | uses: linuxsuren/yaml-readme@v0.0.6 85 | env: 86 | GH_TOKEN: ${{ secrets.GH_SECRETS }} 87 | with: 88 | pattern: 'config/*/*.yml' 89 | username: linuxsuren 90 | org: linuxsuren 91 | repo: hd-home 92 | ``` 93 | 94 | ### Samples 95 | 96 | Below is a simple template sample: 97 | ```gotemplate 98 | The total number of tools is: {{len .}} 99 | | Name | Latest | Download | 100 | |---|---|---| 101 | {{- range $val := .}} 102 | | {{$val.name}} | {{$val.latest}} | {{$val.download}} | 103 | {{- end}} 104 | ``` 105 | 106 | Below is a grouped data sample: 107 | ```gotemplate 108 | {{- range $key, $val := .}} 109 | Year: {{$key}} 110 | | Name | Age | 111 | |---|---| 112 | {{- range $item := $val}} 113 | | {{$item.name}} | {{$item.age}} | 114 | {{- end}} 115 | {{end}} 116 | ``` 117 | 118 | You could use the following command to render it: 119 | ```shell 120 | yaml-readme --group-by year 121 | ``` 122 | 123 | Assume there is a complex YAML like this: 124 | ```yaml 125 | metadata: 126 | annotations: 127 | group/key: 'a value' 128 | ``` 129 | 130 | then you can use the following template: 131 | ```gotemplate 132 | {{index $item.metadata.annotations "group/key"}} 133 | ``` 134 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'README helper' 2 | description: 'A helper to generate README automatically' 3 | inputs: 4 | pattern: 5 | description: 'The pattern of the items' 6 | required: true 7 | default: 'items/*.yaml' 8 | template: 9 | description: 'The template file path' 10 | required: false 11 | default: 'README.tpl' 12 | output: 13 | description: 'The output of the render result' 14 | required: true 15 | default: 'README.md' 16 | username: 17 | description: 'The username of the git repository' 18 | required: true 19 | org: 20 | description: 'The org of the current repo' 21 | required: true 22 | repo: 23 | description: 'The repo name' 24 | required: true 25 | sortby: 26 | description: 'The field which sort by' 27 | required: false 28 | groupby: 29 | description: 'The filed which group by' 30 | required: false 31 | push: 32 | description: 'Indicate if you want to push the changes automatically' 33 | default: 'true' 34 | required: true 35 | tool: 36 | description: 'The tool name which you want to install' 37 | required: false 38 | runs: 39 | using: 'docker' 40 | image: 'Dockerfile' 41 | args: 42 | - --pattern=${{ inputs.pattern }} 43 | - --username=${{ inputs.username }} 44 | - --org=${{ inputs.org }} 45 | - --repo=${{ inputs.repo }} 46 | - --sortby=${{ inputs.sortby }} 47 | - --groupby=${{ inputs.groupby }} 48 | - --output=${{ inputs.output }} 49 | - --template=${{ inputs.template }} 50 | - --push=${{ inputs.push }} 51 | - --tool=${{ inputs.tool }} 52 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.com/docs/ignoring-paths 2 | 3 | # https://docs.codecov.com/docs/commit-status 4 | # it's hard to have a coverage for some code lines 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | target: auto 10 | threshold: 0.1% 11 | patch: 12 | default: 13 | target: auto 14 | threshold: 0% 15 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while [ $# -gt 0 ]; do 4 | case "$1" in 5 | --pattern=*) 6 | pattern="${1#*=}" 7 | ;; 8 | --username=*) 9 | username="${1#*=}" 10 | ;; 11 | --org=*) 12 | org="${1#*=}" 13 | ;; 14 | --repo=*) 15 | repo="${1#*=}" 16 | ;; 17 | --sortby=*) 18 | sortby="${1#*=}" 19 | ;; 20 | --groupby=*) 21 | groupby="${1#*=}" 22 | ;; 23 | --output=*) 24 | output="${1#*=}" 25 | ;; 26 | --template=*) 27 | template="${1#*=}" 28 | ;; 29 | --push=*) 30 | push="${1#*=}" 31 | ;; 32 | --tool=*) 33 | tool="${1#*=}" 34 | ;; 35 | *) 36 | printf "***************************\n" 37 | printf "* Error: Invalid argument.*\n" 38 | printf "***************************\n" 39 | exit 1 40 | esac 41 | shift 42 | done 43 | 44 | if [ "$tool" != "" ] 45 | then 46 | echo "start to install tool $tool" 47 | hd i "$tool" 48 | fi 49 | 50 | yaml-readme -p "$pattern" --sort-by "$sortby" --group-by "$groupby" --template "$template" > "$output" 51 | 52 | if [ "$push" = "true" ] 53 | then 54 | git config --local user.email "${username}@users.noreply.github.com" 55 | git config --local user.name "${username}" 56 | git add . 57 | 58 | git commit -m "Auto commit by bot, ci skip" 59 | git push https://${username}:${GH_TOKEN}@github.com/${org}/${repo}.git 60 | fi 61 | -------------------------------------------------------------------------------- /function/data/README-group.tpl: -------------------------------------------------------------------------------- 1 | {{- range $key, $val := .}} 2 | Year: {{$key}} 3 | | Zh | En | 4 | |---|---| 5 | {{- range $item := $val}} 6 | | {{$item.zh}} | {{$item.en}} | 7 | {{- end}} 8 | {{end}} -------------------------------------------------------------------------------- /function/data/README-with-metadata.tpl: -------------------------------------------------------------------------------- 1 | #!yaml-readme -p data/financing/*.yaml --output financing.md 2 | a fake template -------------------------------------------------------------------------------- /function/data/README.tpl: -------------------------------------------------------------------------------- 1 | |中文名称|英文名称|JD| 2 | |---|---|---| 3 | {{- range $val := .}} 4 | |{{$val.zh}}|{{$val.en}}|{{$val.jd}}| 5 | {{- end}} -------------------------------------------------------------------------------- /function/data/item-2022.yaml: -------------------------------------------------------------------------------- 1 | zh: zh 2 | en: en 3 | jd: jd 4 | year: 2022 -------------------------------------------------------------------------------- /function/data/item-ignore.yaml: -------------------------------------------------------------------------------- 1 | zh: zh 2 | en: en 3 | jd: jd 4 | ignore: true -------------------------------------------------------------------------------- /function/data/item.yaml: -------------------------------------------------------------------------------- 1 | zh: zh 2 | en: en 3 | jd: jd 4 | year: 2021 -------------------------------------------------------------------------------- /function/data/linuxsuren-bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "linuxsuren-bot", 3 | "id": 39147110, 4 | "node_id": "MDQ6VXNlcjM5MTQ3MTEw", 5 | "avatar_url": "https://avatars.githubusercontent.com/u/39147110?v=4", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/linuxsuren-bot", 8 | "html_url": "https://github.com/linuxsuren-bot", 9 | "followers_url": "https://api.github.com/users/linuxsuren-bot/followers", 10 | "following_url": "https://api.github.com/users/linuxsuren-bot/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/linuxsuren-bot/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/linuxsuren-bot/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/linuxsuren-bot/subscriptions", 14 | "organizations_url": "https://api.github.com/users/linuxsuren-bot/orgs", 15 | "repos_url": "https://api.github.com/users/linuxsuren-bot/repos", 16 | "events_url": "https://api.github.com/users/linuxsuren-bot/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/linuxsuren-bot/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "LinuxSuRen-bot", 21 | "company": null, 22 | "blog": "", 23 | "location": null, 24 | "email": null, 25 | "hireable": null, 26 | "bio": null, 27 | "twitter_username": null, 28 | "public_repos": 35, 29 | "public_gists": 0, 30 | "followers": 1, 31 | "following": 2, 32 | "created_at": "2018-05-10T04:38:59Z", 33 | "updated_at": "2022-06-15T02:15:00Z" 34 | } -------------------------------------------------------------------------------- /function/data/linuxsuren.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "LinuxSuRen", 3 | "id": 1450685, 4 | "node_id": "MDQ6VXNlcjE0NTA2ODU=", 5 | "avatar_url": "https://avatars.githubusercontent.com/u/1450685?v=4", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/LinuxSuRen", 8 | "html_url": "https://github.com/LinuxSuRen", 9 | "followers_url": "https://api.github.com/users/LinuxSuRen/followers", 10 | "following_url": "https://api.github.com/users/LinuxSuRen/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/LinuxSuRen/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/LinuxSuRen/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/LinuxSuRen/subscriptions", 14 | "organizations_url": "https://api.github.com/users/LinuxSuRen/orgs", 15 | "repos_url": "https://api.github.com/users/LinuxSuRen/repos", 16 | "events_url": "https://api.github.com/users/LinuxSuRen/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/LinuxSuRen/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "Rick", 21 | "company": "@opensource-f2f @kubesphere", 22 | "blog": "https://linuxsuren.github.io/open-source-best-practice/", 23 | "location": "China", 24 | "email": null, 25 | "hireable": true, 26 | "bio": "程序员,业余开源布道者", 27 | "twitter_username": "linuxsuren", 28 | "public_repos": 706, 29 | "public_gists": 2, 30 | "followers": 595, 31 | "following": 2, 32 | "created_at": "2012-02-19T06:28:06Z", 33 | "updated_at": "2022-05-23T04:17:31Z", 34 | "private_gists": 16, 35 | "total_private_repos": 16, 36 | "owned_private_repos": 16, 37 | "disk_usage": 424624, 38 | "collaborators": 1, 39 | "two_factor_authentication": true, 40 | "plan": { 41 | "name": "free", 42 | "space": 976562499, 43 | "collaborators": 0, 44 | "private_repos": 10000 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /function/data/repos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1296269, 4 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 5 | "name": "Hello-World", 6 | "full_name": "linuxsuren/Hello-World", 7 | "owner": { 8 | "login": "linuxsuren", 9 | "id": 1, 10 | "node_id": "MDQ6VXNlcjE=", 11 | "avatar_url": "https://github.com/images/error/linuxsuren_happy.gif", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/linuxsuren", 14 | "html_url": "https://github.com/linuxsuren", 15 | "followers_url": "https://api.github.com/users/linuxsuren/followers", 16 | "following_url": "https://api.github.com/users/linuxsuren/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/linuxsuren/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/linuxsuren/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/linuxsuren/subscriptions", 20 | "organizations_url": "https://api.github.com/users/linuxsuren/orgs", 21 | "repos_url": "https://api.github.com/users/linuxsuren/repos", 22 | "events_url": "https://api.github.com/users/linuxsuren/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/linuxsuren/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "private": false, 28 | "html_url": "https://github.com/linuxsuren/Hello-World", 29 | "description": "This your first repo!", 30 | "fork": false, 31 | "url": "https://api.github.com/repos/linuxsuren/Hello-World", 32 | "archive_url": "https://api.github.com/repos/linuxsuren/Hello-World/{archive_format}{/ref}", 33 | "assignees_url": "https://api.github.com/repos/linuxsuren/Hello-World/assignees{/user}", 34 | "blobs_url": "https://api.github.com/repos/linuxsuren/Hello-World/git/blobs{/sha}", 35 | "branches_url": "https://api.github.com/repos/linuxsuren/Hello-World/branches{/branch}", 36 | "collaborators_url": "https://api.github.com/repos/linuxsuren/Hello-World/collaborators{/collaborator}", 37 | "comments_url": "https://api.github.com/repos/linuxsuren/Hello-World/comments{/number}", 38 | "commits_url": "https://api.github.com/repos/linuxsuren/Hello-World/commits{/sha}", 39 | "compare_url": "https://api.github.com/repos/linuxsuren/Hello-World/compare/{base}...{head}", 40 | "contents_url": "https://api.github.com/repos/linuxsuren/Hello-World/contents/{+path}", 41 | "contributors_url": "https://api.github.com/repos/linuxsuren/Hello-World/contributors", 42 | "deployments_url": "https://api.github.com/repos/linuxsuren/Hello-World/deployments", 43 | "downloads_url": "https://api.github.com/repos/linuxsuren/Hello-World/downloads", 44 | "events_url": "https://api.github.com/repos/linuxsuren/Hello-World/events", 45 | "forks_url": "https://api.github.com/repos/linuxsuren/Hello-World/forks", 46 | "git_commits_url": "https://api.github.com/repos/linuxsuren/Hello-World/git/commits{/sha}", 47 | "git_refs_url": "https://api.github.com/repos/linuxsuren/Hello-World/git/refs{/sha}", 48 | "git_tags_url": "https://api.github.com/repos/linuxsuren/Hello-World/git/tags{/sha}", 49 | "git_url": "git:github.com/linuxsuren/Hello-World.git", 50 | "issue_comment_url": "https://api.github.com/repos/linuxsuren/Hello-World/issues/comments{/number}", 51 | "issue_events_url": "https://api.github.com/repos/linuxsuren/Hello-World/issues/events{/number}", 52 | "issues_url": "https://api.github.com/repos/linuxsuren/Hello-World/issues{/number}", 53 | "keys_url": "https://api.github.com/repos/linuxsuren/Hello-World/keys{/key_id}", 54 | "labels_url": "https://api.github.com/repos/linuxsuren/Hello-World/labels{/name}", 55 | "languages_url": "https://api.github.com/repos/linuxsuren/Hello-World/languages", 56 | "merges_url": "https://api.github.com/repos/linuxsuren/Hello-World/merges", 57 | "milestones_url": "https://api.github.com/repos/linuxsuren/Hello-World/milestones{/number}", 58 | "notifications_url": "https://api.github.com/repos/linuxsuren/Hello-World/notifications{?since,all,participating}", 59 | "pulls_url": "https://api.github.com/repos/linuxsuren/Hello-World/pulls{/number}", 60 | "releases_url": "https://api.github.com/repos/linuxsuren/Hello-World/releases{/id}", 61 | "ssh_url": "git@github.com:linuxsuren/Hello-World.git", 62 | "stargazers_url": "https://api.github.com/repos/linuxsuren/Hello-World/stargazers", 63 | "statuses_url": "https://api.github.com/repos/linuxsuren/Hello-World/statuses/{sha}", 64 | "subscribers_url": "https://api.github.com/repos/linuxsuren/Hello-World/subscribers", 65 | "subscription_url": "https://api.github.com/repos/linuxsuren/Hello-World/subscription", 66 | "tags_url": "https://api.github.com/repos/linuxsuren/Hello-World/tags", 67 | "teams_url": "https://api.github.com/repos/linuxsuren/Hello-World/teams", 68 | "trees_url": "https://api.github.com/repos/linuxsuren/Hello-World/git/trees{/sha}", 69 | "clone_url": "https://github.com/linuxsuren/Hello-World.git", 70 | "mirror_url": "git:git.example.com/linuxsuren/Hello-World", 71 | "hooks_url": "https://api.github.com/repos/linuxsuren/Hello-World/hooks", 72 | "svn_url": "https://svn.github.com/linuxsuren/Hello-World", 73 | "homepage": "https://github.com", 74 | "language": null, 75 | "forks_count": 9, 76 | "stargazers_count": 80, 77 | "watchers_count": 80, 78 | "size": 108, 79 | "default_branch": "master", 80 | "open_issues_count": 0, 81 | "is_template": false, 82 | "topics": [ 83 | "linuxsuren", 84 | "atom", 85 | "electron", 86 | "api" 87 | ], 88 | "has_issues": true, 89 | "has_projects": true, 90 | "has_wiki": true, 91 | "has_pages": true, 92 | "has_downloads": true, 93 | "has_discussions": false, 94 | "archived": false, 95 | "disabled": false, 96 | "visibility": "public", 97 | "pushed_at": "2011-01-26T19:06:43Z", 98 | "created_at": "2011-01-26T19:01:12Z", 99 | "updated_at": "2011-01-26T19:14:43Z", 100 | "permissions": { 101 | "admin": false, 102 | "push": false, 103 | "pull": true 104 | } 105 | } 106 | ] 107 | -------------------------------------------------------------------------------- /function/data/yaml-readme-contributors.txt: -------------------------------------------------------------------------------- 1 | 2 | 9 |
3 | 4 | LinuxSuRen 5 |
6 | LinuxSuRen 7 |
8 |
10 | -------------------------------------------------------------------------------- /function/data/yaml-readme.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "login": "LinuxSuRen", 4 | "id": 1450685, 5 | "node_id": "MDQ6VXNlcjE0NTA2ODU=", 6 | "avatar_url": "https://avatars.githubusercontent.com/u/1450685?v=4", 7 | "gravatar_id": "", 8 | "url": "https://api.github.com/users/LinuxSuRen", 9 | "html_url": "https://github.com/LinuxSuRen", 10 | "followers_url": "https://api.github.com/users/LinuxSuRen/followers", 11 | "following_url": "https://api.github.com/users/LinuxSuRen/following{/other_user}", 12 | "gists_url": "https://api.github.com/users/LinuxSuRen/gists{/gist_id}", 13 | "starred_url": "https://api.github.com/users/LinuxSuRen/starred{/owner}{/repo}", 14 | "subscriptions_url": "https://api.github.com/users/LinuxSuRen/subscriptions", 15 | "organizations_url": "https://api.github.com/users/LinuxSuRen/orgs", 16 | "repos_url": "https://api.github.com/users/LinuxSuRen/repos", 17 | "events_url": "https://api.github.com/users/LinuxSuRen/events{/privacy}", 18 | "received_events_url": "https://api.github.com/users/LinuxSuRen/received_events", 19 | "type": "User", 20 | "site_admin": false, 21 | "contributions": 33 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /function/github.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | // PrintContributors from a GitHub repository 16 | func PrintContributors(owner, repo string) (output string) { 17 | api := fmt.Sprintf("https://api.github.com/repos/%s/%s/contributors", owner, repo) 18 | 19 | var ( 20 | contributors []map[string]interface{} 21 | err error 22 | ) 23 | 24 | if contributors, err = ghRequestAsSlice(api); err == nil { 25 | var text string 26 | group := 6 27 | for i := 0; i < len(contributors); { 28 | next := i + group 29 | if next > len(contributors) { 30 | next = len(contributors) 31 | } 32 | text = text + "" + generateContributor(contributors[i:next]) + "" 33 | i = next 34 | } 35 | 36 | output = fmt.Sprintf(`%s
37 | `, text) 38 | } 39 | return 40 | } 41 | 42 | // PrintPages prints the repositories which enabled pages 43 | func PrintPages(owner string) (output string) { 44 | api := fmt.Sprintf("https://api.github.com/users/%s/repos?type=owner&per_page=100&sort=updated&username=%s", owner, owner) 45 | 46 | var ( 47 | repos []map[string]interface{} 48 | err error 49 | ) 50 | 51 | if repos, err = ghRequestAsSlice(api); err == nil { 52 | var text string 53 | for i := 0; i < len(repos); i++ { 54 | repo := strings.TrimSpace(generateRepo(repos[i])) 55 | if repo != "" { 56 | text = text + repo + "\n" 57 | } 58 | } 59 | 60 | output = fmt.Sprintf(`|||| 61 | |---|---|---| 62 | %s`, strings.TrimSpace(text)) 63 | } 64 | return 65 | } 66 | 67 | func ghRequest(api string) (data []byte, err error) { 68 | var ( 69 | resp *http.Response 70 | req *http.Request 71 | ) 72 | 73 | if req, err = http.NewRequest(http.MethodGet, api, nil); err == nil { 74 | token := os.Getenv("GITHUB_TOKEN") 75 | if token == "" { 76 | token = os.Getenv("GH_TOKEN") 77 | } 78 | if token != "" { 79 | req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) 80 | } 81 | 82 | if resp, err = http.DefaultClient.Do(req); err == nil && resp.StatusCode == http.StatusOK { 83 | data, err = ioutil.ReadAll(resp.Body) 84 | } 85 | } 86 | return 87 | } 88 | 89 | func ghRequestAsSlice(api string) (data []map[string]interface{}, err error) { 90 | var byteData []byte 91 | if byteData, err = ghRequest(api); err == nil { 92 | err = json.Unmarshal(byteData, &data) 93 | } 94 | return 95 | } 96 | 97 | func ghRequestAsMap(api string) (data map[string]interface{}, err error) { 98 | var byteData []byte 99 | if byteData, err = ghRequest(api); err == nil { 100 | err = json.Unmarshal(byteData, &data) 101 | } 102 | return 103 | } 104 | 105 | var pageRepoTemplate = ` 106 | {{if eq .has_pages true}} 107 | |{{.name}}|![GitHub Repo stars](https://img.shields.io/github/stars/{{.owner.login}}/{{.name}}?style=social)|[view](https://{{.owner.login}}.github.io/{{.name}}/)| 108 | {{end}} 109 | ` 110 | 111 | func generateRepo(repo interface{}) (output string) { 112 | var tpl *template.Template 113 | var err error 114 | if tpl, err = template.New("repo").Parse(pageRepoTemplate); err == nil { 115 | buf := bytes.NewBuffer([]byte{}) 116 | if err = tpl.Execute(buf, repo); err == nil { 117 | output = buf.String() 118 | } 119 | } 120 | return 121 | } 122 | 123 | func generateContributor(contributors []map[string]interface{}) (output string) { 124 | var tpl *template.Template 125 | var err error 126 | if tpl, err = template.New("contributors").Parse(contributorsTpl); err == nil { 127 | buf := bytes.NewBuffer([]byte{}) 128 | if err = tpl.Execute(buf, contributors); err == nil { 129 | output = buf.String() 130 | } 131 | } 132 | return 133 | } 134 | 135 | var contributorsTpl = `{{- range $i, $val := .}} 136 | 137 | 138 | {{$val.login}} 139 |
140 | {{$val.login}} 141 |
142 | 143 | {{- end}} 144 | ` 145 | 146 | // GitHubUsersLink parses a text and try to make the potential GitHub IDs be links 147 | func GitHubUsersLink(ids, sep string) (links string) { 148 | if sep == "" { 149 | sep = " " 150 | } 151 | 152 | splits := strings.Split(ids, sep) 153 | var items []string 154 | for _, item := range splits { 155 | items = append(items, GithubUserLink(strings.TrimSpace(item), false)) 156 | } 157 | 158 | // having additional whitespace it's an ASCII character 159 | if sep == "," { 160 | sep = sep + " " 161 | } 162 | links = strings.Join(items, sep) 163 | return 164 | } 165 | 166 | // GithubUserLink makes a GitHub user link 167 | func GithubUserLink(id string, bio bool) (link string) { 168 | link = id 169 | if strings.Contains(id, " ") { // only handle the valid GitHub ID 170 | return 171 | } 172 | 173 | // return the original text if there are Markdown style link exist 174 | if hasLink(id) { 175 | if bio { 176 | return GithubUserLink(GetIDFromGHLink(id), bio) 177 | } 178 | return 179 | } 180 | 181 | api := fmt.Sprintf("https://api.github.com/users/%s", id) 182 | 183 | var ( 184 | err error 185 | data map[string]interface{} 186 | ) 187 | if data, err = ghRequestAsMap(api); err == nil { 188 | link = fmt.Sprintf("[%s](%s)", data["name"], data["html_url"]) 189 | if bioText, ok := data["bio"]; ok && bio && bioText != nil { 190 | link = fmt.Sprintf("%s (%s)", link, bioText) 191 | } 192 | } 193 | return 194 | } 195 | 196 | // GitHubEmojiLink returns a Markdown style link or empty 197 | func GitHubEmojiLink(user string) (output string) { 198 | if user != "" { 199 | output = Link(":octocat:", fmt.Sprintf("https://github.com/%s", user)) 200 | } 201 | return 202 | } 203 | 204 | // GetIDFromGHLink return the GitHub ID from a link 205 | func GetIDFromGHLink(link string) string { 206 | reg, _ := regexp.Compile("\\[.*\\]\\(.*/|\\)") 207 | return reg.ReplaceAllString(link, "") 208 | } 209 | 210 | // PrintUserAsTable generates a table for a GitHub user 211 | func PrintUserAsTable(id string) (result string) { 212 | api := fmt.Sprintf("https://api.github.com/users/%s", id) 213 | 214 | result = `||| 215 | |---|---| 216 | ` 217 | 218 | var ( 219 | err error 220 | data map[string]interface{} 221 | ) 222 | if data, err = ghRequestAsMap(api); err == nil { 223 | result = result + addWithEmpty("Name", "name", data) + 224 | addWithEmpty("Location", "location", data) + 225 | addWithEmpty("Bio", "bio", data) + 226 | addWithEmpty("Blog", "blog", data) + 227 | addWithEmpty("Twitter", "twitter_username", data) + 228 | addWithEmpty("Organization", "company", data) 229 | } 230 | return 231 | } 232 | 233 | func addWithEmpty(title, key string, data map[string]interface{}) (result string) { 234 | if val, ok := data[key]; ok && val != "" { 235 | desc := val 236 | switch key { 237 | case "twitter_username": 238 | desc = fmt.Sprintf("[%s](https://twitter.com/%s)", val, val) 239 | } 240 | result = fmt.Sprintf(`| %s | %s | 241 | `, title, desc) 242 | } 243 | return 244 | } 245 | 246 | // hasLink determines if there are Markdown style links 247 | func hasLink(text string) (ok bool) { 248 | reg, _ := regexp.Compile(".*\\[.*\\]\\(.*\\)") 249 | ok = reg.MatchString(text) 250 | return 251 | } 252 | -------------------------------------------------------------------------------- /function/github_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/h2non/gock" 7 | "github.com/stretchr/testify/assert" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func Test_printContributor(t *testing.T) { 15 | type args struct { 16 | owner string 17 | repo string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | prepare func() 23 | wantOutput func() string 24 | }{{ 25 | name: "normal case", 26 | args: args{ 27 | owner: "linuxsuren", 28 | repo: "yaml-readme", 29 | }, 30 | prepare: func() { 31 | gock.New("https://api.github.com"). 32 | Get("/repos/linuxsuren/yaml-readme/contributors"). 33 | Reply(http.StatusOK). 34 | File("data/yaml-readme.json") 35 | }, 36 | wantOutput: func() string { 37 | data, _ := ioutil.ReadFile("data/yaml-readme-contributors.txt") 38 | return string(data) 39 | }, 40 | }} 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | defer gock.Off() 44 | tt.prepare() 45 | assert.Equalf(t, tt.wantOutput(), PrintContributors(tt.args.owner, tt.args.repo), "printContributors(%v, %v)", tt.args.owner, tt.args.repo) 46 | }) 47 | } 48 | } 49 | 50 | func TestGithubUserLink(t *testing.T) { 51 | type args struct { 52 | id string 53 | bio bool 54 | } 55 | tests := []struct { 56 | name string 57 | mockUser string 58 | args args 59 | want string 60 | }{{ 61 | name: "normal case without bio", 62 | mockUser: "linuxsuren", 63 | args: args{ 64 | id: "linuxsuren", 65 | bio: false, 66 | }, 67 | want: `[Rick](https://github.com/LinuxSuRen)`, 68 | }, { 69 | name: "normal case with bio", 70 | mockUser: "linuxsuren", 71 | args: args{ 72 | id: "linuxsuren", 73 | bio: true, 74 | }, 75 | want: `[Rick](https://github.com/LinuxSuRen) (程序员,业余开源布道者)`, 76 | }, { 77 | name: "with whitespace", 78 | mockUser: "linuxsuren", 79 | args: args{ 80 | id: "this is not id", 81 | bio: false, 82 | }, 83 | want: "this is not id", 84 | }, { 85 | name: "has Markdown style link", 86 | mockUser: "linuxsuren", 87 | args: args{ 88 | id: "[name](link)", 89 | bio: false, 90 | }, 91 | want: "[name](link)", 92 | }, { 93 | name: "has Markdown style link, want bio", 94 | mockUser: "linuxsuren", 95 | args: args{ 96 | id: "[Rick](https://github.com/linuxsuren)", 97 | bio: true, 98 | }, 99 | want: `[Rick](https://github.com/LinuxSuRen) (程序员,业余开源布道者)`, 100 | }, { 101 | name: "do not have bio", 102 | mockUser: "linuxsuren-bot", 103 | args: args{ 104 | id: "linuxsuren-bot", 105 | bio: true, 106 | }, 107 | want: `[LinuxSuRen-bot](https://github.com/linuxsuren-bot)`, 108 | }} 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | defer gock.Off() 112 | mockGitHubUser(tt.mockUser) 113 | assert.Equalf(t, tt.want, GithubUserLink(tt.args.id, tt.args.bio), "GithubUserLink(%v, %v)", tt.args.id, tt.args.bio) 114 | }) 115 | } 116 | } 117 | 118 | func mockGitHubUser(id string) { 119 | gock.New("https://api.github.com"). 120 | Get(fmt.Sprintf("/users/%s", id)).Reply(http.StatusOK).File(fmt.Sprintf("data/%s.json", id)) 121 | } 122 | 123 | func mockUserRepos(owner string) { 124 | gock.New("https://api.github.com"). 125 | Get(fmt.Sprintf("/users/%s/repos", owner)). 126 | MatchParam("type", "owner"). 127 | MatchParam("per_page", "100"). 128 | MatchParam("sort", "updated"). 129 | MatchParam("username", owner). 130 | Reply(http.StatusOK).File("data/repos.json") 131 | } 132 | 133 | func TestGitHubUsersLink(t *testing.T) { 134 | type args struct { 135 | ids string 136 | sep string 137 | } 138 | tests := []struct { 139 | name string 140 | prepare func() 141 | args args 142 | wantLinks string 143 | }{{ 144 | name: "two GitHub users", 145 | prepare: func() { 146 | mockGitHubUser("linuxsuren") 147 | mockGitHubUser("linuxsuren") 148 | }, 149 | args: args{ 150 | ids: "linuxsuren linuxsuren", 151 | sep: "", 152 | }, 153 | wantLinks: "[Rick](https://github.com/LinuxSuRen) [Rick](https://github.com/LinuxSuRen)", 154 | }, { 155 | name: "two GitHub users with Chinese character as separate", 156 | prepare: func() { 157 | mockGitHubUser("linuxsuren") 158 | mockGitHubUser("linuxsuren") 159 | }, 160 | args: args{ 161 | ids: "linuxsuren、linuxsuren", 162 | sep: "、", 163 | }, 164 | wantLinks: "[Rick](https://github.com/LinuxSuRen)、[Rick](https://github.com/LinuxSuRen)", 165 | }, { 166 | name: "two GitHub users with whitespace and comma as separate", 167 | prepare: func() { 168 | mockGitHubUser("linuxsuren") 169 | mockGitHubUser("linuxsuren") 170 | }, 171 | args: args{ 172 | ids: "linuxsuren, linuxsuren", 173 | sep: ",", 174 | }, 175 | wantLinks: "[Rick](https://github.com/LinuxSuRen), [Rick](https://github.com/LinuxSuRen)", 176 | }} 177 | for _, tt := range tests { 178 | t.Run(tt.name, func(t *testing.T) { 179 | defer gock.Off() 180 | tt.prepare() 181 | assert.Equalf(t, tt.wantLinks, GitHubUsersLink(tt.args.ids, tt.args.sep), "GitHubUsersLink(%v, %v)", tt.args.ids, tt.args.sep) 182 | }) 183 | } 184 | } 185 | 186 | func Test_hasLink(t *testing.T) { 187 | type args struct { 188 | text string 189 | } 190 | tests := []struct { 191 | name string 192 | args args 193 | wantOk bool 194 | }{{ 195 | name: "normal text", 196 | args: args{ 197 | text: "This is a normal text", 198 | }, 199 | wantOk: false, 200 | }, { 201 | name: "has Markdown style link", 202 | args: args{ 203 | text: "[here](link)", 204 | }, 205 | wantOk: true, 206 | }, { 207 | name: "more complex Markdown style link", 208 | args: args{ 209 | text: "Hi there, this is [my card](link).", 210 | }, 211 | wantOk: true, 212 | }, { 213 | name: "multiple Markdown style link", 214 | args: args{ 215 | text: "I have two links, [one](link) and [two](link).", 216 | }, 217 | wantOk: true, 218 | }} 219 | for _, tt := range tests { 220 | t.Run(tt.name, func(t *testing.T) { 221 | assert.Equalf(t, tt.wantOk, hasLink(tt.args.text), "hasLink(%v)", tt.args.text) 222 | }) 223 | } 224 | } 225 | 226 | func Test_ghRequest(t *testing.T) { 227 | type args struct { 228 | api string 229 | } 230 | tests := []struct { 231 | name string 232 | args args 233 | wantData []byte 234 | wantErr assert.ErrorAssertionFunc 235 | }{{ 236 | name: "with token", 237 | args: args{ 238 | api: "https://fake.com", 239 | }, 240 | wantData: []byte("body"), 241 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 242 | assert.Nil(t, err) 243 | return true 244 | }, 245 | }} 246 | for _, tt := range tests { 247 | t.Run(tt.name, func(t *testing.T) { 248 | oldToken := os.Getenv("GITHUB_TOKEN") 249 | _ = os.Setenv("GITHUB_TOKEN", "fake") 250 | defer func() { 251 | _ = os.Setenv("GITHUB_TOKEN", oldToken) 252 | }() 253 | 254 | gock.New(tt.args.api).Get("/").Reply(http.StatusOK).Body(bytes.NewBufferString("body")) 255 | 256 | gotData, err := ghRequest(tt.args.api) 257 | if !tt.wantErr(t, err, fmt.Sprintf("ghRequest(%v)", tt.args.api)) { 258 | return 259 | } 260 | assert.Equalf(t, tt.wantData, gotData, "ghRequest(%v)", tt.args.api) 261 | }) 262 | } 263 | } 264 | 265 | func TestGitHubEmojiLink(t *testing.T) { 266 | type args struct { 267 | user string 268 | } 269 | tests := []struct { 270 | name string 271 | args args 272 | wantOutput string 273 | }{{ 274 | name: "user is empty", 275 | wantOutput: "", 276 | }, { 277 | name: "user is not empty", 278 | args: args{ 279 | user: "linuxsuren", 280 | }, 281 | wantOutput: "[:octocat:](https://github.com/linuxsuren)", 282 | }} 283 | for _, tt := range tests { 284 | t.Run(tt.name, func(t *testing.T) { 285 | assert.Equalf(t, tt.wantOutput, GitHubEmojiLink(tt.args.user), "GitHubEmojiLink(%v)", tt.args.user) 286 | }) 287 | } 288 | } 289 | 290 | func Test_getIDFromGHLink(t *testing.T) { 291 | type args struct { 292 | link string 293 | } 294 | tests := []struct { 295 | name string 296 | args args 297 | want string 298 | }{{ 299 | name: "normal", 300 | args: args{ 301 | link: "[Rick](https://github.com/LinuxSuRen)", 302 | }, 303 | want: "LinuxSuRen", 304 | }} 305 | for _, tt := range tests { 306 | t.Run(tt.name, func(t *testing.T) { 307 | assert.Equalf(t, tt.want, GetIDFromGHLink(tt.args.link), "GetIDFromGHLink(%v)", tt.args.link) 308 | }) 309 | } 310 | } 311 | 312 | func TestPrintUserAsTable(t *testing.T) { 313 | type args struct { 314 | id string 315 | } 316 | tests := []struct { 317 | name string 318 | args args 319 | wantResult string 320 | }{{ 321 | name: "normal", 322 | args: args{ 323 | id: "linuxsuren", 324 | }, 325 | wantResult: `||| 326 | |---|---| 327 | | Name | Rick | 328 | | Location | China | 329 | | Bio | 程序员,业余开源布道者 | 330 | | Blog | https://linuxsuren.github.io/open-source-best-practice/ | 331 | | Twitter | [linuxsuren](https://twitter.com/linuxsuren) | 332 | | Organization | @opensource-f2f @kubesphere | 333 | `, 334 | }} 335 | for _, tt := range tests { 336 | t.Run(tt.name, func(t *testing.T) { 337 | defer gock.Off() 338 | mockGitHubUser(tt.args.id) 339 | assert.Equalf(t, tt.wantResult, PrintUserAsTable(tt.args.id), "PrintUserAsTable(%v)", tt.args.id) 340 | }) 341 | } 342 | } 343 | 344 | func TestPrintPages(t *testing.T) { 345 | type args struct { 346 | owner string 347 | } 348 | tests := []struct { 349 | name string 350 | args args 351 | wantOutput string 352 | }{{ 353 | name: "normal", 354 | args: args{owner: "linuxsuren"}, 355 | wantOutput: `|||| 356 | |---|---|---| 357 | |Hello-World|![GitHub Repo stars](https://img.shields.io/github/stars/linuxsuren/Hello-World?style=social)|[view](https://linuxsuren.github.io/Hello-World/)|`, 358 | }} 359 | for _, tt := range tests { 360 | t.Run(tt.name, func(t *testing.T) { 361 | defer gock.Off() 362 | mockUserRepos(tt.args.owner) 363 | assert.Equalf(t, tt.wantOutput, PrintPages(tt.args.owner), "PrintPages(%v)", tt.args.owner) 364 | }) 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /function/link.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import "fmt" 4 | 5 | // LinkOrEmpty returns a Markdown style link or empty if the link is none 6 | func LinkOrEmpty(text, link string) (output string) { 7 | if output = Link(text, link); output == text { 8 | output = "" 9 | } 10 | return 11 | } 12 | 13 | // Link returns a Markdown style link 14 | func Link(text, link string) (output string) { 15 | output = text 16 | if link != "" { 17 | output = fmt.Sprintf("[%s](%s)", text, link) 18 | } 19 | return 20 | } 21 | 22 | // TwitterLink returns a Markdown style link of Twitter 23 | func TwitterLink(user string) (output string) { 24 | if user != "" { 25 | output = fmt.Sprintf("[![twitter](%s)](https://twitter.com/%s)", GStatic("twitter"), user) 26 | } 27 | return 28 | } 29 | 30 | // YouTubeLink returns a Markdown style link of YouTube 31 | func YouTubeLink(id string) (output string) { 32 | if id != "" { 33 | output = fmt.Sprintf("[![youtube](%s)](https://www.youtube.com/%s)", GStatic("youtube"), id) 34 | } 35 | return 36 | } 37 | 38 | // GStatic returns the known gstatic image URL 39 | func GStatic(id string) (output string) { 40 | var tbn string 41 | switch id { 42 | case "youtube": 43 | tbn = "ANd9GcRY4no9kYJtEAHXBEY2GDprV__HH1zc94olyS6G6fT5isS71bPyqvIi7-9VE1MMy3_3vsNOQLAerwcSQqGNyADWfxKpd2hLc8HuacZdgEjgZc_WLN8" 44 | case "twitter": 45 | tbn = "ANd9GcTA3XDrUCnqJvmP3gfZKpXtV8ZO23EalnKszft6-V73d8G2Lt54v9TEnnkeO_MXseXmT5ERutOo0yPqoODJkFPtvxCeQbg_PYDJjXDAFfIMzM2p4bI" 46 | } 47 | output = fmt.Sprintf("https://encrypted-tbn3.gstatic.com/favicon-tbn?q=tbn:%s", tbn) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /function/link_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestLink(t *testing.T) { 9 | type args struct { 10 | text string 11 | link string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | wantOutput string 17 | }{{ 18 | name: "link is not empty", 19 | args: args{ 20 | text: "text", 21 | link: "link", 22 | }, 23 | wantOutput: "[text](link)", 24 | }, { 25 | name: "link is empty", 26 | args: args{ 27 | text: "text", 28 | }, 29 | wantOutput: "text", 30 | }} 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | assert.Equalf(t, tt.wantOutput, Link(tt.args.text, tt.args.link), "Link(%v, %v)", tt.args.text, tt.args.link) 34 | }) 35 | } 36 | } 37 | 38 | func TestLinkOrEmpty(t *testing.T) { 39 | type args struct { 40 | text string 41 | link string 42 | } 43 | tests := []struct { 44 | name string 45 | args args 46 | wantOutput string 47 | }{{ 48 | name: "link is empty", 49 | args: args{ 50 | text: "text", 51 | }, 52 | }, { 53 | name: "link is not empty", 54 | args: args{ 55 | text: "text", 56 | link: "link", 57 | }, 58 | wantOutput: "[text](link)", 59 | }} 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | assert.Equalf(t, tt.wantOutput, LinkOrEmpty(tt.args.text, tt.args.link), "LinkOrEmpty(%v, %v)", tt.args.text, tt.args.link) 63 | }) 64 | } 65 | } 66 | 67 | func TestTwitterLink(t *testing.T) { 68 | type args struct { 69 | user string 70 | } 71 | tests := []struct { 72 | name string 73 | args args 74 | wantOutput string 75 | }{{ 76 | name: "user is empty", 77 | }, { 78 | name: "user is not empty", 79 | args: args{ 80 | user: "linuxsuren", 81 | }, 82 | wantOutput: "[![twitter](https://encrypted-tbn3.gstatic.com/favicon-tbn?q=tbn:ANd9GcTA3XDrUCnqJvmP3gfZKpXtV8ZO23EalnKszft6-V73d8G2Lt54v9TEnnkeO_MXseXmT5ERutOo0yPqoODJkFPtvxCeQbg_PYDJjXDAFfIMzM2p4bI)](https://twitter.com/linuxsuren)", 83 | }} 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | assert.Equalf(t, tt.wantOutput, TwitterLink(tt.args.user), "TwitterLink(%v)", tt.args.user) 87 | }) 88 | } 89 | } 90 | 91 | func TestYouTubeLink(t *testing.T) { 92 | type args struct { 93 | id string 94 | } 95 | tests := []struct { 96 | name string 97 | args args 98 | wantOutput string 99 | }{{ 100 | name: "empty", 101 | }, { 102 | name: "not empty", 103 | args: args{ 104 | id: "channel/UC63xz3pq26BBgwB3cnwCoqQ", 105 | }, 106 | wantOutput: "[![youtube](https://encrypted-tbn3.gstatic.com/favicon-tbn?q=tbn:ANd9GcRY4no9kYJtEAHXBEY2GDprV__HH1zc94olyS6G6fT5isS71bPyqvIi7-9VE1MMy3_3vsNOQLAerwcSQqGNyADWfxKpd2hLc8HuacZdgEjgZc_WLN8)](https://www.youtube.com/channel/UC63xz3pq26BBgwB3cnwCoqQ)", 107 | }} 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | assert.Equalf(t, tt.wantOutput, YouTubeLink(tt.args.id), "YouTubeLink(%v)", tt.args.id) 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/linuxsuren/yaml-readme 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/Masterminds/sprig v2.22.0+incompatible 7 | github.com/h2non/gock v1.0.9 8 | github.com/spf13/cobra v1.4.0 9 | github.com/stretchr/testify v1.7.1 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/Masterminds/goutils v1.1.1 // indirect 15 | github.com/Masterminds/semver v1.5.0 // indirect 16 | github.com/davecgh/go-spew v1.1.0 // indirect 17 | github.com/google/uuid v1.3.0 // indirect 18 | github.com/huandu/xstrings v1.3.2 // indirect 19 | github.com/imdario/mergo v0.3.13 // indirect 20 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 21 | github.com/mitchellh/copystructure v1.2.0 // indirect 22 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 23 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect 27 | gopkg.in/yaml.v3 v3.0.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 4 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 5 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 6 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 11 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/h2non/gock v1.0.9 h1:17gCehSo8ZOgEsFKpQgqHiR7VLyjxdAG3lkhVvO9QZU= 13 | github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE= 14 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 15 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 16 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 17 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 18 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 19 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 20 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 21 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 22 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 23 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 24 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= 25 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 29 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 30 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 31 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 32 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 35 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= 37 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 38 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 39 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 43 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 48 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 51 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/Masterminds/sprig" 7 | "github.com/linuxsuren/yaml-readme/function" 8 | "github.com/spf13/cobra" 9 | "gopkg.in/yaml.v2" 10 | "html/template" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "regexp" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | var logger *log.Logger 24 | 25 | type option struct { 26 | pattern string 27 | templateFile string 28 | includeHeader bool 29 | sortBy string 30 | groupBy string 31 | 32 | printFunctions bool 33 | printVariables bool 34 | } 35 | 36 | func loadMetadata(pattern, groupBy string) (items []map[string]interface{}, 37 | groupData map[string][]map[string]interface{}, err error) { 38 | groupData = make(map[string][]map[string]interface{}) 39 | 40 | // find YAML files 41 | var files []string 42 | var data []byte 43 | if files, err = filepath.Glob(pattern); err == nil { 44 | for _, metaFile := range files { 45 | if data, err = ioutil.ReadFile(metaFile); err != nil { 46 | logger.Printf("failed to read file [%s], error: %v\n", metaFile, err) 47 | continue 48 | } 49 | 50 | metaMap := make(map[string]interface{}) 51 | if err = yaml.Unmarshal(data, metaMap); err != nil { 52 | logger.Printf("failed to parse file [%s] as a YAML, error: %v\n", metaFile, err) 53 | continue 54 | } 55 | 56 | // skip this item if there is a 'ignore' key is true 57 | if val, ok := metaMap["ignore"]; ok { 58 | if ignore, ok := val.(bool); ok && ignore { 59 | continue 60 | } 61 | } 62 | 63 | filename := strings.TrimSuffix(filepath.Base(metaFile), filepath.Ext(metaFile)) 64 | parentname := filepath.Base(filepath.Dir(metaFile)) 65 | 66 | metaMap["filename"] = filename 67 | metaMap["parentname"] = parentname 68 | metaMap["fullpath"] = metaFile 69 | 70 | if val, ok := metaMap[groupBy]; ok && val != "" { 71 | var strVal string 72 | switch val.(type) { 73 | case string: 74 | strVal = val.(string) 75 | case int: 76 | strVal = strconv.Itoa(val.(int)) 77 | } 78 | 79 | if _, ok := groupData[strVal]; ok { 80 | groupData[strVal] = append(groupData[strVal], metaMap) 81 | } else { 82 | groupData[strVal] = []map[string]interface{}{ 83 | metaMap, 84 | } 85 | } 86 | } 87 | 88 | items = append(items, metaMap) 89 | } 90 | } 91 | return 92 | } 93 | 94 | func sortMetadata(items []map[string]interface{}, sortByField string) { 95 | descending := true 96 | if strings.HasPrefix(sortByField, "!") { 97 | sortByField = strings.TrimPrefix(sortByField, "!") 98 | descending = false 99 | } 100 | sortBy(items, sortByField, descending) 101 | } 102 | 103 | func loadTemplate(templateFile string, includeHeader bool) (readmeTpl string, err error) { 104 | // load readme template 105 | var data []byte 106 | if data, err = ioutil.ReadFile(templateFile); err != nil { 107 | fmt.Printf("failed to load README template, error: %v\n", err) 108 | err = nil 109 | readmeTpl = `|中文名称|英文名称|JD| 110 | |---|---|---| 111 | {{- range $val := .}} 112 | |{{$val.zh}}|{{$val.en}}|{{$val.jd}}| 113 | {{- end}}` 114 | } 115 | if includeHeader { 116 | readmeTpl = fmt.Sprintf("> This file was generated by [%s](%s) via [yaml-readme](https://github.com/LinuxSuRen/yaml-readme), please don't edit it directly!\n\n", 117 | filepath.Base(templateFile), filepath.Base(templateFile)) 118 | } 119 | readmeTpl = readmeTpl + string(data) 120 | s, err := regexp.Compile("#!yaml-readme .*\n") 121 | readmeTpl = s.ReplaceAllString(readmeTpl, "") 122 | return 123 | } 124 | 125 | func (o *option) runE(cmd *cobra.Command, args []string) (err error) { 126 | logger = log.New(cmd.ErrOrStderr(), "", log.LstdFlags) 127 | if o.printFunctions { 128 | printFunctions(cmd.OutOrStdout()) 129 | return 130 | } 131 | 132 | if o.printVariables { 133 | printVariables(cmd.OutOrStdout()) 134 | return 135 | } 136 | 137 | // load metadata from YAML files 138 | var items []map[string]interface{} 139 | var groupData map[string][]map[string]interface{} 140 | if items, groupData, err = loadMetadata(o.pattern, o.groupBy); err != nil { 141 | err = fmt.Errorf("failed to load metadat from %q", o.pattern) 142 | return 143 | } 144 | 145 | if o.sortBy != "" { 146 | sortMetadata(items, o.sortBy) 147 | } 148 | 149 | // load readme template 150 | var readmeTpl string 151 | if readmeTpl, err = loadTemplate(o.templateFile, o.includeHeader); err != nil { 152 | err = fmt.Errorf("failed to load template file from %q", o.templateFile) 153 | return 154 | } 155 | 156 | // render it with grouped data 157 | if o.groupBy != "" { 158 | err = renderTemplate(readmeTpl, groupData, cmd.OutOrStdout()) 159 | } else { 160 | err = renderTemplate(readmeTpl, items, cmd.OutOrStdout()) 161 | } 162 | return 163 | } 164 | 165 | func renderTemplateToString(tplContent string, object interface{}) (output string, err error) { 166 | buf := bytes.NewBuffer([]byte{}) 167 | if err = renderTemplate(tplContent, object, buf); err == nil { 168 | output = buf.String() 169 | } 170 | return 171 | } 172 | 173 | func renderTemplate(tplContent string, object interface{}, writer io.Writer) (err error) { 174 | var tpl *template.Template 175 | if tpl, err = template.New("readme"). 176 | Funcs(getFuncMap(tplContent)). 177 | Funcs(sprig.FuncMap()).Parse(tplContent); err == nil { 178 | err = tpl.Execute(writer, object) 179 | } 180 | return 181 | } 182 | 183 | func printVariables(stdout io.Writer) { 184 | _, _ = stdout.Write([]byte(`filename 185 | parentname 186 | fullpath`)) 187 | } 188 | 189 | func printFunctions(stdout io.Writer) { 190 | funcMap := getFuncMap("") 191 | var funcs []string 192 | for k := range funcMap { 193 | funcs = append(funcs, k) 194 | } 195 | sort.SliceStable(funcs, func(i, j int) bool { 196 | return strings.Compare(funcs[i], funcs[j]) < 0 197 | }) 198 | _, _ = stdout.Write([]byte(strings.Join(funcs, "\n"))) 199 | } 200 | 201 | func getFuncMap(readmeTpl string) template.FuncMap { 202 | return template.FuncMap{ 203 | "printHelp": func(cmd string) (output string) { 204 | var err error 205 | var data []byte 206 | if data, err = exec.Command(cmd, "--help").Output(); err != nil { 207 | _, _ = fmt.Fprintln(os.Stderr, "failed to run command", cmd) 208 | } else { 209 | output = fmt.Sprintf(`%s 210 | %s 211 | %s`, "```shell", string(data), "```") 212 | } 213 | return 214 | }, 215 | "printToc": func() string { 216 | return generateTOC(readmeTpl) 217 | }, 218 | "printContributors": func(owner, repo string) template.HTML { 219 | return template.HTML(function.PrintContributors(owner, repo)) 220 | }, 221 | "printStarHistory": func(owner, repo string) string { 222 | return printStarHistory(owner, repo) 223 | }, 224 | "printVisitorCount": func(id string) string { 225 | return fmt.Sprintf(`![Visitor Count](https://profile-counter.glitch.me/%s/count.svg)`, id) 226 | }, 227 | "printPages": func(owner string) string { 228 | return function.PrintPages(owner) 229 | }, 230 | "render": dataRender, 231 | "gh": function.GithubUserLink, 232 | "ghs": function.GitHubUsersLink, 233 | "ghEmoji": function.GitHubEmojiLink, 234 | "link": function.Link, 235 | "linkOrEmpty": function.LinkOrEmpty, 236 | "twitterLink": function.TwitterLink, 237 | "youTubeLink": function.YouTubeLink, 238 | "gstatic": function.GStatic, 239 | "ghID": function.GetIDFromGHLink, 240 | "printGHTable": function.PrintUserAsTable, 241 | } 242 | } 243 | 244 | func sortBy(items []map[string]interface{}, sortBy string, descending bool) { 245 | sort.SliceStable(items, func(i, j int) (compare bool) { 246 | left, ok := items[i][sortBy].(string) 247 | if !ok { 248 | return false 249 | } 250 | right, ok := items[j][sortBy].(string) 251 | if !ok { 252 | return false 253 | } 254 | 255 | compare = strings.Compare(left, right) < 0 256 | if !descending { 257 | compare = !compare 258 | } 259 | return 260 | }) 261 | } 262 | 263 | func generateTOC(txt string) (toc string) { 264 | items := strings.Split(txt, "\n") 265 | for i := range items { 266 | item := items[i] 267 | 268 | var prefix string 269 | var tag string 270 | if strings.HasPrefix(item, "## ") { 271 | tag = strings.TrimPrefix(item, "## ") 272 | prefix = "- " 273 | } else if strings.HasPrefix(item, "### ") { 274 | tag = strings.TrimPrefix(item, "### ") 275 | prefix = " - " 276 | } else { 277 | continue 278 | } 279 | 280 | // not support those titles which have whitespaces 281 | tag = strings.TrimSpace(tag) 282 | if len(strings.Split(tag, " ")) > 1 { 283 | continue 284 | } 285 | 286 | toc = toc + fmt.Sprintf("%s[%s](#%s)\n", prefix, tag, strings.ToLower(tag)) 287 | } 288 | return 289 | } 290 | 291 | func printStarHistory(owner, repo string) string { 292 | return fmt.Sprintf(`[![Star History Chart](https://api.star-history.com/svg?repos=%[1]s/%[2]s&type=Date)](https://star-history.com/#%[1]s/%[2]s&Date)`, 293 | owner, repo) 294 | } 295 | 296 | func dataRender(data interface{}) string { 297 | switch val := data.(type) { 298 | case bool: 299 | if val { 300 | return ":white_check_mark:" 301 | } else { 302 | return ":x:" 303 | } 304 | case string: 305 | return val 306 | } 307 | return "" 308 | } 309 | 310 | func newRootCommand() (cmd *cobra.Command) { 311 | opt := &option{} 312 | cmd = &cobra.Command{ 313 | Use: "yaml-readme", 314 | Short: "A helper to generate a README file from Golang-based template", 315 | Long: `A helper to generate a README file from Golang-based template 316 | Some functions rely on the GitHub API, in order to avoid X-RateLimit-Limit errors you can set an environment variable: 'GITHUB_TOKEN'`, 317 | RunE: opt.runE, 318 | } 319 | cmd.SetOut(os.Stdout) 320 | flags := cmd.Flags() 321 | flags.StringVarP(&opt.pattern, "pattern", "p", "items/*.yaml", 322 | "The glob pattern with Golang spec to find files") 323 | flags.StringVarP(&opt.templateFile, "template", "t", "README.tpl", 324 | "The template file which should follow Golang template spec") 325 | flags.BoolVarP(&opt.includeHeader, "include-header", "", true, 326 | "Indicate if include a notice header on the top of the README file") 327 | flags.StringVarP(&opt.sortBy, "sort-by", "", "", 328 | "Sort the array data descending by which field, or sort it ascending with the prefix '!'. For example: --sort-by !year") 329 | flags.StringVarP(&opt.groupBy, "group-by", "", "", 330 | "Group the array data by which field") 331 | flags.BoolVarP(&opt.printFunctions, "print-functions", "", false, 332 | "Print all the functions and exit") 333 | flags.BoolVarP(&opt.printVariables, "print-variables", "", false, 334 | "Print all the variables and exit") 335 | return 336 | } 337 | 338 | func main() { 339 | if err := newRootCommand().Execute(); err != nil { 340 | os.Exit(1) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "reflect" 8 | "testing" 9 | ) 10 | import "github.com/stretchr/testify/assert" 11 | 12 | func Test_sortBy(t *testing.T) { 13 | type args struct { 14 | items []map[string]interface{} 15 | sortBy string 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | verify func([]map[string]interface{}, *testing.T) 21 | }{{ 22 | name: "normal", 23 | args: args{ 24 | items: []map[string]interface{}{{ 25 | "name": "b", 26 | }, { 27 | "name": "c", 28 | }, { 29 | "name": "a", 30 | }}, 31 | sortBy: "name", 32 | }, 33 | verify: func(data []map[string]interface{}, t *testing.T) { 34 | assert.Equal(t, map[string]interface{}{ 35 | "name": "a", 36 | }, data[0]) 37 | }, 38 | }, { 39 | name: "number values", 40 | args: args{ 41 | items: []map[string]interface{}{{ 42 | "name": "12", 43 | }, { 44 | "name": "13", 45 | }, { 46 | "name": "11", 47 | }, { 48 | "name": "1", 49 | }}, 50 | sortBy: "name", 51 | }, 52 | verify: func(data []map[string]interface{}, t *testing.T) { 53 | assert.Equal(t, map[string]interface{}{ 54 | "name": "1", 55 | }, data[0]) 56 | }, 57 | }, { 58 | name: "slice values", 59 | args: args{ 60 | items: []map[string]interface{}{{ 61 | "name": []string{}, 62 | }, { 63 | "name": []string{}, 64 | }}, 65 | sortBy: "name", 66 | }, 67 | verify: func(data []map[string]interface{}, t *testing.T) { 68 | assert.Equal(t, map[string]interface{}{ 69 | "name": []string{}, 70 | }, data[0]) 71 | }, 72 | }} 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | sortBy(tt.args.items, tt.args.sortBy, true) 76 | tt.verify(tt.args.items, t) 77 | }) 78 | } 79 | } 80 | 81 | func Test_generateTOC(t *testing.T) { 82 | type args struct { 83 | txt string 84 | } 85 | tests := []struct { 86 | name string 87 | args args 88 | wantToc string 89 | }{{ 90 | name: "simple text", 91 | args: args{ 92 | txt: `## Good`, 93 | }, 94 | wantToc: `- [Good](#good) 95 | `, 96 | }, { 97 | name: "multiple levels of the titles", 98 | args: args{ 99 | txt: `## Good 100 | content 101 | ### Better`, 102 | }, 103 | wantToc: `- [Good](#good) 104 | - [Better](#better) 105 | `, 106 | }, { 107 | name: "has whitespace between title", 108 | args: args{ 109 | txt: `## Good 110 | ## This is good`, 111 | }, 112 | wantToc: `- [Good](#good) 113 | `, 114 | }} 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | assert.Equalf(t, tt.wantToc, generateTOC(tt.args.txt), "generateTOC(%v)", tt.args.txt) 118 | }) 119 | } 120 | } 121 | 122 | func Test_printStarHistory(t *testing.T) { 123 | type args struct { 124 | owner string 125 | repo string 126 | } 127 | tests := []struct { 128 | name string 129 | args args 130 | want string 131 | }{{ 132 | name: "simple", 133 | args: args{ 134 | owner: "linuxsuren", 135 | repo: "yaml-readme", 136 | }, 137 | want: `[![Star History Chart](https://api.star-history.com/svg?repos=linuxsuren/yaml-readme&type=Date)](https://star-history.com/#linuxsuren/yaml-readme&Date)`, 138 | }} 139 | for _, tt := range tests { 140 | t.Run(tt.name, func(t *testing.T) { 141 | assert.Equalf(t, tt.want, printStarHistory(tt.args.owner, tt.args.repo), "printStarHistory(%v, %v)", tt.args.owner, tt.args.repo) 142 | }) 143 | } 144 | } 145 | 146 | func Test_getFuncMap(t *testing.T) { 147 | funcMap := getFuncMap("") 148 | assert.NotNil(t, funcMap["printToc"]) 149 | assert.NotNil(t, funcMap["printHelp"]) 150 | assert.NotNil(t, funcMap["printContributors"]) 151 | assert.NotNil(t, funcMap["printStarHistory"]) 152 | assert.NotNil(t, funcMap["printVisitorCount"]) 153 | 154 | buf := bytes.NewBuffer([]byte{}) 155 | printFunctions(buf) 156 | for k, val := range funcMap { 157 | assert.Contains(t, buf.String(), k) 158 | assert.NotNil(t, val) 159 | 160 | valType := reflect.TypeOf(val) 161 | numOut := valType.NumOut() 162 | assert.True(t, numOut > 0 && numOut < 3) 163 | 164 | params := make([]reflect.Value, valType.NumIn()) 165 | for i := 0; i < valType.NumIn(); i++ { 166 | switch valType.In(i).Kind() { 167 | case reflect.Int: 168 | params[i] = reflect.ValueOf(1) 169 | case reflect.Bool: 170 | params[i] = reflect.ValueOf(true) 171 | case reflect.String: 172 | fallthrough 173 | default: 174 | params[i] = reflect.ValueOf("") 175 | } 176 | } 177 | 178 | reflect.ValueOf(val).Call(params) 179 | 180 | assert.Equal(t, reflect.String, valType.Out(0).Kind()) 181 | if numOut == 2 { 182 | assert.Equal(t, reflect.Interface, valType.Out(1).Kind()) 183 | } 184 | } 185 | } 186 | 187 | func Test_dataRender(t *testing.T) { 188 | type args struct { 189 | data interface{} 190 | } 191 | tests := []struct { 192 | name string 193 | args args 194 | want string 195 | }{{ 196 | name: "bool type with true value", 197 | args: args{ 198 | data: true, 199 | }, 200 | want: ":white_check_mark:", 201 | }, { 202 | name: "bool type with false value", 203 | args: args{ 204 | data: false, 205 | }, 206 | want: ":x:", 207 | }, { 208 | name: "normal string value fake", 209 | args: args{ 210 | data: "fake", 211 | }, 212 | want: "fake", 213 | }, { 214 | name: "struct parameter", 215 | args: args{ 216 | data: struct { 217 | }{}, 218 | }, 219 | want: "", 220 | }} 221 | for _, tt := range tests { 222 | t.Run(tt.name, func(t *testing.T) { 223 | assert.Equalf(t, tt.want, dataRender(tt.args.data), "dataRender(%v)", tt.args.data) 224 | }) 225 | } 226 | } 227 | 228 | func Test_renderTemplateToString(t *testing.T) { 229 | type args struct { 230 | tplContent string 231 | object interface{} 232 | } 233 | tests := []struct { 234 | name string 235 | args args 236 | wantOutput string 237 | wantErr assert.ErrorAssertionFunc 238 | }{{ 239 | name: "built-in function", 240 | args: args{ 241 | tplContent: `{{render true}}`, 242 | }, 243 | wantOutput: ":white_check_mark:", 244 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 245 | return false 246 | }, 247 | }, { 248 | name: "Sprig function", 249 | args: args{ 250 | tplContent: `{{ "hello!" | upper | repeat 5 }}`, 251 | }, 252 | wantOutput: "HELLO!HELLO!HELLO!HELLO!HELLO!", 253 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 254 | return false 255 | }, 256 | }} 257 | for _, tt := range tests { 258 | t.Run(tt.name, func(t *testing.T) { 259 | gotOutput, err := renderTemplateToString(tt.args.tplContent, tt.args.object) 260 | if !tt.wantErr(t, err, fmt.Sprintf("renderTemplateToString(%v, %v)", tt.args.tplContent, tt.args.object)) { 261 | return 262 | } 263 | assert.Equalf(t, tt.wantOutput, gotOutput, "renderTemplateToString(%v, %v)", tt.args.tplContent, tt.args.object) 264 | }) 265 | } 266 | } 267 | 268 | func Test_newRootCommand(t *testing.T) { 269 | cmd := newRootCommand() 270 | flags := []string{"pattern", "template", "include-header", "sort-by", "group-by", "print-functions", "print-variables"} 271 | for _, flag := range flags { 272 | assert.NotNil(t, cmd.Flag(flag)) 273 | } 274 | } 275 | 276 | func TestCommand(t *testing.T) { 277 | tests := []struct { 278 | name string 279 | flags []string 280 | hasError bool 281 | expectOutput string 282 | }{{ 283 | name: "print variables", 284 | flags: []string{"--print-variables"}, 285 | hasError: false, 286 | expectOutput: `filename 287 | parentname 288 | fullpath`, 289 | }, { 290 | name: "print functions", 291 | flags: []string{"--print-functions"}, 292 | hasError: false, 293 | expectOutput: `gh 294 | ghEmoji 295 | ghID 296 | ghs 297 | gstatic 298 | link 299 | linkOrEmpty 300 | printContributors 301 | printGHTable 302 | printHelp 303 | printPages 304 | printStarHistory 305 | printToc 306 | printVisitorCount 307 | render 308 | twitterLink 309 | youTubeLink`, 310 | }, { 311 | name: "normal case", 312 | flags: []string{"--template", "function/data/README.tpl", "--pattern", "function/data/*.yaml", "--sort-by", "zh"}, 313 | hasError: false, 314 | expectOutput: `> This file was generated by [README.tpl](README.tpl) via [yaml-readme](https://github.com/LinuxSuRen/yaml-readme), please don't edit it directly! 315 | 316 | |中文名称|英文名称|JD| 317 | |---|---|---| 318 | |zh|en|jd| 319 | |zh|en|jd|`, 320 | }, { 321 | name: "invalid group feature template with group-by flag", 322 | flags: []string{"--template", "function/data/README.tpl", "--pattern", "function/data/*.yaml", "--group-by", "year"}, 323 | hasError: true, 324 | }, { 325 | name: "valid group feature template", 326 | flags: []string{"--template", "function/data/README-group.tpl", "--pattern", "function/data/*.yaml", "--group-by", "year", "--include-header=false"}, 327 | hasError: false, 328 | expectOutput: ` 329 | Year: 2021 330 | | Zh | En | 331 | |---|---| 332 | | zh | en | 333 | 334 | Year: 2022 335 | | Zh | En | 336 | |---|---| 337 | | zh | en | 338 | `, 339 | }, { 340 | name: "group by a string", 341 | flags: []string{"--template", "function/data/README-group.tpl", "--pattern", "function/data/*.yaml", "--group-by", "zh", "--include-header=false"}, 342 | hasError: false, 343 | expectOutput: ` 344 | Year: zh 345 | | Zh | En | 346 | |---|---| 347 | | zh | en | 348 | | zh | en | 349 | `, 350 | }} 351 | for _, tt := range tests { 352 | t.Run(tt.name, func(t *testing.T) { 353 | cmd := newRootCommand() 354 | buf := bytes.NewBuffer([]byte{}) 355 | cmd.SetOut(buf) 356 | cmd.SetArgs(tt.flags) 357 | 358 | err := cmd.Execute() 359 | if tt.hasError { 360 | assert.NotNil(t, err) 361 | } else { 362 | assert.Nil(t, err) 363 | 364 | assert.Equal(t, tt.expectOutput, buf.String()) 365 | } 366 | }) 367 | } 368 | } 369 | 370 | func Test_sortMetadata(t *testing.T) { 371 | type args struct { 372 | items []map[string]interface{} 373 | sortByField string 374 | } 375 | tests := []struct { 376 | name string 377 | args args 378 | verify func(t *testing.T, items []map[string]interface{}) 379 | }{{ 380 | name: "normal case", 381 | args: args{ 382 | items: []map[string]interface{}{{ 383 | "name": "c", 384 | }, { 385 | "name": "b", 386 | }, { 387 | "name": "a", 388 | }}, 389 | sortByField: "name", 390 | }, 391 | verify: func(t *testing.T, items []map[string]interface{}) { 392 | assert.Equal(t, map[string]interface{}{"name": "a"}, items[0]) 393 | assert.Equal(t, map[string]interface{}{"name": "b"}, items[1]) 394 | assert.Equal(t, map[string]interface{}{"name": "c"}, items[2]) 395 | }, 396 | }, { 397 | name: "sort with descending", 398 | args: args{ 399 | items: []map[string]interface{}{{ 400 | "name": "c", 401 | }, { 402 | "name": "b", 403 | }, { 404 | "name": "a", 405 | }}, 406 | sortByField: "!name", 407 | }, 408 | verify: func(t *testing.T, items []map[string]interface{}) { 409 | assert.Equal(t, map[string]interface{}{"name": "c"}, items[0]) 410 | assert.Equal(t, map[string]interface{}{"name": "b"}, items[1]) 411 | assert.Equal(t, map[string]interface{}{"name": "a"}, items[2]) 412 | }, 413 | }} 414 | for _, tt := range tests { 415 | t.Run(tt.name, func(t *testing.T) { 416 | sortMetadata(tt.args.items, tt.args.sortByField) 417 | }) 418 | } 419 | } 420 | 421 | func Test_loadTemplate(t *testing.T) { 422 | type args struct { 423 | templateFile string 424 | includeHeader bool 425 | } 426 | tests := []struct { 427 | name string 428 | args args 429 | wantReadmeTpl func() string 430 | wantErr assert.ErrorAssertionFunc 431 | }{{ 432 | name: "normal case", 433 | args: args{ 434 | templateFile: "function/data/README.tpl", 435 | includeHeader: false, 436 | }, 437 | wantReadmeTpl: func() string { 438 | data, _ := ioutil.ReadFile("function/data/README.tpl") 439 | return string(data) 440 | }, 441 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 442 | assert.Nil(t, err) 443 | return true 444 | }, 445 | }, { 446 | name: "fake file", 447 | args: args{ 448 | templateFile: "fake", 449 | includeHeader: false, 450 | }, 451 | wantReadmeTpl: func() string { 452 | data, _ := ioutil.ReadFile("function/data/README.tpl") 453 | return string(data) 454 | }, 455 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 456 | assert.Nil(t, err) 457 | return true 458 | }, 459 | }, { 460 | name: "include header", 461 | args: args{ 462 | templateFile: "function/data/README.tpl", 463 | includeHeader: true, 464 | }, 465 | wantReadmeTpl: func() string { 466 | data, _ := ioutil.ReadFile("function/data/README.tpl") 467 | return `> This file was generated by [README.tpl](README.tpl) via [yaml-readme](https://github.com/LinuxSuRen/yaml-readme), please don't edit it directly! 468 | 469 | ` + string(data) 470 | }, 471 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 472 | assert.Nil(t, err) 473 | return true 474 | }, 475 | }, { 476 | name: "has metadata", 477 | args: args{ 478 | templateFile: "function/data/README-with-metadata.tpl", 479 | includeHeader: false, 480 | }, 481 | wantReadmeTpl: func() string { 482 | return `a fake template` 483 | }, 484 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 485 | assert.Nil(t, err) 486 | return true 487 | }, 488 | }} 489 | for _, tt := range tests { 490 | t.Run(tt.name, func(t *testing.T) { 491 | gotReadmeTpl, err := loadTemplate(tt.args.templateFile, tt.args.includeHeader) 492 | if !tt.wantErr(t, err, fmt.Sprintf("loadTemplate(%v, %v)", tt.args.templateFile, tt.args.includeHeader)) { 493 | return 494 | } 495 | assert.Equalf(t, tt.wantReadmeTpl(), gotReadmeTpl, "loadTemplate(%v, %v)", tt.args.templateFile, tt.args.includeHeader) 496 | }) 497 | } 498 | } 499 | 500 | func Test_loadMetadata(t *testing.T) { 501 | type args struct { 502 | pattern string 503 | groupBy string 504 | } 505 | tests := []struct { 506 | name string 507 | args args 508 | wantItems []map[string]interface{} 509 | wantGroupData map[string][]map[string]interface{} 510 | wantErr assert.ErrorAssertionFunc 511 | }{{ 512 | name: "normal case", 513 | args: args{ 514 | pattern: "function/data/*.yaml", 515 | groupBy: "year", 516 | }, 517 | wantItems: []map[string]interface{}{{ 518 | "en": "en", "filename": "item-2022", "fullpath": "function/data/item-2022.yaml", "jd": "jd", "parentname": "data", "zh": "zh", "year": 2022, 519 | }, { 520 | "en": "en", "filename": "item", "fullpath": "function/data/item.yaml", "jd": "jd", "parentname": "data", "zh": "zh", "year": 2021, 521 | }}, 522 | wantGroupData: map[string][]map[string]interface{}{ 523 | "2021": {{ 524 | "en": "en", "filename": "item", "fullpath": "function/data/item.yaml", "jd": "jd", "parentname": "data", "zh": "zh", "year": 2021, 525 | }}, 526 | "2022": {{ 527 | "en": "en", "filename": "item-2022", "fullpath": "function/data/item-2022.yaml", "jd": "jd", "parentname": "data", "zh": "zh", "year": 2022, 528 | }}, 529 | }, 530 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 531 | assert.Nil(t, err) 532 | return true 533 | }, 534 | }} 535 | for _, tt := range tests { 536 | t.Run(tt.name, func(t *testing.T) { 537 | gotItems, gotGroupData, err := loadMetadata(tt.args.pattern, tt.args.groupBy) 538 | if !tt.wantErr(t, err, fmt.Sprintf("loadMetadata(%v, %v)", tt.args.pattern, tt.args.groupBy)) { 539 | return 540 | } 541 | assert.Equalf(t, tt.wantItems, gotItems, "loadMetadata(%v, %v)", tt.args.pattern, tt.args.groupBy) 542 | assert.Equalf(t, tt.wantGroupData, gotGroupData, "loadMetadata(%v, %v)", tt.args.pattern, tt.args.groupBy) 543 | }) 544 | } 545 | } 546 | 547 | func Test_printVariables(t *testing.T) { 548 | tests := []struct { 549 | name string 550 | wantStdout string 551 | }{{ 552 | name: "normal case", 553 | wantStdout: `filename 554 | parentname 555 | fullpath`, 556 | }} 557 | for _, tt := range tests { 558 | t.Run(tt.name, func(t *testing.T) { 559 | stdout := &bytes.Buffer{} 560 | printVariables(stdout) 561 | assert.Equalf(t, tt.wantStdout, stdout.String(), "printVariables(%v)", stdout) 562 | }) 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/jsconfig.json 8 | **/*.map 9 | **/.eslintrc.json 10 | node_modules 11 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "yaml-readme" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/README.md: -------------------------------------------------------------------------------- 1 | This plugin could help you to maintain a complex README file. 2 | 3 | ## Prerequisite 4 | 5 | Before get started, please install `yaml-readme` to you system. You can use [hd](https://github.com/LinuxSuRen/http-downloader/) or others. 6 | 7 | ```shell 8 | hd i yaml-readme 9 | ``` 10 | 11 | ## Get started 12 | Put the following code in the first line of the [Go template](https://pkg.go.dev/text/template) file: 13 | 14 | ``` 15 | #!yaml-readme -p 'data/financing/*.yaml' --output financing.md 16 | ``` 17 | 18 | See also [this example file](https://github.com/LinuxSuRen/open-source-best-practice/blob/master/data/financing/financing.tpl). 19 | 20 | then press `Ctrl+Shift+P` and type `yaml-readme` command to generate the Markdown file specific with `--output`. 21 | 22 | ## Publish 23 | Please see the following steps to publish this plugin: 24 | 25 | * Login with `vsce login linuxsuren` 26 | * Package with `vsce package` 27 | * Publish with `vsce publish` 28 | 29 | See also [the details](https://code.visualstudio.com/api/working-with-extensions/publishing-extension). 30 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/command.js: -------------------------------------------------------------------------------- 1 | function generateCommand(metadata, wf, filename) { 2 | metadata = metadata.replace("#!yaml-readme ", "") 3 | 4 | let commands = ["yaml-readme", "-t", filename] 5 | let output = "" 6 | const items = metadata.split(" ") 7 | 8 | for (var i = 0; i < items.length; i++) { 9 | const item = items[i] 10 | if (item == "-p") { 11 | commands.push("-p", wf + "/" + items[++i]) 12 | } else if (item == "--output") { 13 | output = wf + "/" + items[++i] 14 | } else if (item == "--group-by") { 15 | commands.push("--group-by", items[++i]) 16 | } else if (item == "--sort-by") { 17 | commands.push("--sort-by", items[++i]) 18 | } 19 | } 20 | 21 | return [commands.join(" "), output] 22 | } 23 | 24 | module.exports = { 25 | generateCommand 26 | } 27 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/extension.js: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | const vscode = require('vscode'); 4 | const cp = require('child_process'); 5 | const fs = require('fs'); 6 | const cmd = require('./command'); 7 | 8 | const grpc = require('@grpc/grpc-js'); 9 | const protoLoader = require('@grpc/proto-loader'); 10 | const PROTO_PATH = __dirname +'/server.proto'; 11 | const packageDefinition = protoLoader.loadSync( 12 | PROTO_PATH, { 13 | keepCase: true, 14 | longs: String, 15 | enums: String, 16 | defaults: true, 17 | oneofs: true, 18 | }); 19 | 20 | const serverProto = grpc.loadPackageDefinition(packageDefinition).server; 21 | 22 | const apiConsole = vscode.window.createOutputChannel("API Testing") 23 | 24 | function getFirstLine(filePath) { 25 | const data = fs.readFileSync(filePath); 26 | return data.toString().split('\n')[0]; 27 | } 28 | 29 | // this method is called when your extension is activated 30 | // your extension is activated the very first time the command is executed 31 | 32 | /** 33 | * @param {vscode.ExtensionContext} context 34 | */ 35 | function activate(context) { 36 | 37 | // Use the console to output diagnostic information (console.log) and errors (console.error) 38 | // This line of code will only be executed once when your extension is activated 39 | console.log('Congratulations, your extension "yaml-readme" is now active!'); 40 | 41 | // The command has been defined in the package.json file 42 | // Now provide the implementation of the command with registerCommand 43 | // The commandId parameter must match the command field in package.json 44 | let disposable = vscode.commands.registerCommand('yaml-readme.helloWorld', function () { 45 | if(vscode.workspace.workspaceFolders !== undefined) { 46 | let wf = vscode.workspace.workspaceFolders[0].uri.path ; 47 | 48 | let filename = vscode.window.activeTextEditor.document.fileName 49 | let metadata = getFirstLine(filename) 50 | // vscode.window.showInformationMessage(metadata + "===" + metadata.startsWith("#!yaml-readme")); 51 | if (metadata.startsWith("#!yaml-readme")) { 52 | let command = cmd.generateCommand(metadata, wf, filename) 53 | 54 | // vscode.window.showInformationMessage(`yaml-readme -p "${pattern}" -t "${filename}" > ${output}`) 55 | cp.exec(`${command[0]} > ${command[1]}`, (err) => { 56 | if (err) { 57 | console.log('error: ' + err); 58 | } 59 | vscode.commands.executeCommand("markdown.showPreviewToSide", vscode.Uri.file(`${command[1]}`)); 60 | }); 61 | } 62 | } else { 63 | let message = "YOUR-EXTENSION: Working folder not found, open a folder an try again" ; 64 | 65 | vscode.window.showErrorMessage(message); 66 | } 67 | }); 68 | let atest = vscode.commands.registerCommand('atest', function() { 69 | if(vscode.workspace.workspaceFolders !== undefined) { 70 | let filename = vscode.window.activeTextEditor.document.fileName 71 | const addr = vscode.workspace.getConfiguration().get('yaml-readme.server') 72 | apiConsole.show() 73 | let task 74 | 75 | let editor = vscode.window.activeTextEditor 76 | if (editor) { 77 | let selection = editor.selection 78 | let text = editor.document.getText(selection) 79 | if (text !== undefined && text !== '') { 80 | task = text 81 | } 82 | } 83 | 84 | if (task === undefined || task === '') { 85 | const data = fs.readFileSync(filename); 86 | task = data.toString() 87 | } 88 | 89 | const client = new serverProto.Runner(addr, grpc.credentials.createInsecure()); 90 | client.run({ 91 | kind: "suite", 92 | data: task 93 | } , function(err, response) { 94 | if (err !== undefined && err !== null) { 95 | apiConsole.appendLine(err); 96 | } else { 97 | apiConsole.appendLine(response.message); 98 | } 99 | }); 100 | } else { 101 | let message = "YOUR-EXTENSION: Working folder not found, open a folder an try again" ; 102 | 103 | vscode.window.showErrorMessage(message); 104 | } 105 | }) 106 | 107 | context.subscriptions.push(disposable,atest); 108 | } 109 | 110 | // this method is called when your extension is deactivated 111 | function deactivate() {} 112 | 113 | module.exports = { 114 | activate, 115 | deactivate 116 | } 117 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "ES2020" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yaml-readme", 3 | "displayName": "yaml-readme", 4 | "description": "A helper to generate the READE file automatically.", 5 | "version": "0.0.9", 6 | "repository": "https://github.com/linuxsuren/yaml-readme", 7 | "engines": { 8 | "vscode": "^1.68.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "publisher": "linuxsuren", 14 | "activationEvents": [ 15 | "onCommand:yaml-readme.helloWorld", 16 | "atest" 17 | ], 18 | "main": "./dist/extension.js", 19 | "contributes": { 20 | "commands": [ 21 | { 22 | "command": "yaml-readme.helloWorld", 23 | "title": "yaml-readme" 24 | }, 25 | { 26 | "command": "atest", 27 | "title": "API Testing", 28 | "description": "API Testing." 29 | } 30 | ], 31 | "configuration": { 32 | "title": "API Testing", 33 | "properties": { 34 | "yaml-readme.server": { 35 | "type": "string", 36 | "default": "http://localhost:9090", 37 | "description": "server" 38 | } 39 | } 40 | }, 41 | "editor/context": [ 42 | { 43 | "when": "editorTextFocus && config.markdown.editorContextMenuCommands.testPackage && resourceLangId == markdown", 44 | "command": "atest", 45 | "group": "Go group 1" 46 | } 47 | ], 48 | "markdown.editorContextMenuCommands": { 49 | "type": "object", 50 | "properties": { 51 | "testPackage": { 52 | "type": "boolean", 53 | "default": true, 54 | "description": "If true, adds command to run all tests in the current package to the editor context menu" 55 | } 56 | }, 57 | "default": { 58 | "testPackage": false 59 | }, 60 | "description": "Experimental Feature: Enable/Disable entries from the context menu in the editor.", 61 | "scope": "resource" 62 | }, 63 | "menus": { 64 | "explorer/context": [ 65 | { 66 | "when": "resourceLangId == yaml", 67 | "group": "navigation", 68 | "command": "atest" 69 | } 70 | ], 71 | "editor/context": [ 72 | { 73 | "when": "resourceLangId == yaml", 74 | "group": "navigation", 75 | "command": "atest" 76 | } 77 | ], 78 | "testing/item/context": [ 79 | { 80 | "group": "navigation", 81 | "command": "atest" 82 | } 83 | ], 84 | "testing/item/gutter": [ 85 | { 86 | "group": "navigation", 87 | "command": "atest" 88 | } 89 | ] 90 | } 91 | }, 92 | "scripts": { 93 | "clean": "rm -rf ./dist/* && rm *.vsix", 94 | "package": "vsce package", 95 | "vscode:prepublish": "npm run compile", 96 | "bundle": "esbuild extension.js --bundle --outdir=dist --external:vscode --format=cjs --platform=node && cp server.proto dist", 97 | "lint": "eslint .", 98 | "pretest": "npm run lint", 99 | "test": "node ./test/runTest.js", 100 | "compile": "npm run bundle", 101 | "deploy": "vsce package && vsce publish" 102 | }, 103 | "devDependencies": { 104 | "@types/glob": "^7.2.0", 105 | "@types/mocha": "^9.1.1", 106 | "@types/node": "16.x", 107 | "@types/vscode": "^1.68.0", 108 | "@vscode/test-electron": "^2.1.3", 109 | "async": "~1.5.2", 110 | "eslint": "^8.16.0", 111 | "glob": "^8.0.3", 112 | "mocha": "^10.0.0", 113 | "typescript": "^4.7.2", 114 | "esbuild": "0.17.10", 115 | "webpack": "~4.43.0", 116 | "webpack-cli": "~3.3.11" 117 | }, 118 | "dependencies": { 119 | "@grpc/proto-loader": "~0.5.4", 120 | "lodash": "~4.17.0", 121 | "google-protobuf": "~3.14.0", 122 | "@grpc/grpc-js": "~1.0.5", 123 | "q": "^1.5.1", 124 | "remark": "^14.0.2", 125 | "remark-admonitions": "^1.2.1", 126 | "remark-html": "^15.0.1" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/server.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/linuxsuren/api-testing/pkg/server"; 4 | 5 | package server; 6 | 7 | service Runner { 8 | rpc Run (TestTask) returns (HelloReply) {} 9 | } 10 | 11 | message TestTask { 12 | string data = 1; 13 | string kind = 2; 14 | } 15 | 16 | message HelloReply { 17 | string message = 1; 18 | } -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { runTests } = require('@vscode/test-electron'); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | // const vscode = require('vscode'); 6 | const myExtension = require('../../command'); 7 | 8 | let cmd = myExtension.generateCommand("#!yaml-readme -p data/*.yaml --output README.md --group-by kind --sort-by kind","wf","filename") 9 | assert.equal(cmd[0], "yaml-readme -t filename -p wf/data/*.yaml --group-by kind --sort-by kind") 10 | assert.equal(cmd[1], "wf/README.md") 11 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | module.exports = { 41 | run 42 | }; 43 | -------------------------------------------------------------------------------- /plugins/vscode/yaml-readme/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `extension.js` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `extension.js` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `extension.js`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.js` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | ## Go further 37 | 38 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 39 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 40 | --------------------------------------------------------------------------------