Move mungedocs tool to the docs repo. (#5178)
* Remove seemingly errant file * Move mungedocs tool to docs repo * Bump go version to 1.8.3reviewable/pr5189/r1
parent
1245055dfc
commit
618c212c32
|
@ -1,6 +1,6 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.8.1
|
||||
- 1.8.3
|
||||
|
||||
# Don't want default ./... here:
|
||||
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
|
||||
- go get -t -v k8s.io/md-check
|
||||
|
||||
# (3) Fetch mungedocs
|
||||
- go get -v k8s.io/kubernetes/cmd/mungedocs
|
||||
# (3) Build mungedocs
|
||||
- go install ./_tools/mungedocs
|
||||
|
||||
script:
|
||||
- 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)
|
||||
|
||||
|
||||
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/cmd/mungedocs/README.md?pixel)]()
|
|
@ -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 = "[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/"
|
||||
|
||||
func updateAnalytics(fileName string, mlines mungeLines) (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" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n"},
|
||||
{
|
||||
"aoeu" + "\n" + "\n" + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()",
|
||||
"aoeu" + "\n" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n"},
|
||||
{
|
||||
"aoeu" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n",
|
||||
"aoeu" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n"},
|
||||
{
|
||||
"aoeu" + "\n" + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n",
|
||||
"aoeu" + "\n" + "\n" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n"},
|
||||
{
|
||||
"prefix" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e +
|
||||
"\n" + "suffix",
|
||||
"prefix" + "\n" + "suffix" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n"},
|
||||
{
|
||||
"aoeu" + "\n" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\n" +
|
||||
e + "\n",
|
||||
"aoeu" + "\n" + "\n" + "\n" +
|
||||
b + "\n" +
|
||||
"[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/path/to/file-name.md?pixel)]()" + "\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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
some text
|
|
@ -0,0 +1 @@
|
|||
some text
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
|
@ -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
|
Loading…
Reference in New Issue