You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by li...@apache.org on 2026/09/15 07:29:42 UTC

(answer) branch dev updated (7f226dcd -> ace02c49)

This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a change to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git


    from 7f226dcd fix: lowercase the email before hashing the Gravatar URL
     new d787464a test: guard frontend behaviour a successful build does not demonstrate
     new 791b5f74 fix: parse built asset tags without depending on the emitted format
     new 88c5ef66 fix: render every entry stylesheet, not only the first
     new c7b37a15 style: iterate the rel attribute tokens without building a slice
     new 69fc3c96 test: assert every declared stylesheet is parsed, not just one
     new ace02c49 chore: align asset-check comments with the DOM-tolerant parser

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 go.mod                                          |  2 +-
 internal/controller/template_controller.go      | 75 +++++++++++++++++++-----
 internal/controller/template_controller_test.go | 78 +++++++++++++++++++++++++
 ui/template/header.html                         |  4 +-
 4 files changed, 143 insertions(+), 16 deletions(-)
 create mode 100644 internal/controller/template_controller_test.go


(answer) 03/06: fix: render every entry stylesheet, not only the first

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 88c5ef666df0749b2c1a24d464047d48430c6a58
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Sat Aug 1 19:44:13 2026 +0800

    fix: render every entry stylesheet, not only the first
    
    GetStyle returned a single stylesheet path because the previous build emitted
    exactly one. The current build emits two, so server-rendered pages loaded
    partially unstyled while every build step still reported success.
    
    The stylesheet is now a list, mirroring how script paths are already collected
    and prefixed, and the template renders one link per entry.
    
    This is the same assumption as the tag-shape one fixed earlier: the server
    encoded a property of one bundler's output, here that there is exactly one
    entry stylesheet.
    
    (cherry picked from commit d66e21b252752ef9d231b958e7452e66202ba0f3)
---
 internal/controller/template_controller.go      | 19 ++++++++++++-------
 internal/controller/template_controller_test.go |  4 ++++
 ui/template/header.html                         |  4 +++-
 3 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
index 8290d8b6..3c102166 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -57,8 +57,11 @@ import (
 var SiteUrl = ""
 
 type TemplateController struct {
-	scriptPath               []string
-	cssPath                  string
+	scriptPath []string
+	// cssPath lists every stylesheet the frontend build emits, in document
+	// order; a build that emits more than one entry stylesheet needs all of
+	// them, not just the first, or server-rendered pages come back unstyled.
+	cssPath                  []string
 	templateRenderController *templaterender.TemplateRenderController
 	siteInfoService          siteinfo_common.SiteInfoCommonService
 	eventQueueService        eventqueue.Service
@@ -85,7 +88,7 @@ func NewTemplateController(
 		questionService:          questionService,
 	}
 }
-func GetStyle() (script []string, css string) {
+func GetStyle() (script []string, css []string) {
 	file, err := ui.Build.ReadFile("build/index.html")
 	if err != nil {
 		return
@@ -131,9 +134,9 @@ func GetStyle() (script []string, css string) {
 					script = append(script, src)
 				}
 			case "link":
-				if css == "" && isStylesheet(n) {
+				if isStylesheet(n) {
 					if href, ok := attr(n, "href"); ok && href != "" {
-						css = href
+						css = append(css, href)
 					}
 				}
 			}
@@ -602,7 +605,7 @@ func (tc *TemplateController) Page404(ctx *gin.Context) {
 
 func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) {
 	prefix := ""
-	cssPath := ""
+	cssPath := make([]string, len(tc.cssPath))
 	scriptPath := make([]string, len(tc.scriptPath))
 
 	_ = plugin.CallCDN(func(fn plugin.CDN) error {
@@ -614,7 +617,9 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
 		if prefix[len(prefix)-1:] == "/" {
 			prefix = strings.TrimSuffix(prefix, "/")
 		}
-		cssPath = prefix + tc.cssPath
+		for i, path := range tc.cssPath {
+			cssPath[i] = prefix + path
+		}
 		for i, path := range tc.scriptPath {
 			scriptPath[i] = prefix + path
 		}
diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go
index 273b7df8..6b4b4b79 100644
--- a/internal/controller/template_controller_test.go
+++ b/internal/controller/template_controller_test.go
@@ -53,4 +53,8 @@ func TestGetStyleResolvesBuiltAssets(t *testing.T) {
 
 	require.NotEmpty(t, css,
 		"no stylesheet href parsed out of %s; server-rendered pages would load unstyled", builtIndexPath)
+	for i, href := range css {
+		require.NotEmpty(t, href,
+			"stylesheet href %d parsed out of %s is empty; server-rendered pages would load unstyled", i, builtIndexPath)
+	}
 }
diff --git a/ui/template/header.html b/ui/template/header.html
index d5d9a18a..f9f0468b 100644
--- a/ui/template/header.html
+++ b/ui/template/header.html
@@ -34,7 +34,9 @@
     <link rel="canonical" href="{{.siteinfo.Canonical}}" />
     <link rel="manifest" href="{{$.baseURL}}/manifest.json" />
     <link rel="search" type="application/opensearchdescription+xml" href="{{$.baseURL}}/opensearch.xml" title="{{.siteinfo.General.Name}}" />
-    <link href="{{.cssPath}}" rel="stylesheet" />
+    {{range $path := .cssPath}}
+    <link href="{{$path}}" rel="stylesheet" />
+    {{end}}
     <link href="{{$.baseURL}}/custom.css" rel="stylesheet" />
     <link
       rel="icon"


(answer) 01/06: test: guard frontend behaviour a successful build does not demonstrate

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit d787464a637c840b88828bfdc9550040da094a18
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Wed Aug 19 11:58:07 2026 +0800

    test: guard frontend behaviour a successful build does not demonstrate
    
    Two things the server depends on are invisible to the build. Both fail
    silently, so a green build and a working dev server actively disguise them.
    
    The server parses the script and stylesheet paths out of the built
    index.html and reuses them on every server-rendered page. That parse is
    coupled to the exact attribute set and attribute order the frontend build
    writes into those tags. A build that emits a different tag shape still
    succeeds, the dev server still works, the binary still compiles, and the
    pages simply render with no scripts and no stylesheet.
    TestGetStyleResolvesBuiltAssets asserts the parse still finds them.
    
    Languages other than the default one are loaded with a template-literal
    dynamic import through an alias that points outside the frontend root. A
    bundler that cannot enumerate that pattern still builds and still serves a
    working app; the resources never arrive, and only for non-default
    languages, so a smoke test in the default language misses it.
    check-locale-resolution.js bundles that same import with the project's own
    configuration, runs it, and requires two languages to resolve to distinct
    translated content.
    
    Both run through make check-ui. check-built-assets.sh --self-check
    confirms the asset check still fails on tag shapes the parser cannot read,
    so a check that quietly stopped asserting anything is distinguishable from
    a passing one.
    
    (cherry picked from commit 71cd9246fedbf6430c6a7373759fdf0be4029e4a, internal/controller/template_controller_test.go only)
---
 internal/controller/template_controller_test.go | 56 +++++++++++++++++++++++++
 1 file changed, 56 insertions(+)

diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go
new file mode 100644
index 00000000..273b7df8
--- /dev/null
+++ b/internal/controller/template_controller_test.go
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 controller
+
+import (
+	"testing"
+
+	"github.com/apache/answer/ui"
+	"github.com/stretchr/testify/require"
+)
+
+// GetStyle scrapes the script and stylesheet paths out of the built
+// index.html and every server-rendered page reuses them. The scrape is
+// coupled to the exact attribute order and attribute set that the frontend
+// build tool writes into those tags, and nothing in the system reports a
+// mismatch: the frontend build still succeeds, the dev server still works,
+// the binary still compiles, and the server-rendered pages simply come back
+// with no script tags and no stylesheet.
+//
+// Assert the coupling directly so a change to the emitted tag shape fails
+// here instead of shipping.
+func TestGetStyleResolvesBuiltAssets(t *testing.T) {
+	const builtIndexPath = "build/index.html"
+
+	if _, err := ui.Build.ReadFile(builtIndexPath); err != nil {
+		t.Skipf("no frontend build embedded at %s; build the frontend and re-run: %v", builtIndexPath, err)
+	}
+
+	scripts, css := GetStyle()
+
+	require.NotEmpty(t, scripts,
+		"no script sources parsed out of %s; server-rendered pages would load without any JavaScript", builtIndexPath)
+	for i, src := range scripts {
+		require.NotEmpty(t, src, "script source %d parsed out of %s is empty", i, builtIndexPath)
+	}
+
+	require.NotEmpty(t, css,
+		"no stylesheet href parsed out of %s; server-rendered pages would load unstyled", builtIndexPath)
+}


(answer) 04/06: style: iterate the rel attribute tokens without building a slice

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit c7b37a152b50c03836d533844c70be25a4c83ffe
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Sat Aug 1 20:40:39 2026 +0800

    style: iterate the rel attribute tokens without building a slice
    
    The linter configured for this repository flags the slice-building form,
    so make lint fails on it.
    
    (cherry picked from commit 1cfd5daf81bfe0823f497c27fea43297b15b18c9)
---
 internal/controller/template_controller.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
index 3c102166..4343738a 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -117,7 +117,7 @@ func GetStyle() (script []string, css []string) {
 		if !ok {
 			return false
 		}
-		for _, tok := range strings.Fields(rel) {
+		for tok := range strings.FieldsSeq(rel) {
 			if strings.EqualFold(tok, "stylesheet") {
 				return true
 			}


(answer) 05/06: test: assert every declared stylesheet is parsed, not just one

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 69fc3c96c3800f673f9ad1d2b0c7d32105c86b9f
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Sat Aug 1 21:09:52 2026 +0800

    test: assert every declared stylesheet is parsed, not just one
    
    Asserting that the parsed stylesheet list is non-empty leaves the exact
    regression this repository already hit uncovered: a parser that stops at
    the first stylesheet returns a one element list, satisfies every existing
    assertion, and silently drops the rest of the page's CSS.
    
    Count the declarations again by a cruder method than the parser uses and
    require the two to agree, so the parser has to be checked against
    something other than itself. The count is a lower bound: a build that
    quotes attributes differently drives it to zero and it stops constraining,
    which is why it supplements the shape-independent assertions rather than
    replacing them.
    
    Verified by reintroducing the truncation and watching this fail.
    
    (cherry picked from commit 9232cbd1fa7ef0a19e053474acf7c5468af43e74)
---
 internal/controller/template_controller_test.go | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go
index 6b4b4b79..74c0db0a 100644
--- a/internal/controller/template_controller_test.go
+++ b/internal/controller/template_controller_test.go
@@ -20,6 +20,7 @@
 package controller
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/apache/answer/ui"
@@ -39,7 +40,8 @@ import (
 func TestGetStyleResolvesBuiltAssets(t *testing.T) {
 	const builtIndexPath = "build/index.html"
 
-	if _, err := ui.Build.ReadFile(builtIndexPath); err != nil {
+	raw, err := ui.Build.ReadFile(builtIndexPath)
+	if err != nil {
 		t.Skipf("no frontend build embedded at %s; build the frontend and re-run: %v", builtIndexPath, err)
 	}
 
@@ -57,4 +59,20 @@ func TestGetStyleResolvesBuiltAssets(t *testing.T) {
 		require.NotEmpty(t, href,
 			"stylesheet href %d parsed out of %s is empty; server-rendered pages would load unstyled", i, builtIndexPath)
 	}
+
+	// Finding every stylesheet matters as much as finding one. The build emits
+	// more than a single entry stylesheet, and a parser that stopped at the
+	// first one would still satisfy every assertion above while half the page's
+	// CSS silently stopped loading. That regression has happened once already.
+	//
+	// Count them again by a deliberately different and cruder method than the
+	// parser uses, so the two have to agree. It is a lower bound: a build that
+	// quotes attributes differently drives this to zero and the comparison
+	// simply stops constraining, which is why it supplements the assertions
+	// above rather than replacing them.
+	declared := strings.Count(string(raw), `rel="stylesheet"`)
+	require.GreaterOrEqual(t, len(css), declared,
+		"%s declares at least %d stylesheets but only %d were parsed out of it; "+
+			"server-rendered pages would load missing part of their CSS",
+		builtIndexPath, declared, len(css))
 }


(answer) 02/06: fix: parse built asset tags without depending on the emitted format

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 791b5f743f08a78cb1608c580c3266f7ba8fe059
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Wed Aug 19 11:58:34 2026 +0800

    fix: parse built asset tags without depending on the emitted format
    
    GetStyle scraped index.html with regexes matching one exact tag shape:
    classic scripts with defer first, and stylesheet links with href before rel.
    Any bundler emitting a different shape returned nothing, and server-rendered
    pages would load with no JavaScript and no stylesheet while every build step
    still reported success.
    
    The tags are now read from the parsed document, so attribute order, attribute
    set and quoting no longer matter.
    
    header.html emits the scraped paths as script tags itself, and those were
    classic scripts. A module bundle loaded that way fails on its first import, so
    fixing only the parsing would have left server-rendered pages broken; the tag
    is now declared as a module.
    
    The self-check fixtures are replaced. The previous two asserted failure on
    module scripts and on rel-before-href, both of which the parser now accepts, so
    they would have inverted into false alarms. The replacements cover a stylesheet
    with no script, a script with no stylesheet, and an inline script with no src.
    
    golang.org/x/net moves to a direct requirement, matching its use here.
    
    (cherry picked from commit eab6f9d80c084d8345a4c82821a64aaef512cce5, go.mod and internal/controller/template_controller.go only)
---
 go.mod                                     |  2 +-
 internal/controller/template_controller.go | 60 +++++++++++++++++++++++++-----
 2 files changed, 52 insertions(+), 10 deletions(-)

diff --git a/go.mod b/go.mod
index 5787c8b1..7b68c180 100644
--- a/go.mod
+++ b/go.mod
@@ -64,6 +64,7 @@ require (
 	go.uber.org/mock v0.6.0
 	golang.org/x/crypto v0.53.0
 	golang.org/x/image v0.20.0
+	golang.org/x/net v0.56.0
 	golang.org/x/term v0.44.0
 	golang.org/x/text v0.39.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
@@ -170,7 +171,6 @@ require (
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/arch v0.10.0 // indirect
 	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
-	golang.org/x/net v0.56.0 // indirect
 	golang.org/x/sys v0.46.0 // indirect
 	golang.org/x/tools v0.47.0 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
index 31cc5152..8290d8b6 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -20,6 +20,7 @@
 package controller
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"html/template"
@@ -50,6 +51,7 @@ import (
 	"github.com/apache/answer/ui"
 	"github.com/gin-gonic/gin"
 	"github.com/segmentfault/pacman/log"
+	"golang.org/x/net/html"
 )
 
 var SiteUrl = ""
@@ -88,19 +90,59 @@ func GetStyle() (script []string, css string) {
 	if err != nil {
 		return
 	}
-	scriptRegexp := regexp.MustCompile(`<script defer="defer" src="([^"]*)"></script>`)
-	scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1)
-	for _, s := range scriptData {
-		if len(s) == 2 {
-			script = append(script, s[1])
+
+	// The frontend build tool controls attribute order, attribute set (e.g.
+	// module vs classic scripts), and quoting for the emitted tags, and that
+	// shape has already changed once. Walk the parsed DOM instead of matching
+	// a literal tag shape so the next bundler change fails a test instead of
+	// silently shipping pages with no JS or CSS.
+	doc, err := html.Parse(bytes.NewReader(file))
+	if err != nil {
+		return
+	}
+
+	attr := func(n *html.Node, key string) (string, bool) {
+		for _, a := range n.Attr {
+			if a.Key == key {
+				return a.Val, true
+			}
+		}
+		return "", false
+	}
+	isStylesheet := func(n *html.Node) bool {
+		rel, ok := attr(n, "rel")
+		if !ok {
+			return false
+		}
+		for _, tok := range strings.Fields(rel) {
+			if strings.EqualFold(tok, "stylesheet") {
+				return true
+			}
 		}
+		return false
 	}
 
-	cssRegexp := regexp.MustCompile(`<link href="(.*)" rel="stylesheet">`)
-	cssListData := cssRegexp.FindStringSubmatch(string(file))
-	if len(cssListData) == 2 {
-		css = cssListData[1]
+	var walk func(*html.Node)
+	walk = func(n *html.Node) {
+		if n.Type == html.ElementNode {
+			switch n.Data {
+			case "script":
+				if src, ok := attr(n, "src"); ok && src != "" {
+					script = append(script, src)
+				}
+			case "link":
+				if css == "" && isStylesheet(n) {
+					if href, ok := attr(n, "href"); ok && href != "" {
+						css = href
+					}
+				}
+			}
+		}
+		for c := n.FirstChild; c != nil; c = c.NextSibling {
+			walk(c)
+		}
 	}
+	walk(doc)
 	return
 }
 func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp {


(answer) 06/06: chore: align asset-check comments with the DOM-tolerant parser

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit ace02c499a0ccf64f05e99185d7e6dcfd4aead06
Author: Xan Torres <xa...@gmail.com>
AuthorDate: Wed Aug 19 11:59:30 2026 +0800

    chore: align asset-check comments with the DOM-tolerant parser
    
    The GetStyle comment framed the DOM walk around attribute order,
    attribute set, and quoting, the same properties the old regex parser
    depended on, without stating that the new parser ignores all of
    them. check-built-assets.sh described its self-check as failing when
    asset tags change shape, but the fixtures test a missing script or
    stylesheet tag, and the parser accepts any shape as long as the tag
    is present.
    
    Comments now state the actual constraint: tag shape does not affect
    parsing, and the guarding check fails when a build is present but a
    required script or stylesheet tag is missing from it.
    
    (cherry picked from commit 3bc12e80a659a08dbe72c840a60b25c8c0f90963, internal/controller/template_controller.go only)
---
 internal/controller/template_controller.go | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
index 4343738a..0f2f5b68 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -94,11 +94,11 @@ func GetStyle() (script []string, css []string) {
 		return
 	}
 
-	// The frontend build tool controls attribute order, attribute set (e.g.
-	// module vs classic scripts), and quoting for the emitted tags, and that
-	// shape has already changed once. Walk the parsed DOM instead of matching
-	// a literal tag shape so the next bundler change fails a test instead of
-	// silently shipping pages with no JS or CSS.
+	// Script and stylesheet tags are read from the parsed document, so
+	// attribute order, attribute set (module vs classic scripts), and
+	// quoting do not matter. That shape has already changed once; a
+	// bundler change that breaks it now fails the guarding test instead
+	// of silently shipping pages with no JS or CSS.
 	doc, err := html.Parse(bytes.NewReader(file))
 	if err != nil {
 		return