├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── go-binary-release.yml │ ├── go-build-test.yml │ └── golangci-lint.yml ├── .golangci.yaml ├── README.md ├── go.mod ├── images └── logo.png ├── main.go ├── main_test.go └── testdata ├── 0-okr └── 2023年.md ├── README.md └── test.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告缺陷 Bug Report 3 | about: 报告缺陷以帮助我们改进 Report defects to help us improve 4 | --- 5 | 6 | ### 描述问题 Describe the problem 7 | 8 | 12 | 13 | ````````markdown 14 | 如果是解析渲染问题的话请在此处贴入 Markdown 原文 15 | If it is a problem of parsing and rendering, please post the original Markdown here 16 | ```````` 17 | 18 | ### 期待的结果 Expected result 19 | 20 | 24 | 25 | ### 截屏或录像 Screenshot or video 26 | 27 | 34 | 35 | ### 版本环境 Version environment 36 | 37 | * 版本 Version: 38 | * 操作系统 Operating system: 39 | * 浏览器(如果使用)Browser (if used): 40 | 41 | ### 其他信息 Other information 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Jarvan的博客 4 | url: https://jarvans.com 5 | about: 大厂程序员, 公众号 硬核的Jarvan 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 请求新功能 Request new features 3 | about: 提出你期待的功能特性 Come up with the features you expect 4 | --- 5 | 6 | ### 你在什么场景下需要该功能? In what scenarios do you need this function? 7 | 8 | 12 | 13 | ### 描述最优的解决方案 Describe the optimal solution 14 | 15 | 19 | 20 | ### 描述候选解决方案 Describe the candidate solution 21 | 22 | 26 | 27 | ### 其他信息 Other information 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/go-binary-release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.17 25 | - 26 | name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v2 28 | with: 29 | # either 'goreleaser' (default) or 'goreleaser-pro' 30 | distribution: goreleaser 31 | version: latest 32 | args: release --rm-dist 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/go-build-test.yml: -------------------------------------------------------------------------------- 1 | name: Go package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: '1.15' 16 | 17 | - name: Build 18 | run: go build -v ./... 19 | 20 | - name: Test 21 | run: go test -json > TestResults-${{ matrix.go-version }}.json 22 | - name: Upload Go test results 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: Go-results-${{ matrix.go-version }} 26 | path: TestResults-${{ matrix.go-version }}.json 27 | retention-days: 5 -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.21' 23 | cache: false 24 | - name: golangci-lint 25 | uses: golangci/golangci-lint-action@v3 26 | with: 27 | # Require: The version of golangci-lint to use. 28 | # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. 29 | # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. 30 | version: v1.54 31 | 32 | # Optional: working directory, useful for monorepos 33 | # working-directory: somedir 34 | 35 | # Optional: golangci-lint command line arguments. 36 | # 37 | # Note: By default, the `.golangci.yml` file should be at the root of the repository. 38 | # The location of the configuration file can be changed by using `--config=` 39 | # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 40 | 41 | # Optional: show only new issues if it's a pull request. The default value is `false`. 42 | # only-new-issues: true 43 | 44 | # Optional: if set to true, then all caching functionality will be completely disabled, 45 | # takes precedence over all other caching options. 46 | # skip-cache: true 47 | 48 | # Optional: if set to true, then the action won't cache or restore ~/go/pkg. 49 | # skip-pkg-cache: true 50 | 51 | # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. 52 | # skip-build-cache: true 53 | 54 | # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. 55 | # install-mode: "goinstall" -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Lingfei Kong . All rights reserved. 2 | # Use of this source code is governed by a MIT style 3 | # license that can be found in the LICENSE file. 4 | 5 | # This file contains all available configuration options 6 | # with their default values. 7 | 8 | # options for analysis running 9 | run: 10 | # default concurrency is a available CPU number 11 | concurrency: 4 12 | 13 | # timeout for analysis, e.g. 30s, 5m, default is 1m 14 | timeout: 5m 15 | 16 | # exit code when at least one issue was found, default is 1 17 | issues-exit-code: 1 18 | 19 | # include test files or not, default is true 20 | tests: true 21 | 22 | # list of build tags, all linters use it. Default is empty list. 23 | build-tags: 24 | - mytag 25 | 26 | # which dirs to skip: issues from them won't be reported; 27 | # can use regexp here: generated.*, regexp is applied on full path; 28 | # default value is empty list, but default dirs are skipped independently 29 | # from this option's value (see skip-dirs-use-default). 30 | # "/" will be replaced by current OS file path separator to properly work 31 | # on Windows. 32 | skip-dirs: 33 | - test # 测试目录 34 | - tools # 工具目录 35 | 36 | # default is true. Enables skipping of directories: 37 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 38 | skip-dirs-use-default: true 39 | 40 | # which files to skip: they will be analyzed, but issues from them 41 | # won't be reported. Default value is empty list, but there is 42 | # no need to include all autogenerated files, we confidently recognize 43 | # autogenerated files. If it's not please let us know. 44 | # "/" will be replaced by current OS file path separator to properly work 45 | # on Windows. 46 | skip-files: 47 | - ".*\\.my\\.go$" 48 | - _test.go 49 | 50 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 51 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 52 | # automatic updating of go.mod described above. Instead, it fails when any changes 53 | # to go.mod are needed. This setting is most useful to check that go.mod does 54 | # not need updates, such as in a continuous integration and testing system. 55 | # If invoked with -mod=vendor, the go command assumes that the vendor 56 | # directory holds the correct copies of dependencies and ignores 57 | # the dependency descriptions in go.mod. 58 | #modules-download-mode: release|readonly|vendor 59 | 60 | # Allow multiple parallel golangci-lint instances running. 61 | # If false (default) - golangci-lint acquires file lock on start. 62 | allow-parallel-runners: true 63 | 64 | 65 | # output configuration options 66 | output: 67 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 68 | format: colored-line-number 69 | 70 | # print lines of code with issue, default is true 71 | print-issued-lines: true 72 | 73 | # print linter name in the end of issue text, default is true 74 | print-linter-name: true 75 | 76 | # make issues output unique by line, default is true 77 | uniq-by-line: true 78 | 79 | # add a prefix to the output file references; default is no prefix 80 | path-prefix: "" 81 | 82 | # sorts results by: filepath, line and column 83 | sort-results: true 84 | 85 | # all available settings of specific linters 86 | linters-settings: 87 | gocyclo: 88 | # Minimal code complexity to report. 89 | # Default: 30 (but we recommend 10-20) 90 | min-complexity: 10 91 | linters: 92 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 93 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 94 | # enable-all: true 95 | disable-all: true 96 | enable: 97 | - typecheck # 类型检查 98 | - bodyclose # body是否关闭 99 | - durationcheck # duration相乘检查 100 | - errcheck # 错误检查 101 | - exportloopref # 循环指针检查 102 | # - gofmt # gofmt 103 | # - gofumpt # gofumpt 104 | # - goimports # 导包顺序检查 105 | # - gosec # go安全检查 106 | - gosimple # 简化代码检查 107 | - govet 108 | - ineffassign # 变量是否未被使用 109 | - makezero # make非0长度切片 110 | - nilerr 111 | #- prealloc # 切片预分配 112 | #- revive 冗余代码检查 113 | - staticcheck # 静态检查 114 | - unparam # 无用的参数 115 | - unused # 变量是否使用 116 | - errchkjson # json err是否处理 117 | - gocyclo # 圈复杂度检查 118 | fast: false 119 | 120 | issues: 121 | 122 | # Show only new issues created after git revision `REV` 123 | # new-from-rev: REV 124 | 125 | # Show only new issues created in git patch with set file path. 126 | #new-from-patch: path/to/patch/file 127 | 128 | # Fix found issues (if it's supported by the linter) 129 | fix: false 130 | 131 | 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |

5 | 6 | 7 | 8 |

9 |

10 | 11 | # MarkNum - 自动生成 markdown 标题序号 12 | 13 | 自动添加/更新 markdown 标题序号,支持多级标题。 14 | 15 | ## 1. 示例代码 16 | 17 | 输入 18 | 19 | ```bash 20 | $ marknum -file test.md 21 | [成功] 输出文件: test.md.marknum.md 22 | ``` 23 | 24 | 原始文件 test.md 25 | 26 | ```markdown 27 | # 一级标题 28 | 29 | ## 二级标题 30 | 31 | ### 三级标题 32 | 33 | ## 二级标题 34 | 35 | ### 三级标题 36 | ``` 37 | 38 | 输出文件 test.md.marknum.md 39 | 40 | ```bash 41 | # 一级标题 42 | 43 | ## 1. 二级标题 44 | 45 | ### 1.1. 三级标题 46 | 47 | ## 2. 二级标题 48 | ``` 49 | 50 | ## 2. 安装 51 | 52 | ### 2.1. Go语言安装 53 | 54 | ```bash 55 | go install github.com/jarvanstack/marknum@latest 56 | ``` 57 | 58 | ### 2.2. 可执行文件 59 | 60 | 下载可执行文件: https://github.com/jarvanstack/marknum/releases 61 | 62 | ## 3. 使用 63 | 64 | ```bash 65 | $ marknum -h 66 | Usage of marknum: 67 | -cover 68 | 是否覆盖原文件, 默认为 false, 新建 $filename.marknum.md 文件 69 | -dir string 70 | 深度遍历目录下所有md文件(和 -file 二选一) 71 | -file string 72 | 指定文件 73 | -max int 74 | 最大标题级数, 范围[min,max), 默认为二级,三级,四级标题生成序号 (default 5) 75 | -min int 76 | 最小标题级数, 范围[min,max), 默认为二级,三级,四级标题生成序号 (default 2) 77 | ``` 78 | 79 | ## 4. 常用命令 80 | 81 | ```bash 82 | # 将当前目录下所有 markdown 文件添加/更新序号, 覆盖源文件 83 | marknum -dir ./ -cover ture 84 | ``` 85 | 86 | 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jarvanstack/marknum 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jarvanstack/marknum/a8c7a4e75f3196db93e875910e5b48b81eed7e29/images/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/fs" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | var file = flag.String("file", "", "指定文件") 17 | var dir = flag.String("dir", "", "深度遍历目录下所有md文件(和 -file 二选一)") 18 | var cover = flag.Bool("cover", false, "是否覆盖原文件, 默认为 false, 新建 $filename.marknum.md 文件") 19 | var min = flag.Int("min", 2, "最小标题级数, 范围[min,max), 默认为二级,三级,四级标题生成序号") 20 | var max = flag.Int("max", 5, "最大标题级数, 范围[min,max), 默认为二级,三级,四级标题生成序号") 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | if *file == "" && *dir == "" { 26 | fmt.Printf("Help:\n %s -h \n", os.Args[0]) 27 | fmt.Printf("Example: \n marknum -dir ./ -cover \n") 28 | os.Exit(1) 29 | } 30 | 31 | if *file != "" { 32 | oneFile(*file) 33 | } 34 | 35 | if *dir != "" { 36 | files := mdPaths(*dir) 37 | for _, filename := range files { 38 | oneFile(filename) 39 | } 40 | } 41 | 42 | } 43 | 44 | // 通过目录获取所有的 md 文件 45 | func mdPaths(dir string) []string { 46 | var files []string 47 | err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 48 | if !d.IsDir() { 49 | if strings.HasSuffix(d.Name(), ".md") { 50 | files = append(files, path) 51 | } 52 | } 53 | return nil 54 | }) 55 | if err != nil { 56 | fmt.Printf("filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error) err:%v \n", err) 57 | os.Exit(1) 58 | } 59 | return files 60 | } 61 | 62 | func oneFile(filename string) { 63 | f, err := os.Open(filename) 64 | if err != nil { 65 | fmt.Printf("os.Open(filename) err:%v \n", err) 66 | os.Exit(1) 67 | } 68 | 69 | s, err := sectionNumber(f) 70 | if err != nil { 71 | fmt.Printf("sectionNumber(f) err:%v \n", err) 72 | os.Exit(1) 73 | } 74 | 75 | var output string 76 | if !*cover { 77 | output = filename + ".marknum.md" 78 | } else { 79 | output = filename 80 | } 81 | 82 | // 写入文件或者覆盖文件 83 | err = os.WriteFile(output, []byte(s), 0644) 84 | if err != nil { 85 | fmt.Printf("os.WriteFile(output, []byte(s), 0644) err:%v \n", err) 86 | os.Exit(1) 87 | } 88 | fmt.Printf("[成功] 输出文件: %s \n", output) 89 | } 90 | 91 | // add/update section numbers 92 | // 一行行读取; 识别代码块; 识别标题 -> 删除标题序号 -> 添加标题序号 -> 写入 93 | func sectionNumber(in io.Reader) (string, error) { 94 | r := bufio.NewReader(in) 95 | buf := bytes.Buffer{} 96 | 97 | // 标题序号 98 | sectionNumbers := make([]int, 6) 99 | // 是否在代码块中 100 | inCodeBlock := false 101 | 102 | // 一行行读取 103 | var finish bool 104 | for !finish { 105 | line, err := r.ReadString('\n') 106 | if err != nil { 107 | if err == io.EOF { 108 | finish = true 109 | } else { 110 | return "", err 111 | } 112 | } 113 | 114 | // 识别代码块 115 | if isCodeBlock(line) { 116 | inCodeBlock = !inCodeBlock 117 | } 118 | 119 | // 不在代码块中 120 | if !inCodeBlock { 121 | // 是标题 122 | if level := headerLevel(line); level != 0 { 123 | // 更新 sectionNumbers 124 | updateSectionNumbers(sectionNumbers, level) 125 | 126 | // 删除标题序号 127 | line = delSectionNumber(line) 128 | 129 | // 添加标题序号 130 | line = addSectionNumber(line, sectionNumbers, level) 131 | } 132 | } 133 | 134 | buf.WriteString(line) 135 | 136 | } 137 | 138 | return buf.String(), nil 139 | } 140 | 141 | // 更新 sectionNumbers 142 | // 如果 level = 1, 一级标题, 可以由一开始 0 到 1; => 添加 sectionNumbers[level-1]++ 即可 143 | // 如果 level = 1, 一级标题, 可以由一开始 2 到 1; => 添加 sectionNumbers[level-1]++ 即可, 并且清理后面的, 将后面的设置为 0 144 | // 所以每次更新新只需要清理后面的就行了 145 | func updateSectionNumbers(sectionNumbers []int, level int) { 146 | sectionNumbers[level-1]++ 147 | 148 | for level < len(sectionNumbers) { 149 | sectionNumbers[level] = 0 150 | level++ 151 | } 152 | } 153 | 154 | // 添加标题序号 155 | // 比如输入 "## 标题" 返回 "## 1. 标题" 156 | func addSectionNumber(line string, sectionNumbers []int, level int) string { 157 | s := sectionNumberStr(sectionNumbers[:level]) 158 | if s != "" { 159 | return fmt.Sprintf("%s %s\n", strings.TrimSpace(s), strings.TrimSpace(line)) 160 | } 161 | return "" 162 | } 163 | 164 | // 删除标题的header和序号 165 | // 比如输入 "## 1. 标题" 返回 "标题" 166 | // 比如输入 "## 1 标题" 返回 "标题" 167 | // 比如输入 "## 1.1 标题" 返回 "标题" 168 | // 比如输入 "## 1.1. 标题" 返回 "标题" 169 | // 比如输入 "## 1.1.1 标题" 返回 "标题" 170 | // 比如输入 "## 1.1.1. 标题" 返回 "标题" 171 | var delSectionNumberRe = regexp.MustCompile(`(\s*#+\s+)([\d\.]*)(\s*)`) 172 | 173 | func delSectionNumber(line string) string { 174 | return delSectionNumberRe.ReplaceAllString(line, "") 175 | } 176 | 177 | // 获取标题级别 178 | func headerLevel(line string) int { 179 | level := 0 180 | for _, ch := range line { 181 | if ch == '#' { 182 | level++ 183 | } else { 184 | break 185 | } 186 | } 187 | return level 188 | } 189 | 190 | func isCodeBlock(line string) bool { 191 | return strings.HasPrefix(line, "```") 192 | } 193 | 194 | func sectionNumberStr(sectionNumbers []int) string { 195 | var buf bytes.Buffer 196 | // 添加 # 197 | for i := 0; i < len(sectionNumbers); i++ { 198 | buf.WriteString("#") 199 | } 200 | 201 | // 空格 202 | buf.WriteString(" ") 203 | 204 | // 例如一级标题和六级标题不需要序号 205 | if len(sectionNumbers) >= *min && len(sectionNumbers) < *max { 206 | for i, n := range sectionNumbers { 207 | // 例如给二级标题编号的时候忽略一级标题 208 | level := i + 1 209 | if level < *min { 210 | continue 211 | } 212 | 213 | buf.WriteString(fmt.Sprintf("%d.", n)) 214 | } 215 | } 216 | 217 | return buf.String() 218 | } 219 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_mdPaths(t *testing.T) { 9 | paths := mdPaths("./testdata") 10 | fmt.Printf("paths=%#v\n", paths) 11 | } 12 | -------------------------------------------------------------------------------- /testdata/0-okr/2023年.md: -------------------------------------------------------------------------------- 1 | 2 | - O1: 高评价的通过试用期 3 | * 熟悉整体服务器框架结构, 梳理服务器整个架构和数据流动 (1/1) 4 | * 熟悉logic服务器代码, 主动 review 他们的代码, 能给出 3 次有用的建议 (0/3) 5 | * 主动沟通, 遇到问题及时抛出, 有问题大胆问, 主动推进进度 (持续) 6 | - O3: 提高思维能力 7 | * 每周思维分享, 提前准备, 提前通知 (持续) 8 | * 3次精华思维分享会, 剪辑为视频发抖音 (0/3) 9 | * 阅读思维类书籍, 记录心得, 分享 20 篇高质量文章 (11/20) 10 | - O4: 提高技术影响力 11 | * 阅读技术类书籍, 分享 20 篇高质量文章 (1/20) 12 | * 完成操作系统体系化文档 (0%) 13 | * 完成计算机网络体系化文档 (0%) 14 | - O5: 赚钱的机会 15 | * 完成 Golang 入门高质量文档, 出书草稿标准 (1%) 16 | * 完成精品课程制作, 上传 B站 (0%) 17 | * 吸引 100 人进粉丝群 (0%) 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | # MarkNum - 自动生成 markdown 标题序号 2 | 3 | 自动添加/更新 markdown 标题序号,支持多级标题。 4 | 5 | ## 安装 6 | 7 | ```bash 8 | 9 | ``` 10 | 11 | ## 使用 12 | 13 | * -file: 指定文件 14 | * -dir: 指定目录(和 -f 二选一) 15 | * -cover: 是否覆盖原文件, 默认为 false, 新建 $filename.marknum.md 文件 16 | * -min: 最小标题级数, 范围[min,max), 默认为 2; 生成二级, 三级标题的序号(`## 1. 标题` 和 `### 1.1. 标题`) 17 | * -max: 最大标题级数, 范围[min,max), 默认为 4; 生成二级, 三级标题的序号(`## 1. 标题` 和 `### 1.1. 标题`) 18 | 19 | -------------------------------------------------------------------------------- /testdata/test.md: -------------------------------------------------------------------------------- 1 | # 一级标题 2 | 3 | ## 1. 二级标题 4 | 5 | ### 1.1. 三级标题 6 | 7 | ## 2. 二级标题 8 | 9 | --------------------------------------------------------------------------------