Move mungedocs tool to the docs repo. (#5178)
* Remove seemingly errant file * Move mungedocs tool to docs repo * Bump go version to 1.8.3
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- 1.8.1
|
- 1.8.3
|
||||||
|
|
||||||
# Don't want default ./... here:
|
# Don't want default ./... here:
|
||||||
install:
|
install:
|
||||||
@@ -19,8 +19,8 @@ install:
|
|||||||
- git clone --depth=50 --branch=master https://github.com/kubernetes/md-check $HOME/gopath/src/k8s.io/md-check
|
- git clone --depth=50 --branch=master https://github.com/kubernetes/md-check $HOME/gopath/src/k8s.io/md-check
|
||||||
- go get -t -v k8s.io/md-check
|
- go get -t -v k8s.io/md-check
|
||||||
|
|
||||||
# (3) Fetch mungedocs
|
# (3) Build mungedocs
|
||||||
- go get -v k8s.io/kubernetes/cmd/mungedocs
|
- go install ./_tools/mungedocs
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- go test -v k8s.io/kubernetes.github.io/test
|
- go test -v k8s.io/kubernetes.github.io/test
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Documentation Mungers
|
||||||
|
|
||||||
|
Basically this is like lint/gofmt for md docs.
|
||||||
|
|
||||||
|
It basically does the following:
|
||||||
|
- iterate over all files in the given doc root.
|
||||||
|
- for each file split it into a slice (mungeLines) of lines (mungeLine)
|
||||||
|
- a mungeline has metadata about each line typically determined by a 'fast' regex.
|
||||||
|
- metadata contains things like 'is inside a preformatted block'
|
||||||
|
- contains a markdown header
|
||||||
|
- has a link to another file
|
||||||
|
- etc..
|
||||||
|
- if you have a really slow regex with a lot of backtracking you might want to write a fast one to limit how often you run the slow one.
|
||||||
|
- each munger is then called in turn
|
||||||
|
- they are given the mungeLines
|
||||||
|
- they create an entirely new set of mungeLines with their modifications
|
||||||
|
- the new set is returned
|
||||||
|
- the new set is then fed into the next munger.
|
||||||
|
- in the end we might commit the end mungeLines to the file or not (--verify)
|
||||||
|
|
||||||
|
|
||||||
|
[]()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const analyticsMungeTag = "GENERATED_ANALYTICS"
|
||||||
|
const analyticsLinePrefix = "[ (mungeLines, error) {
|
||||||
|
var out mungeLines
|
||||||
|
fileName, err := makeRepoRelative(fileName, fileName)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf(analyticsLinePrefix+"%s?pixel)]()", fileName)
|
||||||
|
insertLines := getMungeLines(link)
|
||||||
|
mlines, err = removeMacroBlock(analyticsMungeTag, mlines)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove floating analytics links not surrounded by the munge tags.
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted || mline.header || mline.beginTag || mline.endTag {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(mline.data, analyticsLinePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, mline)
|
||||||
|
}
|
||||||
|
out = appendMacroBlock(out, analyticsMungeTag)
|
||||||
|
out, err = updateMacroBlock(out, analyticsMungeTag, insertLines)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnalytics(t *testing.T) {
|
||||||
|
b := beginMungeTag("GENERATED_ANALYTICS")
|
||||||
|
e := endMungeTag("GENERATED_ANALYTICS")
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"aoeu",
|
||||||
|
"aoeu" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
{
|
||||||
|
"aoeu" + "\n" + "\n" + "\n" +
|
||||||
|
"[]()",
|
||||||
|
"aoeu" + "\n" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
{
|
||||||
|
"aoeu" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n",
|
||||||
|
"aoeu" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
{
|
||||||
|
"aoeu" + "\n" + "\n" +
|
||||||
|
"[]()" + "\n" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n",
|
||||||
|
"aoeu" + "\n" + "\n" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
{
|
||||||
|
"prefix" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e +
|
||||||
|
"\n" + "suffix",
|
||||||
|
"prefix" + "\n" + "suffix" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
{
|
||||||
|
"aoeu" + "\n" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n",
|
||||||
|
"aoeu" + "\n" + "\n" + "\n" +
|
||||||
|
b + "\n" +
|
||||||
|
"[]()" + "\n" +
|
||||||
|
e + "\n"},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
out, err := updateAnalytics("path/to/file-name.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expected.Equal(out) {
|
||||||
|
t.Errorf("Case %d Expected \n\n%v\n\n but got \n\n%v\n\n", i, expected.String(), out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const exampleToken = "EXAMPLE"
|
||||||
|
|
||||||
|
const exampleLineStart = "<!-- BEGIN MUNGE: EXAMPLE"
|
||||||
|
|
||||||
|
var exampleMungeTagRE = regexp.MustCompile(beginMungeTag(fmt.Sprintf("%s %s", exampleToken, `(([^ ])*[.]([^.]*))`)))
|
||||||
|
|
||||||
|
// syncExamples updates all examples in markdown file.
|
||||||
|
//
|
||||||
|
// Finds the magic macro block tags, find the link to the example
|
||||||
|
// specified in the tags, and replaces anything between those with
|
||||||
|
// the content of the example, thereby syncing it.
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
// <!-- BEGIN MUNGE: EXAMPLE ../../examples/guestbook/frontend-service.yaml -->
|
||||||
|
//
|
||||||
|
// ```yaml
|
||||||
|
// foo:
|
||||||
|
// bar:
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// [Download example](../../examples/guestbook/frontend-service.yaml?raw=true)
|
||||||
|
// <!-- END MUNGE: EXAMPLE -->
|
||||||
|
func syncExamples(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var err error
|
||||||
|
type exampleTag struct {
|
||||||
|
token string
|
||||||
|
linkText string
|
||||||
|
fileType string
|
||||||
|
}
|
||||||
|
exampleTags := []exampleTag{}
|
||||||
|
|
||||||
|
// collect all example Tags
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted || !mline.beginTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := mline.data
|
||||||
|
if !strings.HasPrefix(line, exampleLineStart) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match := exampleMungeTagRE.FindStringSubmatch(line)
|
||||||
|
if len(match) < 4 {
|
||||||
|
err = fmt.Errorf("Found unparsable EXAMPLE munge line %v", line)
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
tag := exampleTag{
|
||||||
|
token: exampleToken + " " + match[1],
|
||||||
|
linkText: match[1],
|
||||||
|
fileType: match[3],
|
||||||
|
}
|
||||||
|
exampleTags = append(exampleTags, tag)
|
||||||
|
}
|
||||||
|
// update all example Tags
|
||||||
|
for _, tag := range exampleTags {
|
||||||
|
ft := ""
|
||||||
|
if tag.fileType == "json" {
|
||||||
|
ft = "json"
|
||||||
|
}
|
||||||
|
if tag.fileType == "yaml" {
|
||||||
|
ft = "yaml"
|
||||||
|
}
|
||||||
|
example, err := exampleContent(filePath, tag.linkText, ft)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
mlines, err = updateMacroBlock(mlines, tag.token, example)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mlines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exampleContent retrieves the content of the file at linkPath
|
||||||
|
func exampleContent(filePath, linkPath, fileType string) (mungeLines, error) {
|
||||||
|
repoRel, err := makeRepoRelative(linkPath, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRel, err := makeFileRelative(linkPath, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dat, err := ioutil.ReadFile(repoRel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove leading and trailing spaces and newlines
|
||||||
|
trimmedFileContent := strings.TrimSpace(string(dat))
|
||||||
|
content := fmt.Sprintf("\n```%s\n%s\n```\n\n[Download example](%s?raw=true)", fileType, trimmedFileContent, fileRel)
|
||||||
|
out := getMungeLines(content)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_syncExamples(t *testing.T) {
|
||||||
|
var podExample = `apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
`
|
||||||
|
var textExample = `some text
|
||||||
|
`
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE testdata/pod.yaml -->\n<!-- END MUNGE: EXAMPLE testdata/pod.yaml -->\n",
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE testdata/pod.yaml -->\n\n```yaml\n" + podExample + "```\n\n[Download example](testdata/pod.yaml?raw=true)\n<!-- END MUNGE: EXAMPLE testdata/pod.yaml -->\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE ../mungedocs/testdata/pod.yaml -->\n<!-- END MUNGE: EXAMPLE ../mungedocs/testdata/pod.yaml -->\n",
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE ../mungedocs/testdata/pod.yaml -->\n\n```yaml\n" + podExample + "```\n\n[Download example](../mungedocs/testdata/pod.yaml?raw=true)\n<!-- END MUNGE: EXAMPLE ../mungedocs/testdata/pod.yaml -->\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE testdata/example.txt -->\n<!-- END MUNGE: EXAMPLE testdata/example.txt -->\n",
|
||||||
|
"<!-- BEGIN MUNGE: EXAMPLE testdata/example.txt -->\n\n```\n" + textExample + "```\n\n[Download example](testdata/example.txt?raw=true)\n<!-- END MUNGE: EXAMPLE testdata/example.txt -->\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repoRoot = ""
|
||||||
|
for _, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := syncExamples("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
t.Errorf("Expected example \n'%q' but got \n'%q'", expected.String(), actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var headerRegex = regexp.MustCompile(`^(#+)\s*(.*)$`)
|
||||||
|
|
||||||
|
func fixHeaderLine(mlines mungeLines, newlines mungeLines, linenum int) mungeLines {
|
||||||
|
var out mungeLines
|
||||||
|
|
||||||
|
mline := mlines[linenum]
|
||||||
|
line := mlines[linenum].data
|
||||||
|
|
||||||
|
matches := headerRegex.FindStringSubmatch(line)
|
||||||
|
if matches == nil {
|
||||||
|
out = append(out, mline)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// There must be a blank line before the # (unless first line in file)
|
||||||
|
if linenum != 0 {
|
||||||
|
newlen := len(newlines)
|
||||||
|
if newlines[newlen-1].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There must be a space AFTER the ##'s
|
||||||
|
newline := fmt.Sprintf("%s %s", matches[1], matches[2])
|
||||||
|
newmline := newMungeLine(newline)
|
||||||
|
out = append(out, newmline)
|
||||||
|
|
||||||
|
// The next line needs to be a blank line (unless last line in file)
|
||||||
|
if len(mlines) > linenum+1 && mlines[linenum+1].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header lines need whitespace around them and after the #s.
|
||||||
|
func updateHeaderLines(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var out mungeLines
|
||||||
|
for i, mline := range mlines {
|
||||||
|
if mline.preformatted {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !mline.header {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newLines := fixHeaderLine(mlines, out, i)
|
||||||
|
out = append(out, newLines...)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeaderLines(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{
|
||||||
|
"# ok",
|
||||||
|
"# ok",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"## ok",
|
||||||
|
"## ok",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##### ok",
|
||||||
|
"##### ok",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##fix",
|
||||||
|
"## fix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\n\n##fix\n\nbar",
|
||||||
|
"foo\n\n## fix\n\nbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\n##fix\nbar",
|
||||||
|
"foo\n\n## fix\n\nbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\n```\n##fix\n```\nbar",
|
||||||
|
"foo\n```\n##fix\n```\nbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\n#fix1\n##fix2\nbar",
|
||||||
|
"foo\n\n# fix1\n\n## fix2\n\nbar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := updateHeaderLines("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Errorf("case[%d]: expected %q got %q", i, c.expected, actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Looks for lines that have kubectl commands with -f flags and files that
|
||||||
|
// don't exist.
|
||||||
|
func updateKubectlFileTargets(file string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var errors []string
|
||||||
|
for i, mline := range mlines {
|
||||||
|
if !mline.preformatted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := lookForKubectl(mline.data, i); err != nil {
|
||||||
|
errors = append(errors, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := error(nil)
|
||||||
|
if len(errors) != 0 {
|
||||||
|
err = fmt.Errorf("%s", strings.Join(errors, "\n"))
|
||||||
|
}
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookForKubectl(line string, lineNum int) error {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
for i := range fields {
|
||||||
|
if fields[i] == "kubectl" {
|
||||||
|
return gotKubectl(lineNum, fields, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gotKubectl(lineNum int, fields []string, fieldNum int) error {
|
||||||
|
for i := fieldNum + 1; i < len(fields); i++ {
|
||||||
|
switch fields[i] {
|
||||||
|
case "create", "update", "replace", "delete":
|
||||||
|
return gotCommand(lineNum, fields, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gotCommand(lineNum int, fields []string, fieldNum int) error {
|
||||||
|
for i := fieldNum + 1; i < len(fields); i++ {
|
||||||
|
if strings.HasPrefix(fields[i], "-f") {
|
||||||
|
return gotDashF(lineNum, fields, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gotDashF(lineNum int, fields []string, fieldNum int) error {
|
||||||
|
target := ""
|
||||||
|
if fields[fieldNum] == "-f" {
|
||||||
|
if fieldNum+1 == len(fields) {
|
||||||
|
return fmt.Errorf("ran out of fields after '-f'")
|
||||||
|
}
|
||||||
|
target = fields[fieldNum+1]
|
||||||
|
} else {
|
||||||
|
target = fields[fieldNum][2:]
|
||||||
|
}
|
||||||
|
// Turn dirs into file-like names.
|
||||||
|
target = strings.TrimRight(target, "/")
|
||||||
|
|
||||||
|
// Now exclude special-cases
|
||||||
|
|
||||||
|
if target == "-" || target == "FILENAME" {
|
||||||
|
// stdin and "FILENAME" are OK
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
||||||
|
// URLs are ok
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(target, "./") {
|
||||||
|
// Same-dir files are usually created in the same example
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(target, "~/") {
|
||||||
|
// Home directory may also be created by the same example
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(target, "/") {
|
||||||
|
// Absolute paths tend to be /tmp/* and created in the same example.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(target, "$") {
|
||||||
|
// Allow the start of the target to be an environment
|
||||||
|
// variable that points to the root of the kubernetes
|
||||||
|
// repo.
|
||||||
|
split := strings.SplitN(target, "/", 2)
|
||||||
|
if len(split) == 2 {
|
||||||
|
target = split[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here we expect the file to exist.
|
||||||
|
_, err := os.Stat(path.Join(repoRoot, target))
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("%d: target file %q does not exist", lineNum, target)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestKubectlDashF(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
// No match
|
||||||
|
{"", true},
|
||||||
|
{
|
||||||
|
"Foo\nBar\n",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\nkubectl blah blech\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```shell\nkubectl blah blech\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create blech\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// Special cases
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f -\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f-\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f FILENAME\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -fFILENAME\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f http://google.com/foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -fhttp://google.com/foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f ./foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f./foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f /foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f/foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f~/foobar\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// Real checks
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -f mungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah create -fmungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah update -f mungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah update -fmungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah replace -f mungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah replace -fmungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah delete -f mungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah delete -fmungedocs.go\n```\nBar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// Failures
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah delete -f does_not_exist\n```\nBar",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Foo\n```\nkubectl -blah delete -fdoes_not_exist\n```\nBar",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
repoRoot = ""
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
_, err := updateKubectlFileTargets("filename.md", in)
|
||||||
|
if err != nil && c.ok {
|
||||||
|
t.Errorf("case[%d]: expected success, got %v", i, err)
|
||||||
|
}
|
||||||
|
if err == nil && !c.ok {
|
||||||
|
t.Errorf("case[%d]: unexpected success", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Finds markdown links of the form [foo](bar "alt-text").
|
||||||
|
linkRE = regexp.MustCompile(`\[([^]]*)\]\(([^)]*)\)`)
|
||||||
|
// Finds markdown link typos of the form (foo)[bar]
|
||||||
|
badLinkRE = regexp.MustCompile(`\([^]()]*\)\[[^]()]*\]`)
|
||||||
|
// Splits the link target into link target and alt-text.
|
||||||
|
altTextRE = regexp.MustCompile(`([^)]*)( ".*")`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func processLink(in string, filePath string) (string, error) {
|
||||||
|
var errs []string
|
||||||
|
out := linkRE.ReplaceAllStringFunc(in, func(in string) string {
|
||||||
|
var err error
|
||||||
|
match := linkRE.FindStringSubmatch(in)
|
||||||
|
if match == nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("Detected this line had a link, but unable to parse, %v", in))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// match[0] is the entire expression;
|
||||||
|
visibleText := match[1]
|
||||||
|
linkText := match[2]
|
||||||
|
altText := ""
|
||||||
|
if parts := altTextRE.FindStringSubmatch(linkText); parts != nil {
|
||||||
|
linkText = parts[1]
|
||||||
|
altText = parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up some random garbage I found in our docs.
|
||||||
|
linkText = strings.Trim(linkText, " ")
|
||||||
|
linkText = strings.Trim(linkText, "\n")
|
||||||
|
linkText = strings.Trim(linkText, " ")
|
||||||
|
|
||||||
|
u, terr := url.Parse(linkText)
|
||||||
|
if terr != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("link %q is unparsable: %v", linkText, terr))
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Host != "" && u.Host != "github.com" {
|
||||||
|
// We only care about relative links and links within github.
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestedVisibleText := visibleText
|
||||||
|
if u.Path != "" && !strings.HasPrefix(linkText, "TODO:") {
|
||||||
|
newPath, targetExists := checkPath(filePath, path.Clean(u.Path))
|
||||||
|
if !targetExists {
|
||||||
|
errs = append(errs, fmt.Sprintf("%q: target not found", linkText))
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
u.Path = newPath
|
||||||
|
if strings.HasPrefix(u.Path, "/") {
|
||||||
|
u.Host = "github.com"
|
||||||
|
u.Scheme = "https"
|
||||||
|
} else {
|
||||||
|
// Remove host and scheme from relative paths
|
||||||
|
u.Host = ""
|
||||||
|
u.Scheme = ""
|
||||||
|
}
|
||||||
|
// Make the visible text show the absolute path if it's
|
||||||
|
// not nested in or beneath the current directory.
|
||||||
|
if strings.HasPrefix(u.Path, "..") {
|
||||||
|
dir := path.Dir(filePath)
|
||||||
|
suggestedVisibleText, err = makeRepoRelative(path.Join(dir, u.Path), filePath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%q: unable to make path relative", filePath))
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
suggestedVisibleText = u.Path
|
||||||
|
}
|
||||||
|
var unescaped string
|
||||||
|
if unescaped, err = url.QueryUnescape(u.String()); err != nil {
|
||||||
|
// Remove %28 type stuff, be nice to humans.
|
||||||
|
// And don't fight with the toc generator.
|
||||||
|
linkText = unescaped
|
||||||
|
} else {
|
||||||
|
linkText = u.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the current visible text is trying to be a file name, use
|
||||||
|
// the correct file name.
|
||||||
|
if strings.HasSuffix(visibleText, ".md") && !strings.ContainsAny(visibleText, ` '"`+"`") {
|
||||||
|
visibleText = suggestedVisibleText
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%s](%s)", visibleText, linkText+altText)
|
||||||
|
})
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return "", errors.New(strings.Join(errs, ","))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateLinks assumes lines has links in markdown syntax, and verifies that
|
||||||
|
// any relative links actually point to files that exist.
|
||||||
|
func updateLinks(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var out mungeLines
|
||||||
|
allErrs := []string{}
|
||||||
|
|
||||||
|
for lineNum, mline := range mlines {
|
||||||
|
if mline.preformatted {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if badMatch := badLinkRE.FindString(mline.data); badMatch != "" {
|
||||||
|
allErrs = append(allErrs,
|
||||||
|
fmt.Sprintf("On line %d: found backwards markdown link %q", lineNum, badMatch))
|
||||||
|
}
|
||||||
|
if !mline.link {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line, err := processLink(mline.data, filePath)
|
||||||
|
if err != nil {
|
||||||
|
var s = fmt.Sprintf("On line %d: %s", lineNum, err.Error())
|
||||||
|
err := errors.New(s)
|
||||||
|
allErrs = append(allErrs, err.Error())
|
||||||
|
}
|
||||||
|
ml := newMungeLine(line)
|
||||||
|
out = append(out, ml)
|
||||||
|
}
|
||||||
|
err := error(nil)
|
||||||
|
if len(allErrs) != 0 {
|
||||||
|
err = fmt.Errorf("%s", strings.Join(allErrs, "\n"))
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to append together before path.Clean will be able to tell that stuff
|
||||||
|
// like ../docs isn't needed.
|
||||||
|
func cleanPath(dirPath, linkPath string) string {
|
||||||
|
clean := path.Clean(path.Join(dirPath, linkPath))
|
||||||
|
if strings.HasPrefix(clean, dirPath+"/") {
|
||||||
|
out := strings.TrimPrefix(clean, dirPath+"/")
|
||||||
|
if out != linkPath {
|
||||||
|
fmt.Printf("%s -> %s\n", linkPath, out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
return linkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPath(filePath, linkPath string) (newPath string, ok bool) {
|
||||||
|
dir := path.Dir(filePath)
|
||||||
|
absFilePrefixes := []string{
|
||||||
|
"/kubernetes/kubernetes/blob/master/",
|
||||||
|
"/kubernetes/kubernetes/tree/master/",
|
||||||
|
}
|
||||||
|
for _, prefix := range absFilePrefixes {
|
||||||
|
if strings.HasPrefix(linkPath, prefix) {
|
||||||
|
linkPath = strings.TrimPrefix(linkPath, prefix)
|
||||||
|
// Now linkPath is relative to the root of the repo. The below
|
||||||
|
// loop that adds ../ at the beginning of the path should find
|
||||||
|
// the right path.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(linkPath, "/") {
|
||||||
|
// These links might go to e.g. the github issues page, or a
|
||||||
|
// file at a particular revision, or another github project
|
||||||
|
// entirely.
|
||||||
|
return linkPath, true
|
||||||
|
}
|
||||||
|
linkPath = cleanPath(dir, linkPath)
|
||||||
|
|
||||||
|
// Fast exit if the link is already correct.
|
||||||
|
if info, err := os.Stat(path.Join(dir, linkPath)); err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
return linkPath + "/", true
|
||||||
|
}
|
||||||
|
return linkPath, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for strings.HasPrefix(linkPath, "../") {
|
||||||
|
linkPath = strings.TrimPrefix(linkPath, "../")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix - vs _ automatically
|
||||||
|
nameMungers := []func(string) string{
|
||||||
|
func(s string) string { return s },
|
||||||
|
func(s string) string { return strings.Replace(s, "-", "_", -1) },
|
||||||
|
func(s string) string { return strings.Replace(s, "_", "-", -1) },
|
||||||
|
}
|
||||||
|
// Fix being moved into/out of admin (replace "admin" with directory
|
||||||
|
// you're doing mass movements to/from).
|
||||||
|
pathMungers := []func(string) string{
|
||||||
|
func(s string) string { return s },
|
||||||
|
func(s string) string { return path.Join("admin", s) },
|
||||||
|
func(s string) string { return strings.TrimPrefix(s, "admin/") },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, namer := range nameMungers {
|
||||||
|
for _, pather := range pathMungers {
|
||||||
|
newPath = pather(namer(linkPath))
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
// The file must exist.
|
||||||
|
target := path.Join(dir, newPath)
|
||||||
|
if info, err := os.Stat(target); err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
return newPath + "/", true
|
||||||
|
}
|
||||||
|
return cleanPath(dir, newPath), true
|
||||||
|
}
|
||||||
|
newPath = path.Join("..", newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return linkPath, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Printf
|
||||||
|
|
||||||
|
func TestBadLinks(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
}{
|
||||||
|
{"[NOTREADME](https://github.com/kubernetes/kubernetes/tree/master/NOTREADME.md)"},
|
||||||
|
{"[NOTREADME](https://github.com/kubernetes/kubernetes/tree/master/docs/NOTREADME.md)"},
|
||||||
|
{"[NOTREADME](../NOTREADME.md)"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
_, err := updateLinks("filename.md", in)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestGoodLinks(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"[README](https://lwn.net)",
|
||||||
|
"[README](https://lwn.net)"},
|
||||||
|
// _ to -
|
||||||
|
{"[README](https://github.com/kubernetes/kubernetes/tree/master/cmd/mungedocs/testdata/test_dashes.md)",
|
||||||
|
"[README](../../cmd/mungedocs/testdata/test-dashes.md)"},
|
||||||
|
// - to _
|
||||||
|
{"[README](../../cmd/mungedocs/testdata/test-underscores.md)",
|
||||||
|
"[README](../../cmd/mungedocs/testdata/test_underscores.md)"},
|
||||||
|
|
||||||
|
// Does this even make sense? i dunno
|
||||||
|
{"[README](/docs/README.md)",
|
||||||
|
"[README](https://github.com/docs/README.md)"},
|
||||||
|
{"[README](/kubernetes/kubernetes/tree/master/cmd/mungedocs/testdata/README.md)",
|
||||||
|
"[README](../../cmd/mungedocs/testdata/README.md)"},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := updateLinks("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Errorf("case[%d]: expected %q got %q", i, c.expected, actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
verbose = flag.Bool("verbose", false, "On verification failure, emit pre-munge and post-munge versions.")
|
||||||
|
verify = flag.Bool("verify", false, "Exit with status 1 if files would have needed changes but do not change.")
|
||||||
|
norecurse = flag.Bool("norecurse", false, "Only process the files of --root-dir.")
|
||||||
|
upstream = flag.String("upstream", "upstream", "The name of the upstream Git remote to pull from")
|
||||||
|
rootDir = flag.String("root-dir", "", "Root directory containing documents to be processed.")
|
||||||
|
// "repo-root" seems like a dumb name, this is the relative path (from rootDir) to get to the repoRoot
|
||||||
|
relRoot = flag.String("repo-root", "..", `Appended to --root-dir to get the repository root.
|
||||||
|
It's done this way so that generally you just have to set --root-dir.
|
||||||
|
Examples:
|
||||||
|
* --root-dir=docs/ --repo-root=.. means the repository root is ./
|
||||||
|
* --root-dir=/usr/local/long/path/repo/docs/ --repo-root=.. means the repository root is /usr/local/long/path/repo/
|
||||||
|
* --root-dir=/usr/local/long/path/repo/docs/admin --repo-root=../.. means the repository root is /usr/local/long/path/repo/`)
|
||||||
|
skipMunges = flag.String("skip-munges", "", "Comma-separated list of munges to *not* run. Available munges are: "+availableMungeList)
|
||||||
|
repoRoot string
|
||||||
|
|
||||||
|
ErrChangesNeeded = errors.New("mungedocs: changes required")
|
||||||
|
|
||||||
|
// All of the munge operations to perform.
|
||||||
|
// TODO: allow selection from command line. (e.g., just check links in the examples directory.)
|
||||||
|
allMunges = []munge{
|
||||||
|
// Simple "check something" functions must run first.
|
||||||
|
{"preformat-balance", checkPreformatBalance},
|
||||||
|
// Functions which modify state.
|
||||||
|
{"remove-whitespace", updateWhitespace},
|
||||||
|
{"table-of-contents", updateTOC},
|
||||||
|
{"md-links", updateLinks},
|
||||||
|
{"blank-lines-surround-preformatted", updatePreformatted},
|
||||||
|
{"header-lines", updateHeaderLines},
|
||||||
|
{"analytics", updateAnalytics},
|
||||||
|
{"kubectl-dash-f", updateKubectlFileTargets},
|
||||||
|
{"sync-examples", syncExamples},
|
||||||
|
}
|
||||||
|
availableMungeList = func() string {
|
||||||
|
names := []string{}
|
||||||
|
for _, m := range allMunges {
|
||||||
|
names = append(names, m.name)
|
||||||
|
}
|
||||||
|
return strings.Join(names, ",")
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
// a munge processes a document, returning an updated document xor an error.
|
||||||
|
// The fn is NOT allowed to mutate 'before', if changes are needed it must copy
|
||||||
|
// data into a new byte array and return that.
|
||||||
|
type munge struct {
|
||||||
|
name string
|
||||||
|
fn func(filePath string, mlines mungeLines) (after mungeLines, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileProcessor struct {
|
||||||
|
// Which munge functions should we call?
|
||||||
|
munges []munge
|
||||||
|
|
||||||
|
// Are we allowed to make changes?
|
||||||
|
verifyOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either change a file or verify that it needs no changes (according to modify argument)
|
||||||
|
func (f fileProcessor) visit(path string) error {
|
||||||
|
if !strings.HasSuffix(path, ".md") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mungeLines := getMungeLines(string(fileBytes))
|
||||||
|
|
||||||
|
modificationsMade := false
|
||||||
|
errFound := false
|
||||||
|
filePrinted := false
|
||||||
|
for _, munge := range f.munges {
|
||||||
|
after, err := munge.fn(path, mungeLines)
|
||||||
|
if err != nil || !after.Equal(mungeLines) {
|
||||||
|
if !filePrinted {
|
||||||
|
fmt.Printf("%s\n----\n", path)
|
||||||
|
filePrinted = true
|
||||||
|
}
|
||||||
|
fmt.Printf("%s:\n", munge.name)
|
||||||
|
if *verbose {
|
||||||
|
if len(mungeLines) <= 20 {
|
||||||
|
fmt.Printf("INPUT: <<<%v>>>\n", mungeLines)
|
||||||
|
fmt.Printf("MUNGED: <<<%v>>>\n", after)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("not printing failed chunk: too many lines\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
errFound = true
|
||||||
|
} else {
|
||||||
|
fmt.Println("contents were modified")
|
||||||
|
modificationsMade = true
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
mungeLines = after
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write out new file with any changes.
|
||||||
|
if modificationsMade {
|
||||||
|
if f.verifyOnly {
|
||||||
|
// We're not allowed to make changes.
|
||||||
|
return ErrChangesNeeded
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(path, mungeLines.Bytes(), 0644)
|
||||||
|
}
|
||||||
|
if errFound {
|
||||||
|
return ErrChangesNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWalkFunc(fp *fileProcessor, changesNeeded *bool) filepath.WalkFunc {
|
||||||
|
return func(path string, info os.FileInfo, err error) error {
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path != *rootDir && stat.IsDir() && *norecurse {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
if err := fp.visit(path); err != nil {
|
||||||
|
*changesNeeded = true
|
||||||
|
if err != ErrChangesNeeded {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wantedMunges() (filtered []munge) {
|
||||||
|
skipList := strings.Split(*skipMunges, ",")
|
||||||
|
skipped := map[string]bool{}
|
||||||
|
for _, m := range skipList {
|
||||||
|
if len(m) > 0 {
|
||||||
|
skipped[m] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range allMunges {
|
||||||
|
if !skipped[m.name] {
|
||||||
|
filtered = append(filtered, m)
|
||||||
|
} else {
|
||||||
|
// Remove from the map so we can verify that everything
|
||||||
|
// requested was in fact valid.
|
||||||
|
delete(skipped, m.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(skipped) != 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: requested to skip %v, but these are not valid munges. (valid: %v)\n", skipped, availableMungeList)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *rootDir == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: %s [--help] [--verify] [--norecurse] --root-dir [--skip-munges=<skip list>] [--upstream=<git remote>] <docs root>\n", flag.Arg(0))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repoRoot = path.Join(*rootDir, *relRoot)
|
||||||
|
repoRoot, err = filepath.Abs(repoRoot)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := fileProcessor{
|
||||||
|
munges: wantedMunges(),
|
||||||
|
verifyOnly: *verify,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each markdown file under source docs root, process the doc.
|
||||||
|
// - If any error occurs: exit with failure (exit >1).
|
||||||
|
// - If verify is true: exit 0 if no changes needed, exit 1 if changes
|
||||||
|
// needed.
|
||||||
|
// - If verify is false: exit 0 if changes successfully made or no
|
||||||
|
// changes needed, exit 1 if manual changes are needed.
|
||||||
|
var changesNeeded bool
|
||||||
|
|
||||||
|
err = filepath.Walk(*rootDir, newWalkFunc(&fp, &changesNeeded))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if changesNeeded {
|
||||||
|
if *verify {
|
||||||
|
fmt.Fprintf(os.Stderr, "FAIL: changes needed but not made due to --verify\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "FAIL: some manual changes are still required.\n")
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Blocks of ``` need to have blank lines on both sides or they don't look
|
||||||
|
// right in HTML.
|
||||||
|
func updatePreformatted(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var out mungeLines
|
||||||
|
inpreformat := false
|
||||||
|
for i, mline := range mlines {
|
||||||
|
if !inpreformat && mline.preformatted {
|
||||||
|
if i == 0 || out[len(out)-1].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
// start of a preformat block
|
||||||
|
inpreformat = true
|
||||||
|
}
|
||||||
|
out = append(out, mline)
|
||||||
|
if inpreformat && !mline.preformatted {
|
||||||
|
if i >= len(mlines)-2 || mlines[i+1].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
inpreformat = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file ends on a preformatted line, there must have been an imbalance.
|
||||||
|
func checkPreformatBalance(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
if len(mlines) > 0 && mlines[len(mlines)-1].preformatted {
|
||||||
|
return mlines, fmt.Errorf("unbalanced triple backtick delimiters")
|
||||||
|
}
|
||||||
|
return mlines, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPreformatted(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{
|
||||||
|
"```\nbob\n```",
|
||||||
|
"\n```\nbob\n```\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"```\nbob\n```\n```\nnotbob\n```\n",
|
||||||
|
"\n```\nbob\n```\n\n```\nnotbob\n```\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"```bob```\n",
|
||||||
|
"```bob```\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
" ```\n bob\n ```",
|
||||||
|
"\n ```\n bob\n ```\n\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := updatePreformatted("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Errorf("case[%d]: expected %q got %q", i, c.expected, actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreformattedImbalance(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"", true},
|
||||||
|
{"```\nin\n```", true},
|
||||||
|
{"```\nin\n```\nout", true},
|
||||||
|
{"```", false},
|
||||||
|
{"```\nin\n```\nout\n```", false},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
out, err := checkPreformatBalance("filename.md", in)
|
||||||
|
if err != nil && c.ok {
|
||||||
|
t.Errorf("case[%d]: expected success", i)
|
||||||
|
}
|
||||||
|
if err == nil && !c.ok {
|
||||||
|
t.Errorf("case[%d]: expected failure", i)
|
||||||
|
}
|
||||||
|
// Even in case of misformat, return all the text,
|
||||||
|
// so that the user's work is not lost.
|
||||||
|
if !equalMungeLines(out, in) {
|
||||||
|
t.Errorf("case[%d]: expected munged text to be identical to input text", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalMungeLines(a, b mungeLines) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
some text
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
some text
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
some text
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
some text
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tocMungeTag = "GENERATED_TOC"
|
||||||
|
|
||||||
|
var r = regexp.MustCompile("[^A-Za-z0-9-]")
|
||||||
|
|
||||||
|
// inserts/updates a table of contents in markdown file.
|
||||||
|
//
|
||||||
|
// First, builds a ToC.
|
||||||
|
// Then, finds the magic macro block tags and replaces anything between those with
|
||||||
|
// the ToC, thereby updating any previously inserted ToC.
|
||||||
|
//
|
||||||
|
// TODO(erictune): put this in own package with tests
|
||||||
|
func updateTOC(filePath string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
toc := buildTOC(mlines)
|
||||||
|
updatedMarkdown, err := updateMacroBlock(mlines, tocMungeTag, toc)
|
||||||
|
if err != nil {
|
||||||
|
return mlines, err
|
||||||
|
}
|
||||||
|
return updatedMarkdown, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// builds table of contents for markdown file
|
||||||
|
//
|
||||||
|
// First scans for all section headers (lines that begin with "#" but not within code quotes)
|
||||||
|
// and builds a table of contents from those. Assumes bookmarks for those will be
|
||||||
|
// like #each-word-in-heading-in-lowercases-with-dashes-instead-of-spaces.
|
||||||
|
// builds the ToC.
|
||||||
|
|
||||||
|
func buildTOC(mlines mungeLines) mungeLines {
|
||||||
|
var out mungeLines
|
||||||
|
bookmarks := map[string]int{}
|
||||||
|
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted || !mline.header {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Add a blank line after the munge start tag
|
||||||
|
if len(out) == 0 {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
line := mline.data
|
||||||
|
noSharps := strings.TrimLeft(line, "#")
|
||||||
|
numSharps := len(line) - len(noSharps)
|
||||||
|
heading := strings.Trim(noSharps, " \n")
|
||||||
|
if numSharps > 0 {
|
||||||
|
indent := strings.Repeat(" ", numSharps-1)
|
||||||
|
bookmark := strings.Replace(strings.ToLower(heading), " ", "-", -1)
|
||||||
|
// remove symbols (except for -) in bookmarks
|
||||||
|
bookmark = r.ReplaceAllString(bookmark, "")
|
||||||
|
// Incremental counter for duplicate bookmarks
|
||||||
|
next := bookmarks[bookmark]
|
||||||
|
bookmarks[bookmark] = next + 1
|
||||||
|
if next > 0 {
|
||||||
|
bookmark = fmt.Sprintf("%s-%d", bookmark, next)
|
||||||
|
}
|
||||||
|
tocLine := fmt.Sprintf("%s- [%s](#%s)", indent, heading, bookmark)
|
||||||
|
out = append(out, newMungeLine(tocLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Add a blank line before the munge end tag
|
||||||
|
if len(out) != 0 {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_buildTOC(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"Lorem ipsum\ndolor sit amet\n", ""},
|
||||||
|
{
|
||||||
|
"# Title\nLorem ipsum \n## Section Heading\ndolor sit amet\n",
|
||||||
|
"\n- [Title](#title)\n - [Section Heading](#section-heading)\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"# Title\nLorem ipsum \n## Section Heading\ndolor sit amet\n```bash\n#!/bin/sh\n```",
|
||||||
|
"\n- [Title](#title)\n - [Section Heading](#section-heading)\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"# Title\nLorem ipsum \n## Section Heading\n### Ok, why doesn't this work? ...add 4 *more* `symbols`!\ndolor sit amet\n",
|
||||||
|
"\n- [Title](#title)\n - [Section Heading](#section-heading)\n - [Ok, why doesn't this work? ...add 4 *more* `symbols`!](#ok-why-doesnt-this-work-add-4-more-symbols)\n\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual := buildTOC(in)
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
t.Errorf("Case[%d] Expected TOC '%v' but got '%v'", i, expected.String(), actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_updateTOC(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{
|
||||||
|
"Lorem ipsum\ndolor sit amet\n",
|
||||||
|
"Lorem ipsum\ndolor sit amet\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"# Title\nLorem ipsum \n**table of contents**\n<!-- BEGIN MUNGE: GENERATED_TOC -->\nold cruft\n<!-- END MUNGE: GENERATED_TOC -->\n## Section Heading\ndolor sit amet\n",
|
||||||
|
"# Title\nLorem ipsum \n**table of contents**\n<!-- BEGIN MUNGE: GENERATED_TOC -->\n\n- [Title](#title)\n - [Section Heading](#section-heading)\n\n<!-- END MUNGE: GENERATED_TOC -->\n## Section Heading\ndolor sit amet\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := updateTOC("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
t.Errorf("Expected TOC '%v' but got '%v'", expected.String(), actual.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replaces the text between matching "beginMark" and "endMark" within the
|
||||||
|
// document represented by "lines" with "insertThis".
|
||||||
|
//
|
||||||
|
// Delimiters should occupy own line.
|
||||||
|
// Returns copy of document with modifications.
|
||||||
|
func updateMacroBlock(mlines mungeLines, token string, insertThis mungeLines) (mungeLines, error) {
|
||||||
|
beginMark := beginMungeTag(token)
|
||||||
|
endMark := endMungeTag(token)
|
||||||
|
var out mungeLines
|
||||||
|
betweenBeginAndEnd := false
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted && !betweenBeginAndEnd {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := mline.data
|
||||||
|
if mline.beginTag && line == beginMark {
|
||||||
|
if betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("found second begin mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
betweenBeginAndEnd = true
|
||||||
|
out = append(out, mline)
|
||||||
|
} else if mline.endTag && line == endMark {
|
||||||
|
if !betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("found end mark without begin mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
betweenBeginAndEnd = false
|
||||||
|
out = append(out, insertThis...)
|
||||||
|
out = append(out, mline)
|
||||||
|
} else {
|
||||||
|
if !betweenBeginAndEnd {
|
||||||
|
out = append(out, mline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("never found closing end mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a document, represented as a slice of lines, has a line. Ignores
|
||||||
|
// leading and trailing space.
|
||||||
|
func hasLine(lines mungeLines, needle string) bool {
|
||||||
|
for _, mline := range lines {
|
||||||
|
haystack := strings.TrimSpace(mline.data)
|
||||||
|
if haystack == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMacroBlock(token string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
beginMark := beginMungeTag(token)
|
||||||
|
endMark := endMungeTag(token)
|
||||||
|
var out mungeLines
|
||||||
|
betweenBeginAndEnd := false
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := mline.data
|
||||||
|
if mline.beginTag && line == beginMark {
|
||||||
|
if betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("found second begin mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
betweenBeginAndEnd = true
|
||||||
|
} else if mline.endTag && line == endMark {
|
||||||
|
if !betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("found end mark without begin mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
betweenBeginAndEnd = false
|
||||||
|
} else {
|
||||||
|
if !betweenBeginAndEnd {
|
||||||
|
out = append(out, mline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if betweenBeginAndEnd {
|
||||||
|
return nil, fmt.Errorf("never found closing end mark while updating macro blocks")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a macro block to the beginning of a set of lines
|
||||||
|
func prependMacroBlock(token string, mlines mungeLines) mungeLines {
|
||||||
|
beginLine := newMungeLine(beginMungeTag(token))
|
||||||
|
endLine := newMungeLine(endMungeTag(token))
|
||||||
|
out := mungeLines{beginLine, endLine}
|
||||||
|
if len(mlines) > 0 && mlines[0].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
return append(out, mlines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a macro block to the end of a set of lines
|
||||||
|
func appendMacroBlock(mlines mungeLines, token string) mungeLines {
|
||||||
|
beginLine := newMungeLine(beginMungeTag(token))
|
||||||
|
endLine := newMungeLine(endMungeTag(token))
|
||||||
|
out := mlines
|
||||||
|
if len(mlines) > 0 && mlines[len(mlines)-1].data != "" {
|
||||||
|
out = append(out, blankMungeLine)
|
||||||
|
}
|
||||||
|
return append(out, beginLine, endLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a document, represented as a slice of lines, has a macro block.
|
||||||
|
func hasMacroBlock(lines mungeLines, token string) bool {
|
||||||
|
beginMark := beginMungeTag(token)
|
||||||
|
endMark := endMungeTag(token)
|
||||||
|
|
||||||
|
foundBegin := false
|
||||||
|
for _, mline := range lines {
|
||||||
|
if mline.preformatted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !mline.beginTag && !mline.endTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := mline.data
|
||||||
|
switch {
|
||||||
|
case !foundBegin && line == beginMark:
|
||||||
|
foundBegin = true
|
||||||
|
case foundBegin && line == endMark:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the canonical begin-tag for a given description. This does not
|
||||||
|
// include the trailing newline.
|
||||||
|
func beginMungeTag(desc string) string {
|
||||||
|
return fmt.Sprintf("<!-- BEGIN MUNGE: %s -->", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the canonical end-tag for a given description. This does not
|
||||||
|
// include the trailing newline.
|
||||||
|
func endMungeTag(desc string) string {
|
||||||
|
return fmt.Sprintf("<!-- END MUNGE: %s -->", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mungeLine struct {
|
||||||
|
data string
|
||||||
|
preformatted bool
|
||||||
|
header bool
|
||||||
|
link bool
|
||||||
|
beginTag bool
|
||||||
|
endTag bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type mungeLines []mungeLine
|
||||||
|
|
||||||
|
func (m1 mungeLines) Equal(m2 mungeLines) bool {
|
||||||
|
if len(m1) != len(m2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range m1 {
|
||||||
|
if m1[i].data != m2[i].data {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlines mungeLines) String() string {
|
||||||
|
slice := []string{}
|
||||||
|
for _, mline := range mlines {
|
||||||
|
slice = append(slice, mline.data)
|
||||||
|
}
|
||||||
|
s := strings.Join(slice, "\n")
|
||||||
|
// We need to tack on an extra newline at the end of the file
|
||||||
|
return s + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlines mungeLines) Bytes() []byte {
|
||||||
|
return []byte(mlines.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Finds all preformatted block start/stops.
|
||||||
|
preformatRE = regexp.MustCompile("^\\s*```")
|
||||||
|
notPreformatRE = regexp.MustCompile("^\\s*```.*```")
|
||||||
|
// Is this line a header?
|
||||||
|
mlHeaderRE = regexp.MustCompile(`^#`)
|
||||||
|
// Is there a link on this line?
|
||||||
|
mlLinkRE = regexp.MustCompile(`\[[^]]*\]\([^)]*\)`)
|
||||||
|
beginTagRE = regexp.MustCompile(`<!-- BEGIN MUNGE:`)
|
||||||
|
endTagRE = regexp.MustCompile(`<!-- END MUNGE:`)
|
||||||
|
|
||||||
|
blankMungeLine = newMungeLine("")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Does not set 'preformatted'
|
||||||
|
func newMungeLine(line string) mungeLine {
|
||||||
|
return mungeLine{
|
||||||
|
data: line,
|
||||||
|
header: mlHeaderRE.MatchString(line),
|
||||||
|
link: mlLinkRE.MatchString(line),
|
||||||
|
beginTag: beginTagRE.MatchString(line),
|
||||||
|
endTag: endTagRE.MatchString(line),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimRightSpace(in string) string {
|
||||||
|
return strings.TrimRightFunc(in, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits a document up into a slice of lines.
|
||||||
|
func splitLines(document string) []string {
|
||||||
|
lines := strings.Split(document, "\n")
|
||||||
|
// Skip trailing empty string from Split-ing
|
||||||
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMungeLines(in string) mungeLines {
|
||||||
|
var out mungeLines
|
||||||
|
preformatted := false
|
||||||
|
|
||||||
|
lines := splitLines(in)
|
||||||
|
// We indicate if any given line is inside a preformatted block or
|
||||||
|
// outside a preformatted block
|
||||||
|
for _, line := range lines {
|
||||||
|
if !preformatted {
|
||||||
|
if preformatRE.MatchString(line) && !notPreformatRE.MatchString(line) {
|
||||||
|
preformatted = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if preformatRE.MatchString(line) {
|
||||||
|
preformatted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ml := newMungeLine(line)
|
||||||
|
ml.preformatted = preformatted
|
||||||
|
out = append(out, ml)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// filePath is the file we are looking for
|
||||||
|
// inFile is the file where we found the link. So if we are processing
|
||||||
|
// /path/to/repoRoot/docs/admin/README.md and are looking for
|
||||||
|
// ../../file.json we can find that location.
|
||||||
|
// In many cases filePath and processingFile may be the same
|
||||||
|
func makeRepoRelative(filePath string, processingFile string) (string, error) {
|
||||||
|
if filePath, err := filepath.Rel(repoRoot, filePath); err == nil {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
cwd := path.Dir(processingFile)
|
||||||
|
return filepath.Rel(repoRoot, path.Join(cwd, filePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFileRelative(filePath string, processingFile string) (string, error) {
|
||||||
|
cwd := path.Dir(processingFile)
|
||||||
|
if filePath, err := filepath.Rel(cwd, filePath); err == nil {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
return filepath.Rel(cwd, path.Join(cwd, filePath))
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_updateMacroBlock(t *testing.T) {
|
||||||
|
token := "TOKEN"
|
||||||
|
BEGIN := beginMungeTag(token)
|
||||||
|
END := endMungeTag(token)
|
||||||
|
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"Lorem ipsum\ndolor sit amet\n",
|
||||||
|
"Lorem ipsum\ndolor sit amet\n"},
|
||||||
|
{"Lorem ipsum \n" + BEGIN + "\ndolor\n" + END + "\nsit amet\n",
|
||||||
|
"Lorem ipsum \n" + BEGIN + "\nfoo\n" + END + "\nsit amet\n"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.out)
|
||||||
|
actual, err := updateMacroBlock(in, token, getMungeLines("foo"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
t.Errorf("Expected '%v' but got '%v'", expected.String(), expected.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_updateMacroBlock_errors(t *testing.T) {
|
||||||
|
token := "TOKEN"
|
||||||
|
b := beginMungeTag(token)
|
||||||
|
e := endMungeTag(token)
|
||||||
|
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
}{
|
||||||
|
{b + "\n"},
|
||||||
|
{"blah\n" + b + "\nblah"},
|
||||||
|
{e + "\n"},
|
||||||
|
{"blah\n" + e + "\nblah\n"},
|
||||||
|
{e + "\n" + b},
|
||||||
|
{b + "\n" + e + "\n" + e},
|
||||||
|
{b + "\n" + b + "\n" + e},
|
||||||
|
{b + "\n" + b + "\n" + e + "\n" + e},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
_, err := updateMacroBlock(in, token, getMungeLines("foo"))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasLine(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
haystack string
|
||||||
|
needle string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"abc\ndef\nghi", "abc", true},
|
||||||
|
{" abc\ndef\nghi", "abc", true},
|
||||||
|
{"abc \ndef\nghi", "abc", true},
|
||||||
|
{"\n abc\ndef\nghi", "abc", true},
|
||||||
|
{"abc \n\ndef\nghi", "abc", true},
|
||||||
|
{"abc\ndef\nghi", "def", true},
|
||||||
|
{"abc\ndef\nghi", "ghi", true},
|
||||||
|
{"abc\ndef\nghi", "xyz", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.haystack)
|
||||||
|
if hasLine(in, c.needle) != c.expected {
|
||||||
|
t.Errorf("case[%d]: %q, expected %t, got %t", i, c.needle, c.expected, !c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasMacroBlock(t *testing.T) {
|
||||||
|
token := "<<<"
|
||||||
|
b := beginMungeTag(token)
|
||||||
|
e := endMungeTag(token)
|
||||||
|
cases := []struct {
|
||||||
|
lines []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{[]string{b, e}, true},
|
||||||
|
{[]string{b, "abc", e}, true},
|
||||||
|
{[]string{b, b, "abc", e}, true},
|
||||||
|
{[]string{b, "abc", e, e}, true},
|
||||||
|
{[]string{b, e, b, e}, true},
|
||||||
|
{[]string{b}, false},
|
||||||
|
{[]string{e}, false},
|
||||||
|
{[]string{b, "abc"}, false},
|
||||||
|
{[]string{"abc", e}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(strings.Join(c.lines, "\n"))
|
||||||
|
if hasMacroBlock(in, token) != c.expected {
|
||||||
|
t.Errorf("case[%d]: expected %t, got %t", i, c.expected, !c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendMacroBlock(t *testing.T) {
|
||||||
|
token := "<<<"
|
||||||
|
b := beginMungeTag(token)
|
||||||
|
e := endMungeTag(token)
|
||||||
|
cases := []struct {
|
||||||
|
in []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{[]string{}, []string{b, e}},
|
||||||
|
{[]string{"bob"}, []string{"bob", "", b, e}},
|
||||||
|
{[]string{b, e}, []string{b, e, "", b, e}},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(strings.Join(c.in, "\n"))
|
||||||
|
expected := getMungeLines(strings.Join(c.expected, "\n"))
|
||||||
|
out := appendMacroBlock(in, token)
|
||||||
|
if !out.Equal(expected) {
|
||||||
|
t.Errorf("Case[%d]: expected '%q' but got '%q'", i, expected.String(), out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrependMacroBlock(t *testing.T) {
|
||||||
|
token := "<<<"
|
||||||
|
b := beginMungeTag(token)
|
||||||
|
e := endMungeTag(token)
|
||||||
|
cases := []struct {
|
||||||
|
in []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{[]string{}, []string{b, e}},
|
||||||
|
{[]string{"bob"}, []string{b, e, "", "bob"}},
|
||||||
|
{[]string{b, e}, []string{b, e, "", b, e}},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(strings.Join(c.in, "\n"))
|
||||||
|
expected := getMungeLines(strings.Join(c.expected, "\n"))
|
||||||
|
out := prependMacroBlock(token, in)
|
||||||
|
if !out.Equal(expected) {
|
||||||
|
t.Errorf("Case[%d]: expected '%q' but got '%q'", i, expected.String(), out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Remove all trailing whitespace
|
||||||
|
func updateWhitespace(file string, mlines mungeLines) (mungeLines, error) {
|
||||||
|
var out mungeLines
|
||||||
|
for _, mline := range mlines {
|
||||||
|
if mline.preformatted {
|
||||||
|
out = append(out, mline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newline := trimRightSpace(mline.data)
|
||||||
|
out = append(out, newMungeLine(newline))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_updateWhiteSpace(t *testing.T) {
|
||||||
|
var cases = []struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"\n", "\n"},
|
||||||
|
{" \t \t \n", "\n"},
|
||||||
|
{"bob \t", "bob"},
|
||||||
|
{"```\n \n```\n", "```\n \n```\n"},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
in := getMungeLines(c.in)
|
||||||
|
expected := getMungeLines(c.expected)
|
||||||
|
actual, err := updateWhitespace("filename.md", in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
t.Errorf("Case[%d] Expected Whitespace '%v' but got '%v'", i, string(expected.Bytes()), string(actual.Bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: apps/v1beta1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: pod-quota-demo
|
|
||||||
spec:
|
|
||||||
replicas: 3
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: pod-quota-demo
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: pod-quota-demo
|
|
||||||
image: nginx
|
|
||||||
Reference in New Issue
Block a user