mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 08:50:23 +08:00
dedup feature rendering, align default limit, simplify CSS guard
This commit is contained in:
@@ -315,6 +315,11 @@ func (q Query) IsEmpty() bool {
|
||||
// MaxQueryLimit is the maximum allowed value for the limit parameter.
|
||||
const MaxQueryLimit = 100
|
||||
|
||||
// defaultQueryLimit is the assumed limit when a request omits it (InitFromContext)
|
||||
// and the fallback used by pagination math for internally-built queries that
|
||||
// leave Limit unset.
|
||||
const defaultQueryLimit = 10
|
||||
|
||||
// InitFromContext populates Query from HTTP query parameters and request
|
||||
// headers. It validates numeric/boolean inputs and returns an *APIError for
|
||||
// invalid client input (400) or a plain error for internal failures.
|
||||
@@ -326,7 +331,7 @@ func (searchQuery *Query) InitFromContext(reqCtx *fiber.Ctx) error {
|
||||
searchQuery.Filetype = strings.TrimSpace(reqCtx.Query("file"))
|
||||
searchQuery.Site = strings.TrimSpace(reqCtx.Query("site"))
|
||||
|
||||
limitRaw := reqCtx.Query("limit", "10")
|
||||
limitRaw := reqCtx.Query("limit", strconv.Itoa(defaultQueryLimit))
|
||||
limit, err := strconv.Atoi(limitRaw)
|
||||
if err != nil {
|
||||
return errInvalidLimit("limit must be an integer")
|
||||
|
||||
@@ -34,7 +34,7 @@ func RenderMarkdown(env *Envelope) []byte {
|
||||
fmt.Fprintf(&b, "-> %s\n\n", r.URL)
|
||||
}
|
||||
|
||||
renderMarkdownFeatures(&b, env.SerpFeatures, featureRenderOrderAfterResults())
|
||||
renderMarkdownFeatures(&b, env.SerpFeatures, featureRenderOrderAfterResults(env.SerpFeatures))
|
||||
|
||||
return []byte(b.String())
|
||||
}
|
||||
@@ -59,14 +59,9 @@ func RenderMarkdownImage(env *ImageEnvelope) []byte {
|
||||
}
|
||||
|
||||
func renderMarkdownFeatures(b *strings.Builder, features []SerpFeature, order []ResultType) {
|
||||
for _, featureType := range order {
|
||||
for _, feature := range features {
|
||||
if feature.Type != featureType {
|
||||
continue
|
||||
}
|
||||
renderMarkdownFeature(b, feature)
|
||||
}
|
||||
}
|
||||
forEachFeatureInOrder(features, order, func(feature SerpFeature) {
|
||||
renderMarkdownFeature(b, feature)
|
||||
})
|
||||
}
|
||||
|
||||
func renderMarkdownFeature(b *strings.Builder, feature SerpFeature) {
|
||||
|
||||
@@ -34,7 +34,7 @@ func RenderText(env *Envelope) []byte {
|
||||
fmt.Fprintf(&b, "URL: %s\n\n", r.URL)
|
||||
}
|
||||
|
||||
renderTextFeatures(&b, env.SerpFeatures, featureRenderOrderAfterResults())
|
||||
renderTextFeatures(&b, env.SerpFeatures, featureRenderOrderAfterResults(env.SerpFeatures))
|
||||
|
||||
return []byte(b.String())
|
||||
}
|
||||
@@ -76,12 +76,20 @@ func RenderNDJSONImage(env *ImageEnvelope) []byte {
|
||||
}
|
||||
|
||||
func renderTextFeatures(b *strings.Builder, features []SerpFeature, order []ResultType) {
|
||||
forEachFeatureInOrder(features, order, func(feature SerpFeature) {
|
||||
renderTextFeature(b, feature)
|
||||
})
|
||||
}
|
||||
|
||||
// forEachFeatureInOrder invokes render for every feature whose Type appears in
|
||||
// order, type by type. It is the single iteration shared by the text and
|
||||
// markdown renderers.
|
||||
func forEachFeatureInOrder(features []SerpFeature, order []ResultType, render func(SerpFeature)) {
|
||||
for _, featureType := range order {
|
||||
for _, feature := range features {
|
||||
if feature.Type != featureType {
|
||||
continue
|
||||
if feature.Type == featureType {
|
||||
render(feature)
|
||||
}
|
||||
renderTextFeature(b, feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,9 +159,12 @@ func featureRenderOrderBeforeResults() []ResultType {
|
||||
}
|
||||
|
||||
// featureRenderOrderAfterResults lists the feature sections rendered below the
|
||||
// results list (related searches and the module gallery), in fixed order.
|
||||
func featureRenderOrderAfterResults() []ResultType {
|
||||
return []ResultType{
|
||||
// results list (related searches and the module gallery), in fixed order. Any
|
||||
// feature type present in features but absent from both fixed orders is appended
|
||||
// at the end, so a newly added feature enum is never silently dropped from text
|
||||
// or markdown output.
|
||||
func featureRenderOrderAfterResults(features []SerpFeature) []ResultType {
|
||||
order := []ResultType{
|
||||
ResultTypeRelatedSearches,
|
||||
ResultTypeNews,
|
||||
ResultTypeVideo,
|
||||
@@ -166,6 +177,20 @@ func featureRenderOrderAfterResults() []ResultType {
|
||||
ResultTypeWeather,
|
||||
ResultTypeDictionary,
|
||||
}
|
||||
placed := make(map[ResultType]bool, len(order)+len(featureRenderOrderBeforeResults()))
|
||||
for _, t := range featureRenderOrderBeforeResults() {
|
||||
placed[t] = true
|
||||
}
|
||||
for _, t := range order {
|
||||
placed[t] = true
|
||||
}
|
||||
for _, feature := range features {
|
||||
if !placed[feature.Type] {
|
||||
order = append(order, feature.Type)
|
||||
placed[feature.Type] = true
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
func featureHeading(feature SerpFeature) string {
|
||||
|
||||
@@ -124,7 +124,7 @@ func (e *Envelope) Finalize(startedAt time.Time, q Query) {
|
||||
|
||||
limit := q.Limit
|
||||
if limit <= 0 {
|
||||
limit = 25
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
page := q.Start/limit + 1
|
||||
e.Pagination = Pagination{
|
||||
@@ -150,7 +150,7 @@ func (e *ImageEnvelope) Finalize(startedAt time.Time, q Query) {
|
||||
|
||||
limit := q.Limit
|
||||
if limit <= 0 {
|
||||
limit = 25
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
page := q.Start/limit + 1
|
||||
e.Pagination = Pagination{
|
||||
|
||||
@@ -150,3 +150,25 @@ func TestRenderersIncludeSerpFeatures(t *testing.T) {
|
||||
t.Fatalf("feature ndjson lines missing kind tag: %v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderFeaturesUnorderedTypeStillRenders guards against silently dropping a
|
||||
// feature whose Type is in neither render-order section (e.g. a newly added
|
||||
// enum value not yet placed). It must still appear in text and markdown output.
|
||||
func TestRenderFeaturesUnorderedTypeStillRenders(t *testing.T) {
|
||||
const unplaced ResultType = "experimental_module"
|
||||
env := NewEnvelope(Query{Text: "openserp"}, "req-1", time.Unix(0, 0), []string{"google"})
|
||||
env.SerpFeatures = append(env.SerpFeatures, SerpFeature{
|
||||
Type: unplaced,
|
||||
Text: "experimental feature body",
|
||||
})
|
||||
|
||||
text := string(RenderText(env))
|
||||
if !strings.Contains(text, "experimental feature body") {
|
||||
t.Fatalf("text output dropped an unordered feature type:\n%s", text)
|
||||
}
|
||||
|
||||
markdown := string(RenderMarkdown(env))
|
||||
if !strings.Contains(markdown, "experimental feature body") {
|
||||
t.Fatalf("markdown output dropped an unordered feature type:\n%s", markdown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,12 @@ func looksLikeCSS(text string) bool {
|
||||
return true
|
||||
}
|
||||
// CSS rule blocks look like "} .cls {" / "} #id {"; prose almost never does.
|
||||
return strings.Contains(text, "} .") || strings.Contains(text, "} #") || strings.Contains(text, "{ ") && strings.Count(text, ": ") > 20 && strings.Count(text, ";") > 20
|
||||
if strings.Contains(text, "} .") || strings.Contains(text, "} #") {
|
||||
return true
|
||||
}
|
||||
// A dense run of "prop: value;" declarations inside a "{ ... }" block is the
|
||||
// other tell for inlined stylesheet text.
|
||||
return strings.Contains(text, "{ ") && strings.Count(text, ": ") > 20 && strings.Count(text, ";") > 20
|
||||
}
|
||||
|
||||
func extractGoogleFeaturesFromPage(page *rod.Page) []core.SerpFeature {
|
||||
|
||||
Reference in New Issue
Block a user