Repository: gohugoio/hugo (language: go)

Issue:
resources/page: Escaping colon in permalinks erroneously throws error in some circumstances
I found this while attempting to document how to escape a colon.

With this front matter:

```text
---
title: p1
url: "/foo\\:bar/:slug/"
---
```

Hugo throws an error:

```text
Error: error building site: render: error expanding "/foo\\:bar/:slug/": permalink ill-formed
```

Although not related to this bug, if I had taken the time to weigh in on the original proposal I would have expressed my concerns about OS compatibility. You cannot use this feature on Windows because the path cannot contain a colon (restricted character). We will have to note in the documentation that you can't use this feature on Windows, but that won't really help for projects with multiple contributors where some use Windows. 

The existing tests pass on Windows because they don't write to disk. 

I think we should consider removing this feature.


Discussion / hints:
If we keep the feature I'll move this comment to a separate issue.

This currently works in front matter:

```text
---
title: p1
url: "/special\\::slug/"
---
```

This currently fails in the site configuration (colon omitted from file path):

```text
[permalinks]
s1 = "/special\\::slug/"
```

This test case covers both front matter and site configuration:

```go
func TestPermalinksWithEscapedColons(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
[permalinks.page]
s2 = "/c\\:d/:slug/"
-- content/s1/p1.md --
---
title: p1
url: "/a\\:b/:slug/"
---
-- content/s2/p2.md --
---
title: p2
---
-- layouts/_default/single.html --
{{ .Title }}
`

	b := hugolib.Test(t, files)

	b.AssertFileExists("public/a:b/p1/index.html", true)
	b.AssertFileExists("public/c:d/p2/index.html", true)
}
```



Fix patch:
diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go
index fe9d9f78d8b..ece10bb40e5 100644
--- a/resources/page/permalinks.go
+++ b/resources/page/permalinks.go
@@ -156,9 +156,6 @@ func init() {
 
 func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string, error), error) {
 	return l.patternCache.GetOrCreate(pattern, func() (func(Page) (string, error), error) {
-		if !l.validate(pattern) {
-			return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
-		}
 		var normalized bool
 		pattern, normalized = l.normalizeEscapeSequencesIn(pattern)
 
@@ -234,37 +231,6 @@ type pageToPermaAttribute func(Page, string) (string, error)
 
 var attributeRegexp = regexp.MustCompile(`:\w+(\[.+?\])?`)
 
-// validate determines if a PathPattern is well-formed
-func (l PermalinkExpander) validate(pp string) bool {
-	if len(pp) == 0 {
-		return false
-	}
-	fragments := strings.Split(pp[1:], "/")
-	bail := false
-	for i := range fragments {
-		if bail {
-			return false
-		}
-		if len(fragments[i]) == 0 {
-			bail = true
-			continue
-		}
-
-		matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
-		if matches == nil {
-			continue
-		}
-
-		for _, match := range matches {
-			k := match[0][1:]
-			if _, ok := l.callback(k); !ok {
-				return false
-			}
-		}
-	}
-	return true
-}
-
 type permalinkExpandError struct {
 	pattern string
 	err     error
@@ -274,10 +240,7 @@ func (pee *permalinkExpandError) Error() string {
 	return fmt.Sprintf("error expanding %q: %s", pee.pattern, pee.err)
 }
 
-var (
-	errPermalinkIllFormed        = errors.New("permalink ill-formed")
-	errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
-)
+var errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
 
 func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) {
 	// a Page contains a Node which provides a field Date, time.Time


Test patch:
diff --git a/resources/page/permalinks_integration_test.go b/resources/page/permalinks_integration_test.go
index 2b9e878b168..4188c70ca0c 100644
--- a/resources/page/permalinks_integration_test.go
+++ b/resources/page/permalinks_integration_test.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/bep/logg"
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/htesting"
 	"github.com/gohugoio/hugo/hugolib"
 )
 
@@ -232,3 +233,38 @@ slug: custom-recipe-2
 	b.AssertFileContent("public/delicious-recipe/recipe-1/index.html", "Single|page|/delicious-recipe/recipe-1/")
 	b.AssertFileContent("public/delicious-recipe/custom-recipe-2/index.html", "Single|page|/delicious-recipe/custom-recipe-2/")
 }
+
+// Issue 12948.
+func TestPermalinksWithEscapedColons(t *testing.T) {
+	t.Parallel()
+
+	if htesting.IsWindows() {
+		t.Skip("Windows does not support colons in paths")
+	}
+
+	files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+[permalinks.page]
+s2 = "/c\\:d/:slug/"
+-- content/s1/p1.md --
+---
+title: p1
+url: "/a\\:b/:slug/"
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/_default/single.html --
+{{ .Title }}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileExists("public/a:b/p1/index.html", true)
+
+	// The above URL comes from the URL front matter field where everything is allowed.
+	// We strip colons from paths constructed by Hugo (they are not supported on Windows).
+	b.AssertFileExists("public/cd/p2/index.html", true)
+}