├── .dockerignore ├── README.md ├── .gitignore ├── Dockerfile ├── linereader └── linereader.go ├── data └── data.go ├── checkers ├── links_test.go ├── frontmatter_test.go ├── frontmatter.go └── links.go ├── Makefile ├── main.go └── LICENSE /.dockerignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Dockerfile 3 | .dockerignore 4 | markdownlink.zip 5 | markdownlink 6 | markdownlink.app 7 | markdownlink.exe 8 | README.md 9 | LICENSE 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # markdownlint 3 | 4 | Docker documentation source checker tool 5 | 6 | This tool ensures the markdown files its given are free from common errors. 7 | 8 | Its used by the https://docs.docker.com project for testing PR's. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | markdownlint 23 | *.exe 24 | *.app 25 | *.zip 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | # Simplify making releases 4 | RUN apt-get update \ 5 | && apt-get install -yq zip bzip2 6 | RUN wget -O github-release.bz2 https://github.com/aktau/github-release/releases/download/v0.6.2/linux-amd64-github-release.tar.bz2 \ 7 | && tar jxvf github-release.bz2 \ 8 | && mv bin/linux/amd64/github-release /usr/local/bin/ \ 9 | && rm github-release.bz2 10 | 11 | ENV GOPATH /go 12 | ENV USER root 13 | 14 | WORKDIR /go/src/github.com/docker/markdownlint 15 | 16 | RUN go get github.com/russross/blackfriday 17 | RUN go get github.com/miekg/mmark 18 | 19 | ADD . /go/src/github.com/docker/markdownlint 20 | RUN go get -d -v 21 | RUN go test -v ./... 22 | 23 | RUN go build -o markdownlint main.go \ 24 | && GOOS=windows GOARCH=amd64 go build -o markdownlint.exe main.go \ 25 | && GOOS=darwin GOARCH=amd64 go build -o markdownlint.app main.go \ 26 | && zip markdownlint.zip markdownlint markdownlint.exe markdownlint.app 27 | -------------------------------------------------------------------------------- /linereader/linereader.go: -------------------------------------------------------------------------------- 1 | package linereader 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Fake Reader that can 'unread' a complete line 12 | type LineReader struct { 13 | file *os.File 14 | reader *bufio.Reader 15 | unreadLine string 16 | } 17 | 18 | // For testing 19 | func ByteReader(str string) *LineReader { 20 | reader := strings.NewReader(str) 21 | r := new(LineReader) 22 | r.reader = bufio.NewReader(reader) 23 | return r 24 | } 25 | 26 | func OpenReader(filename string) (*LineReader, error) { 27 | f, err := os.Open(filename) 28 | if err != nil { 29 | return nil, err 30 | } 31 | reader := bufio.NewReader(f) 32 | r := new(LineReader) 33 | r.file = f 34 | r.reader = reader 35 | return r, nil 36 | } 37 | 38 | func (r *LineReader) ReadLine() (line []byte, isPrefix bool, err error) { 39 | if r.unreadLine == "" { 40 | return r.reader.ReadLine() 41 | } 42 | lines := strings.SplitN(r.unreadLine, "\n", 2) 43 | r.unreadLine = lines[1] 44 | return []byte(lines[0]), false, nil 45 | } 46 | 47 | func (r *LineReader) UnreadLine(str string) { 48 | r.unreadLine = strings.Join([]string{str, r.unreadLine}, "\n") 49 | } 50 | 51 | func (r *LineReader) Read(p []byte) (l int, err error) { 52 | offset := 0 53 | if r.unreadLine != "" { 54 | copy(p, r.unreadLine) 55 | offset = len(r.unreadLine) 56 | p[offset] = '\n' 57 | offset++ 58 | } 59 | buff, err := ioutil.ReadAll(r.reader) 60 | cl := copy(p[offset:], buff[0:]) 61 | if cl != len(buff) && err == nil { 62 | err = fmt.Errorf("supplied buffer (%d) too small to fit remainder of file (%d), only copied %d", len(p), offset+len(buff), cl) 63 | } 64 | l = offset + cl 65 | return l, err 66 | } 67 | 68 | func (r *LineReader) Close() { 69 | if r.file != nil { 70 | r.file.Close() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var verbose = flag.Bool("v", false, "verbose log output") 9 | 10 | func ErrorLog(format string, a ...interface{}) (n int, err error) { 11 | return fmt.Printf("ERROR: "+format, a...) 12 | } 13 | 14 | func VerboseLog(format string, a ...interface{}) (n int, err error) { 15 | if !*verbose { 16 | return 0, nil 17 | } 18 | return fmt.Printf(format, a...) 19 | } 20 | 21 | // allFiles a lookup table of all the files in the 'docs' dir 22 | // also takes advantage of the random order to avoid testing markdown files in the same order. 23 | type FileDetails struct { 24 | FullPath string 25 | Meta map[string]string 26 | FormatErrors string 27 | FormatErrorCount int 28 | } 29 | 30 | func NewFileDetails(file, path string) *FileDetails { 31 | detail := new(FileDetails) 32 | detail.FullPath = path 33 | detail.Meta = make(map[string]string) 34 | detail.FormatErrors = "" 35 | detail.FormatErrorCount = 0 36 | return detail 37 | } 38 | 39 | type BigMap map[string]*FileDetails 40 | 41 | var AllFiles BigMap 42 | 43 | func AddFile(file, path string) { 44 | if _, ok := AllFiles[file]; !ok { 45 | AllFiles[file] = NewFileDetails(file, path) 46 | } 47 | } 48 | 49 | type LinkDetails struct { 50 | Count int 51 | LinksFrom map[int]string 52 | ActualLink map[int]string 53 | Response int 54 | } 55 | 56 | var ResponseCode = map[int]string{ 57 | 999: "failed to parse", 58 | 888: "failed to crawl, ignoring", 59 | 2900: "local file path - ok", 60 | 900: "mail/irc link, not checked", 61 | 200: "ok", 62 | 777: "source type path, but no match found", 63 | 290: "local file path, but missing `.md`", 64 | 404: "external url, but failed", 65 | 666: "Don't link to docs.docker.com", 66 | 299: "Skipped due to filter", 67 | } 68 | 69 | var AllLinks map[string]*LinkDetails = make(map[string]*LinkDetails) 70 | -------------------------------------------------------------------------------- /checkers/links_test.go: -------------------------------------------------------------------------------- 1 | package checkers 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/docker/markdownlint/data" 9 | "github.com/miekg/mmark" 10 | ) 11 | 12 | func TestMarkdownLinks(t *testing.T) { 13 | file := "test/index.md" 14 | data.AllFiles = make(map[string]*data.FileDetails) 15 | data.AddFile(file, file) 16 | 17 | htmlFlags := 0 18 | renderParameters := mmark.HtmlRendererParameters{} 19 | renderer := &TestRenderer{ 20 | LinkFrom: file, 21 | //Html: mmark.HtmlRenderer(htmlFlags, "", "").(*mmark.Html), 22 | Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters), 23 | } 24 | out := bytes.NewBuffer(make([]byte, 1024)) 25 | 26 | tests := map[string]string{ 27 | "../first.md": "first.md", 28 | "second.md": "test/second.md", 29 | "./second.md": "test/second.md", 30 | "banana/second.md": "test/banana/second.md", 31 | "/test/banana/second.md": "test/banana/second.md", 32 | "twice.md": "test/twice.md", 33 | "banana/twice.md": "test/banana/twice.md", 34 | } 35 | 36 | for _, path := range tests { 37 | data.AddFile(path, path) 38 | } 39 | for link, _ := range tests { 40 | renderer.Link(out, []byte(link), []byte("title"), []byte("content")) 41 | } 42 | 43 | for link, details := range data.AllLinks { 44 | data.AllLinks[link].Response = testUrl(link, "", true) 45 | //fmt.Printf("\t\t(%d) %d links to %s\n", data.AllLinks[link].Response, details.Count, link) 46 | fmt.Printf("%s links to %s\n", details.ActualLink[0], link) 47 | if _, ok := data.AllFiles[link]; !ok { 48 | t.Errorf("ERROR(%d): not found %s links to %s\n", details.Response, details.ActualLink[0], link) 49 | } 50 | if tests[details.ActualLink[0]] != link { 51 | t.Errorf("ERROR(%d): %s links to %s, should link to %s\n", details.Response, details.ActualLink[0], link, tests[details.ActualLink[0]]) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Adds build information from git repo 3 | # 4 | # as suggested by tatsushid in 5 | # https://github.com/spf13/hugo/issues/540 6 | 7 | COMMIT_HASH=`git rev-parse --short HEAD 2>/dev/null` 8 | BUILD_DATE=`date +%FT%T%z` 9 | LDFLAGS=-ldflags "-X github.com/spf13/hugo/hugolib.CommitHash=${COMMIT_HASH} -X github.com/spf13/hugo/hugolib.BuildDate=${BUILD_DATE}" 10 | 11 | build: 12 | go build -o markdownlint main.go 13 | 14 | shell: docker-build 15 | docker run --rm -it -v $(CURDIR):/go/src/github.com/docker/markdownlint markdownlint bash 16 | 17 | docker-build: 18 | rm -f markdownlint.zip 19 | docker build -t markdownlint . 20 | 21 | docker: docker-build 22 | docker rm markdownlint-build || true 23 | docker run --name markdownlint-build markdownlint ls 24 | docker cp markdownlint-build:/go/src/github.com/docker/markdownlint/markdownlint.zip . 25 | rm -f markdownlint 26 | unzip -o markdownlint.zip 27 | 28 | run: 29 | ./markdownlint . 30 | 31 | validate: 32 | docker run \ 33 | -v $(CURDIR)/markdownlint:/usr/bin/markdownlint \ 34 | --volumes-from docsdockercom_data_1 \ 35 | --rm -it \ 36 | debian /usr/bin/markdownlint /docs/content/ 37 | 38 | AWSTOKENSFILE ?= ../aws.env 39 | -include $(AWSTOKENSFILE) 40 | export GITHUB_USERNAME GITHUB_TOKEN 41 | 42 | RELEASE_DATE=`date +%F` 43 | 44 | release: docker 45 | # TODO: check that we have upstream master, bail if not 46 | docker run --rm -it -e GITHUB_TOKEN markdownlint \ 47 | github-release release --user docker --repo markdownlint --tag $(RELEASE_DATE) 48 | docker run --rm -it -e GITHUB_TOKEN markdownlint \ 49 | github-release upload --user docker --repo markdownlint --tag $(RELEASE_DATE) \ 50 | --name markdownlint \ 51 | --file markdownlint 52 | docker run --rm -it -e GITHUB_TOKEN markdownlint \ 53 | github-release upload --user docker --repo markdownlint --tag $(RELEASE_DATE) \ 54 | --name markdownlint-osx \ 55 | --file markdownlint.app 56 | docker run --rm -it -e GITHUB_TOKEN markdownlint \ 57 | github-release upload --user docker --repo markdownlint --tag $(RELEASE_DATE) \ 58 | --name markdownlint.exe \ 59 | --file markdownlint.exe 60 | -------------------------------------------------------------------------------- /checkers/frontmatter_test.go: -------------------------------------------------------------------------------- 1 | package checkers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/markdownlint/data" 7 | "github.com/docker/markdownlint/linereader" 8 | ) 9 | 10 | // NOTE: this has some spaces and tabs as well as newlines at the start. this is intentional 11 | const OK_TOPIC = ` 12 | 13 | 14 | 15 | 24 | # Dockerfile reference 25 | ` 26 | const MISSING_COMMENT_START_TOPIC = ` 27 | 28 | 29 | 30 | +++ 31 | title = "Dockerfile reference" 32 | description = "Dockerfiles use a simple DSL which allows you to automate the steps you would normally manually take to create an image." 33 | keywords = ["builder, docker, Dockerfile, automation, image creation"] 34 | [menu.main] 35 | parent = "mn_reference" 36 | +++ 37 | 38 | # Dockerfile reference 39 | ` 40 | const MISSING_COMMENT_END_TOPIC = ` 41 | 42 | 43 | 44 | ") { 38 | data.VerboseLog("found comment start") 39 | foundComment = true 40 | continue 41 | } 42 | } 43 | //data.VerboseLog("ReadLine: %s, %v, %s\n", string(byteBuff), isPrefix, err) 44 | for i := 0; i < len(buff); { 45 | runeValue, width := utf8.DecodeRuneInString(buff[i:]) 46 | if unicode.IsSpace(runeValue) { 47 | i += width 48 | } else { 49 | data.VerboseLog("Unexpected non-whitespace char: %s", buff) 50 | return fmt.Errorf("Unexpected non-whitespace char: %s", buff) 51 | } 52 | } 53 | } 54 | 55 | data.AllFiles[file].Meta = make(map[string]string) 56 | 57 | // read lines until `+++` ending 58 | for err == nil { 59 | byteBuff, _, err := reader.ReadLine() 60 | if err != nil { 61 | return err 62 | } 63 | buff := string(byteBuff) 64 | if buff == "+++" { 65 | data.VerboseLog("Found TOML end") 66 | break 67 | } 68 | data.VerboseLog("\t%s\n", buff) 69 | 70 | meta := strings.SplitN(buff, "=", 2) 71 | data.VerboseLog("\t%d\t%v\n", len(meta), meta) 72 | if len(meta) == 2 { 73 | data.VerboseLog("\t\t%s: %s\n", meta[0], meta[1]) 74 | data.AllFiles[file].Meta[strings.Trim(meta[0], " ")] = strings.Trim(meta[1], " ") 75 | } 76 | } 77 | // remove trailing close comment 78 | if foundComment { 79 | byteBuff, _, err := reader.ReadLine() 80 | if err != nil { 81 | return err 82 | } 83 | buff := string(byteBuff) 84 | data.VerboseLog("is this a comment? (%s)\n", buff) 85 | if strings.HasSuffix(buff, "-->") { 86 | if !strings.HasPrefix(buff, "