├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.bat ├── build.sh ├── go.mod ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /binaries 3 | /_testfiles 4 | /_testoutput.wiki 5 | gitbook-to-wiki.exe 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: 3 | - linux 4 | - osx 5 | go: 6 | - 1.14.x 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kataras2006@hotmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all read our [Code of Conduct](CODE_OF_CONDUCT.md). 4 | 5 | ## Found a bug? 6 | 7 | Open a new [issue](https://github.com/kataras/gitbook-to-wiki/issues/new). 8 | * Write the Operating System and the version of your machine. 9 | * Describe your problem, what did you expect to see and what you see instead. 10 | * If it's a feature request, describe your idea as better as you can. 11 | 12 | ## Code 13 | 14 | 1. Fork the [repository](https://github.com/kataras/gitbook-to-wiki). 15 | 2. Make your changes. 16 | 3. Compare & Push the PR from [here](https://github.com/kataras/gitbook-to-wiki/compare). 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Gerasimos Maropoulos 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitBook to GitHub Wiki 2 | 3 | [![build status](https://img.shields.io/travis/com/kataras/gitbook-to-wiki/master.svg?style=for-the-badge&logo=travis)](https://travis-ci.com/github/kataras/gitbook-to-wiki) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/kataras/gitbook-to-wiki) [![godocs](https://img.shields.io/badge/go-%20docs-488AC7.svg?style=for-the-badge)](https://pkg.go.dev/github.com/kataras/gitbook-to-wiki) 4 | 5 | This CLI tool was initially created to generate the [Iris Wiki](https://github.com/kataras/iris/wiki). Works with the latest https://gitbook.com as of **2020**. 6 | 7 | GitBook is the best tool on writing & publishing markdown books. They have an excellent support team and they do offer premium plans for Free and Open Source Projects (I have seen it myself!). 8 | 9 | ## Installation 10 | 11 | The only requirement is the [Go Programming Language](https://golang.org/dl). 12 | 13 | ```sh 14 | $ go get github.com/kataras/gitbook-to-wiki 15 | ``` 16 | 17 | By navigating to the [Releases page](https://github.com/kataras/gitbook-to-wiki/releases) you can also **download the executable file** for your operating system. Note that, the $PATH system environment variable should contain an entry of the `gitbook-to-wiki` program you have just downloaded. Alternatively, copy-paste the `gitbook-to-wiki` executable to the current working directory. 18 | 19 | ## Getting Started 20 | 21 | Navigate to the parent directory of your gitbook, e.g. `/home/me`. 22 | 23 | Clone your repository's wiki: 24 | ```sh 25 | $ git clone https://github.com/$username/$repo.wiki.git 26 | ``` 27 | 28 | The directory structure should look like that: 29 | ``` 30 | │ 31 | └───$repo.wiki 32 | | .git 33 | | Home.md 34 | └───$gitbook 35 | | SUMMARY.md 36 | ├───subdir 37 | | ...other files 38 | ``` 39 | 40 | Open a terminal window and execute the `gitbook-to-wiki`: 41 | ```sh 42 | $ gitbook-to-wiki -v --src=./$gitbook --dest=./$repo.wiki --remote=/$username/$repo/wiki 43 | ``` 44 | 45 | Push your changes to the `master` branch of your `$repo.wiki`: 46 | ```sh 47 | $ git add . 48 | $ git commit -S -m "add new sections" 49 | $ git push -u origin master 50 | ``` 51 | 52 | Navigate to and you should be able to read your GitBook as GitHub Wiki. Congrats, that's all! 53 | 54 | ## How it works? 55 | 56 | 1. Unescapes `\(should be unescaped\)` to `(should be unescaped)` (when you push GitBook's contents to a GitHub repository it automatically adds those escape characters) 57 | 2. Handles page references (`{% page-ref page="../dir/mypage.md" %}`) 58 | 3. Copies code snippets **untouchable** (that's trivial but important because older tools I've used reported and stopped the parsing because of a simple HTML code snippet!) 59 | 4. Handles **asset links**, both absolute(http...) and relative, e.g. `![](.gitbook/assets/image.png)` 60 | 5. Handles **section links**, e.g. `[page title](relative.md)` to `[[page title rel|relative]]` 61 | 6. Handles **sub directories and sub sections**, e.g. `responses/json.md` to `responses/responses-json.md` (so GitHub Wiki can see it as unique, as it does not support sub-directory-content). 62 | 7. Handles `SUMMARY.md` to `_Sidebar.md`, it is not just a simple copy-paste, a content like that: 63 | ```md 64 | 68 | # Table of contents 69 | 70 | * [What is Iris](README.md) 71 | 72 | ## 📌Getting started 73 | 74 | * [Installation](getting-started/installation.md) 75 | * [Quick start](getting-started/quick-start.md) 76 | ``` 77 | 78 | Is translated to: 79 | ```md 80 | 83 | * [[What is Iris|Home]] 84 | * 📌Getting started 85 | * [[Installation|getting-started-installation]] 86 | * [[Quick start|getting-started-quick-start]] 87 | ``` 88 | 8. And, of course, the `.git` directory is not copied or touched at all. 89 | 90 | Don't hesitate to ask for more features. This tool works for the Git Book of a 18k starred project's documentation but if I missed something please [let me know](https://github.com/kataras/gitbook-to-wiki/issues/new). 91 | 92 | ## License 93 | 94 | This software is created by [Gerasimos Maropoulos](https://twitter.com/MakisMaropoulos) and it is distributed under the [MIT License](LICENSE). 95 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | set executable=gitbook-to-wiki 2 | set output=./binaries 3 | set input=./main.go 4 | 5 | REM disable CGO 6 | set CGO_ENABLED=0 7 | 8 | ECHO Building windows binaries... 9 | REM windows-x64 10 | set GOOS=windows 11 | set GOARCH=amd64 12 | go build -ldflags="-s -w" -o %output%/%executable%-windows-amd64.exe %input% 13 | REM windows-x86 14 | set GOOS=windows 15 | set GOARCH=386 16 | go build -ldflags="-s -w" -o %output%/%executable%-windows-386.exe %input% 17 | 18 | ECHO Building linux binaries... 19 | REM linux-x64 20 | set GOOS=linux 21 | set GOARCH=amd64 22 | go build -ldflags="-s -w" -o %output%/%executable%-linux-amd64 %input% 23 | REM linux-x86 24 | set GOOS=linux 25 | set GOARCH=386 26 | go build -ldflags="-s -w" -o %output%/%executable%-linux-386 %input% 27 | REM linux-arm64 28 | set GOOS=linux 29 | set GOARCH=arm64 30 | go build -ldflags="-s -w" -o %output%/%executable%-linux-arm64 %input% 31 | REM linux-arm 32 | set GOOS=linux 33 | set GOARCH=arm 34 | go build -ldflags="-s -w" -o %output%/%executable%-linux-arm %input% 35 | 36 | ECHO Building darwin (osx) x64 binary... 37 | REM darwin-x64 38 | set GOOS=darwin 39 | set GOARCH=amd64 40 | go build -ldflags="-s -w" -o %output%/%executable%-darwin-amd64 %input% -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | executable="gitbook-to-wiki" 4 | output="./binaries" 5 | input="./main.go" 6 | 7 | # disable CGO 8 | export CGO_ENABLED=0 9 | 10 | # [-------Windows-------] 11 | echo "Building windows binaries..." 12 | # windows-x64 13 | export GOOS=windows 14 | export GOARCH=amd64 15 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-windows-amd64.exe $input 16 | # windows-x86 17 | export GOOS=windows 18 | export GOARCH=386 19 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-windows-386.exe $input 20 | 21 | # [---------Linux--------] 22 | echo "Building linux binaries..." 23 | # linux-x64 24 | export GOOS=linux 25 | export GOARCH=amd64 26 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-linux-amd64 $input 27 | # linux-x86 28 | export GOOS=linux 29 | export GOARCH=386 30 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-linux-386 $input 31 | # linux-arm64 32 | export GOOS=linux 33 | export GOARCH=arm64 34 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-linux-arm64 $input 35 | # linux-arm 36 | export GOOS=linux 37 | export GOARCH=arm 38 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-linux-arm $input 39 | 40 | # [---------OSX--------] 41 | echo "Building darwin (osx) x64 binary..." 42 | #darwin-x64 43 | export GOOS=darwin 44 | export GOARCH=amd64 45 | go build -ldflags="-s -w -X main.buildRevision=$(git rev-parse HEAD) -X main.buildTime=$(date +%s)" -o $output/$executable-darwin-amd64 $input -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/gitbook-to-wiki 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "regexp" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // TODO: add --version flag and version command. 20 | // Variables set by go build. 21 | var ( 22 | // buildRevision is the build revision (docker commit string or git rev-parse HEAD). 23 | buildRevision = "" 24 | // buildTime is the build unix time (in seconds since 1970-01-01 00:00:00 UTC). 25 | buildTime = "" 26 | ) 27 | 28 | var ( 29 | slash = []byte("/") 30 | newLine = []byte("\n") 31 | parenStart = []byte("(") 32 | parenEnd = []byte(")") 33 | 34 | bracketStart = []byte("[") 35 | bracketEnd = []byte("]") 36 | verticalBar = []byte("|") 37 | 38 | whitespace = []byte(" ") 39 | asteriskEntry = []byte("* ") 40 | 41 | refPrefix = []byte("> Reference: ") 42 | httpPrefix = []byte("http") 43 | 44 | markdownSuffix = []byte(".md") 45 | 46 | codeSnippet = []byte("```") 47 | 48 | slideBreak = []byte("") 49 | ) 50 | 51 | var ( 52 | srcDir = "./_testfiles" 53 | destDir = "./_testoutput.wiki" 54 | wikiRepo = "/kataras/iris/wiki" 55 | verbose = false 56 | keepLinks = false 57 | appendSlideBreak = false 58 | ) 59 | 60 | var ( 61 | // Note: if async use atomic package for these: 62 | totalFilesParsedCount int // uint32 63 | totalFilesCopiedCount int 64 | ) 65 | 66 | var ( 67 | errNotResponsible = errors.New("not responsible") 68 | errSkipLine = errors.New("skip line") 69 | ) 70 | 71 | /* 72 | TODO: skip comments as we do with snippets, note that: 73 | snippets can have comments too and comments can have snippets too. 74 | */ 75 | 76 | // Examples: 77 | // $ gitbook-to-wiki -v ./iris-book ./iris-wiki-test.wiki /kataras/iris-wiki-test/wiki 78 | // $ gitbook-to-wiki -v --keep-links --slidebreak --src=./_testfiles --dest=./_testoutput 79 | func main() { 80 | flag.StringVar(&srcDir, "src", srcDir, "--src=./my_gitbook (source input)") 81 | flag.StringVar(&destDir, "dest", destDir, "--dest=./my_repo.wiki (destination output)") 82 | flag.StringVar(&wikiRepo, "remote", wikiRepo, "--remote=/me/my_repo/wiki (GitHub wiki page base)") 83 | flag.BoolVar(&verbose, "v", verbose, "-v (to enable verbose messages)") 84 | flag.BoolVar(&keepLinks, "keep-links", keepLinks, "--keep-links (to keep the files and links as they are)") 85 | flag.BoolVar(&appendSlideBreak, "slidebreak", appendSlideBreak, "--slidebreak (to enable adding a slidebreak comment at the end of each markdown file)") 86 | flag.Parse() 87 | 88 | for i, arg := range flag.Args() { 89 | switch i { 90 | case 0: 91 | srcDir = arg 92 | case 1: 93 | destDir = arg 94 | case 2: 95 | wikiRepo = arg 96 | default: 97 | os.Stderr.WriteString("unknown argument " + arg) 98 | os.Exit(-1) 99 | } 100 | } 101 | 102 | os.MkdirAll(destDir, 0666) 103 | 104 | start := time.Now() 105 | err := filepath.Walk(srcDir, walkFn) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | 110 | finishDur := time.Since(start) 111 | logf("Total files parsed: %d", totalFilesParsedCount) 112 | logf("Total files copied: %d", totalFilesCopiedCount) 113 | logf("Time taken to complete: %s", finishDur) 114 | } 115 | 116 | func logf(format string, args ...interface{}) { 117 | if !verbose { 118 | return 119 | } 120 | 121 | log.Printf(format, args...) 122 | } 123 | 124 | func walkFn(inPath string, info os.FileInfo, err error) error { 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if info.IsDir() || !info.Mode().IsRegular() { 130 | if info.Name() == ".git" { 131 | logf("Skip <.git> directory") 132 | return filepath.SkipDir 133 | } 134 | 135 | return nil 136 | } 137 | 138 | f, err := os.Open(inPath) 139 | if err != nil { 140 | return err 141 | } 142 | defer f.Close() 143 | 144 | // relative path work after opening the file. 145 | rel, err := filepath.Rel(srcDir, inPath) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | rel = filepath.ToSlash(rel) 151 | 152 | outPath := filepath.Join(destDir, resolvePath(rel)) 153 | os.MkdirAll(filepath.Dir(outPath), 0666) 154 | 155 | outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_TRUNC, 0666) 156 | if err != nil { 157 | return err 158 | } 159 | defer outFile.Close() 160 | 161 | if verbose { 162 | inPath = filepath.ToSlash(inPath) 163 | outPath = filepath.ToSlash(outPath) 164 | } 165 | 166 | switch filepath.Ext(outPath) { 167 | case ".md": 168 | if verbose { 169 | if path.Base(inPath) != path.Base(outPath) { 170 | logf("Parse <%s> as <%s>", inPath, outPath) 171 | } else { 172 | logf("Parse <%s>", inPath) 173 | } 174 | } 175 | 176 | err = parse(rel, f, outFile) 177 | if err == nil { 178 | totalFilesParsedCount++ 179 | } 180 | return err 181 | default: 182 | 183 | logf("Copy <%s> to <%s>", inPath, outPath) 184 | _, err = io.Copy(outFile, f) 185 | if err == nil { 186 | totalFilesCopiedCount++ 187 | } 188 | return err 189 | } 190 | } 191 | 192 | func parse(filename string, src io.Reader, dest io.Writer) error { 193 | var ( 194 | out = bufio.NewWriter(dest) 195 | isMarkdownFile = strings.HasSuffix(filename, string(markdownSuffix)) 196 | // nr = &nameResolver{ 197 | // wikiRepo: "/kataras/iris/wiki", 198 | // } 199 | 200 | p = newParser(src) 201 | 202 | lineReplacers = []simpleLineReplacer{ 203 | tocEntry(filename, p), 204 | unescapeParens, 205 | unescapePageRefs, 206 | unescapeLinks(wikiRepo), 207 | } 208 | ) 209 | 210 | for { 211 | line, err := p.readLine() 212 | if err != nil { 213 | break 214 | } 215 | 216 | if bytes.HasPrefix(line, codeSnippet) { // start snippet, read and write without changes until 217 | out.Write(line) 218 | out.Write(newLine) 219 | for { 220 | line, err = p.readLine() 221 | if err != nil { 222 | break 223 | } 224 | 225 | out.Write(line) 226 | out.Write(newLine) 227 | 228 | if bytes.Equal(line, codeSnippet) { // end of the snippet. 229 | p.prevLine = p.prevLine[0:0] 230 | break 231 | } 232 | } 233 | 234 | continue 235 | } 236 | 237 | for _, rpl := range lineReplacers { 238 | line, err = rpl(line) 239 | if err != nil { 240 | // if err == errSkip { 241 | // continue readLoop 242 | // } 243 | if err == errNotResponsible { 244 | continue 245 | } 246 | 247 | if err == errSkipLine { 248 | break 249 | } 250 | 251 | return err 252 | } 253 | } 254 | 255 | // if last error was skip, no need to write a new line. 256 | if err == errSkipLine { 257 | continue 258 | } 259 | 260 | if len(line) > 0 { 261 | // If current and previous starts with > 262 | // then we must separate them with a second new line, 263 | // it's markdown thing, otherwise they act as one > . 264 | if line[0] == '>' && len(p.prevLine) > 0 && p.prevLine[0] == '>' { 265 | out.Write(newLine) 266 | } 267 | out.Write(line) 268 | } 269 | 270 | out.Write(newLine) 271 | 272 | // Could be inaccurate via a second call of p.readLine 273 | // inside of a line replacer, currently that happens to 274 | // SUMMARY.md file only so we are safe. 275 | p.prevLine = line 276 | } 277 | 278 | if appendSlideBreak && isMarkdownFile { 279 | out.Write(newLine) 280 | out.Write(slideBreak) 281 | out.Write(newLine) 282 | } 283 | 284 | return out.Flush() 285 | } 286 | 287 | // resolvePath returns output (to be saved) fullpath. 288 | func resolvePath(name string) string { 289 | name = filepath.ToSlash(name) 290 | if keepLinks { 291 | return name 292 | } 293 | 294 | name = strings.ReplaceAll(name, "../", "") // remove all ../, we don't handle it atm. 295 | 296 | dir := path.Dir(name) 297 | base := path.Base(name) 298 | if dir == "." { 299 | dir = "" 300 | } 301 | 302 | switch base { 303 | case "README.md": 304 | return path.Join(dir, "Home.md") 305 | case "SUMMARY.md": 306 | return path.Join(dir, "_Sidebar.md") 307 | default: 308 | // ../.gitbook/assets/image.png 309 | // _assets/image.png 310 | if strings.HasPrefix(name, ".gitbook/assets") { 311 | return strings.ReplaceAll(name, ".gitbook/assets", "_assets") 312 | } 313 | 314 | // responses/json.md 315 | // responses/responses-json.md 316 | // 317 | // responses/sub/other.md 318 | // responses/sub/responses-sub-other.md 319 | if dir != "" { 320 | base = "-" + base 321 | } 322 | 323 | newBase := strings.ReplaceAll(dir, "/", "-") + base 324 | return path.Join(dir, newBase) 325 | } 326 | } 327 | 328 | // resolveLink returns the wiki section name of "name" 329 | // or if it is asset, returns the full wiki link of the asset. 330 | func resolveLink(name string, wikiRepo string) string { 331 | name = resolvePath(name) 332 | if keepLinks { 333 | return name 334 | } 335 | 336 | if strings.HasPrefix(name, "_assets") { 337 | return path.Join(wikiRepo, name) 338 | } 339 | 340 | name = strings.TrimSuffix(path.Base(name), string(markdownSuffix)) 341 | return name 342 | } 343 | 344 | type parser struct { 345 | rd *bufio.Reader 346 | prevLine []byte // outside set, in order to give access for line placers. 347 | } 348 | 349 | func newParser(r io.Reader) *parser { 350 | return &parser{ 351 | rd: bufio.NewReader(r), 352 | } 353 | } 354 | 355 | func (p *parser) readLine() ([]byte, error) { 356 | var linePrefix []byte 357 | for { 358 | line, isPrefixed, err := p.rd.ReadLine() 359 | if err != nil { 360 | return nil, err 361 | } 362 | 363 | if isPrefixed { 364 | linePrefix = append(linePrefix, line...) 365 | continue 366 | } 367 | 368 | if len(linePrefix) > 0 { // not prefixed and has a prior line prefix, so it's the end of the big line. 369 | line = append(linePrefix, line...) 370 | linePrefix = linePrefix[0:0] 371 | } 372 | 373 | return line, nil 374 | } 375 | } 376 | 377 | func (p *parser) skipNextEmptyLine() bool { 378 | nextLine, err := p.rd.Peek(2) 379 | if err != nil { 380 | if err == io.EOF { 381 | return false 382 | } 383 | // Note: can fire EOF too. 384 | return false 385 | } 386 | 387 | isNewLine := len(nextLine) > 1 && nextLine[1] == newLine[0] 388 | if isNewLine { 389 | _, _ = p.readLine() 390 | } 391 | 392 | return isNewLine 393 | } 394 | 395 | type simpleLineReplacer func(line []byte) (result []byte, err error) 396 | 397 | var unescapeParensRegex = regexp.MustCompile(`\\\((.*?)\\\)`) 398 | 399 | func unescapeParens(line []byte) ([]byte, error) { 400 | return wrapRegex(unescapeParensRegex, line, parenStart, parenEnd), nil 401 | } 402 | 403 | var unescapePageRefRegex = regexp.MustCompile(`{% page-ref page="(.*?)" %}`) 404 | 405 | func unescapePageRefs(src []byte) ([]byte, error) { 406 | result := make([]byte, len(src)) 407 | copy(result, src) 408 | 409 | for _, submatches := range unescapePageRefRegex.FindAllSubmatch(src, -1) { 410 | // {% page-ref page="../view/view.md" %} 411 | // [View](../view/view.md) 412 | 413 | link := submatches[1] 414 | 415 | if baseIdx := bytes.LastIndex(link, slash); baseIdx != -1 && len(link)-1 > baseIdx { 416 | link = link[baseIdx+1:] 417 | } 418 | 419 | title := bytes.TrimSuffix(link, markdownSuffix) 420 | title = bytes.Title(title) 421 | 422 | start := refPrefix 423 | start = append(start, append(append(bracketStart, title...), bracketEnd...)...) 424 | start = append(start, parenStart...) 425 | end := parenEnd 426 | 427 | result = bytes.Replace(result, submatches[0], append(start, append(submatches[1], end...)...), 1) 428 | } 429 | 430 | return result, nil 431 | } 432 | 433 | var unescapeLinksRegex = regexp.MustCompile(`\[(.*?)]\(([^()]+)\)`) 434 | 435 | func unescapeLinks(wikiRepo string) simpleLineReplacer { 436 | return func(src []byte) ([]byte, error) { 437 | if keepLinks { 438 | return src, nil 439 | } 440 | 441 | result := make([]byte, len(src)) 442 | copy(result, src) 443 | 444 | // group 0: "[...](...)" 445 | // group 1: "title" 446 | // group 2: ("...") 447 | for _, submatches := range unescapeLinksRegex.FindAllSubmatch(src, -1) { 448 | name := submatches[2] 449 | if bytes.HasPrefix(name, httpPrefix) { 450 | continue 451 | } 452 | 453 | link := []byte(resolveLink(string(name), wikiRepo)) 454 | if !bytes.HasSuffix(name, markdownSuffix) { 455 | // if it's not a page, it's a link to an asset: 456 | result = bytes.Replace(result, name, link, 1) 457 | // not 100% precise, it could replace a link outside of []() in the same line but we rly don't care about it atm, it's a good thing. 458 | continue 459 | } 460 | 461 | // It's a section link: 462 | // [JSON](responses/json.md) to 463 | // [[JSON|responses-json]] 464 | title := submatches[1] 465 | 466 | if len(title) == 0 { 467 | return nil, fmt.Errorf("Title is missing from: %s", submatches[0]) 468 | } 469 | 470 | result = bytes.Replace(result, submatches[0], bytes.Join([][]byte{ 471 | bracketStart, 472 | bracketStart, 473 | title, 474 | verticalBar, 475 | link, 476 | bracketEnd, 477 | bracketEnd, 478 | }, nil), 1) 479 | } 480 | 481 | return result, nil 482 | } 483 | } 484 | 485 | func tocEntry(filename string, p *parser) simpleLineReplacer { 486 | return func(src []byte) ([]byte, error) { 487 | if path.Base(filename) != "SUMMARY.md" { 488 | return src, errNotResponsible 489 | } 490 | 491 | src = bytes.TrimSpace(src) 492 | if len(src) == 0 { 493 | return nil, nil 494 | } 495 | 496 | if len(src) < 4 { 497 | return src, nil 498 | } 499 | 500 | defer p.skipNextEmptyLine() // skip next empty line, the _Sidebar.md should NOT have any line separators. 501 | 502 | if src[0] == '#' { 503 | if src[1] != '#' { // is not followed by #, so it's a space or text. 504 | // 1st level header, probably a "Table of Contents" thing, 505 | // remove it by skipping (no new line). 506 | return nil, errSkipLine 507 | } 508 | 509 | // 2nd level header 510 | // From: 511 | // ## Compression 512 | // 513 | // * [Index](link.md) 514 | // -------To--------- 515 | // * Compression 516 | // * [Index](link) 517 | if src[2] == ' ' { 518 | // next is empty char, e.g. "## " 519 | return append(asteriskEntry, src[3:]...), nil 520 | } 521 | return append(asteriskEntry, src[2:]...), nil 522 | } 523 | 524 | if src[0] == '*' { 525 | tab := bytes.Repeat(whitespace, 2) 526 | 527 | // if the prev line was: 528 | // "* " or " * " then add two spaces, otherwise 529 | // it is a root * which could be translated from a "## header". 530 | // Example: 531 | // * [[What is Iris|Home]] 532 | // * 📌Getting started 533 | // * [[Installation|getting-started-installation]] 534 | // * [[Quick start|getting-started-quick-start]] 535 | if bytes.HasPrefix(p.prevLine, asteriskEntry) || bytes.HasPrefix(p.prevLine, bytes.Join([][]byte{ 536 | tab, 537 | asteriskEntry, 538 | }, nil)) { 539 | return append(tab, src...), nil 540 | } 541 | } 542 | 543 | return src, nil 544 | } 545 | } 546 | 547 | func wrapRegex(regex *regexp.Regexp, src, start, end []byte) []byte { 548 | result := make([]byte, len(src)) 549 | copy(result, src) 550 | for _, submatches := range regex.FindAllSubmatch(src, -1) { 551 | // \(text)\ 552 | // start + text + end 553 | result = bytes.Replace(result, submatches[0], append(start, append(submatches[1], end...)...), 1) 554 | } 555 | 556 | return result 557 | } 558 | 559 | func wrap(src []byte, start, end []byte) []byte { 560 | return append(start, append(src, end...)...) 561 | } 562 | 563 | // Of course need cleanup but it works like a charm for my needs. However, 564 | // if users ask to make it faster or perform code cleanup or add more features, as always, 565 | // I am ready to fulfill their wishes. 566 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | func TestWrap(t *testing.T) { 10 | var ( 11 | src = []byte("should wrapped") 12 | expected = []byte("(should wrapped)") 13 | ) 14 | 15 | if got := wrap(src, parenStart, parenEnd); !bytes.Equal(expected, got) { 16 | t.Fatalf("expected: %s but got: %s", expected, got) 17 | } 18 | } 19 | 20 | func TestWrapRegex(t *testing.T) { 21 | var ( 22 | expr = regexp.MustCompile(`\\\((.*?)\\\)`) 23 | src = []byte(`this \(should be unescaped\) always and \(that as well\).`) 24 | expected = []byte(`this (should be unescaped) always and (that as well).`) 25 | ) 26 | 27 | if got := wrapRegex(expr, src, parenStart, parenEnd); !bytes.Equal(expected, got) { 28 | t.Fatalf("expected: %s but got: %s", expected, got) 29 | } 30 | } 31 | 32 | func TestResolvePathLink(t *testing.T) { 33 | var tests = []struct { 34 | in string 35 | outFile string 36 | link string 37 | }{ 38 | {"relative.md", "relative.md", "relative"}, 39 | {"responses/json.md", "responses/responses-json.md", "responses-json"}, 40 | {"responses/sub/other.md", "responses/sub/responses-sub-other.md", "responses-sub-other"}, 41 | {"../view/view.md", "view/view-view.md", "view-view"}, 42 | {"../dependency-injection/inputs.md", "dependency-injection/dependency-injection-inputs.md", "dependency-injection-inputs"}, 43 | {"../.gitbook/assets/image.png", "_assets/image.png", "/kataras/iris/wiki/_assets/image.png"}, 44 | } 45 | 46 | wikiRepo := "/kataras/iris/wiki" 47 | 48 | for i, tt := range tests { 49 | if expected, got := tt.outFile, resolvePath(tt.in); expected != got { 50 | t.Fatalf("[%d] expected path: %s but got: %s", i, expected, got) 51 | } 52 | 53 | if expected, got := tt.link, resolveLink(tt.in, wikiRepo); expected != got { 54 | t.Fatalf("[%d] expected link: %s but got: %s", i, expected, got) 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------