jekyll-client-search 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
4
-
5
3
  module Jekyll
6
4
  module ClientSearch
7
5
  # Liquid tag that renders a compact live-search dropdown suitable for
@@ -15,27 +13,22 @@ module Jekyll
15
13
  #
16
14
  # When client_search is disabled the tag renders nothing.
17
15
  class DropdownTag < Liquid::Tag
16
+ include ScriptTags
17
+
18
18
  SYNTAX = /\A(max:(\d+))?\s*(scripts_only|no_scripts)?\z/
19
19
 
20
20
  def initialize(tag_name, markup, tokens)
21
21
  super
22
+ # No validation here: `{% comment %}` still parses nested tags, so
23
+ # `initialize` runs on commented-out tags. Validate in `render` —
24
+ # a bad tag inside a comment must not break the build.
22
25
  @markup = markup.to_s.strip
23
- unless (match = @markup.match(SYNTAX))
24
- raise Liquid::SyntaxError,
25
- "search_dropdown: invalid syntax. Use {% search_dropdown %}, " \
26
- "{% search_dropdown max:10 %}, {% search_dropdown scripts_only %}, " \
27
- "or {% search_dropdown no_scripts %}"
28
- end
29
-
30
- @max_items = match[2].to_i if match[2]
31
- raise Liquid::SyntaxError, "search_dropdown: max must be greater than zero" if @max_items && @max_items < 1
32
-
33
- @mode = match[3] || "full"
34
26
  @input_id = "cs-dropdown-input-#{object_id}"
35
27
  @results_id = "cs-dropdown-results-#{object_id}"
36
28
  end
37
29
 
38
30
  def render(context)
31
+ validate_markup!
39
32
  site = context.registers[:site]
40
33
  return "" unless enabled?(site)
41
34
 
@@ -47,6 +40,23 @@ module Jekyll
47
40
 
48
41
  private
49
42
 
43
+ def validate_markup!
44
+ return if @validated
45
+
46
+ @validated = true
47
+ unless (match = @markup.match(SYNTAX))
48
+ raise Liquid::SyntaxError,
49
+ "search_dropdown: invalid syntax. Use {% search_dropdown %}, " \
50
+ "{% search_dropdown max:10 %}, {% search_dropdown scripts_only %}, " \
51
+ "or {% search_dropdown no_scripts %}"
52
+ end
53
+
54
+ @max_items = match[2].to_i if match[2]
55
+ raise Liquid::SyntaxError, "search_dropdown: max must be greater than zero" if @max_items && @max_items < 1
56
+
57
+ @mode = match[3] || "full"
58
+ end
59
+
50
60
  def enabled?(site)
51
61
  return false if site.nil?
52
62
 
@@ -62,7 +72,7 @@ module Jekyll
62
72
  form_html = build_form(max)
63
73
  return form_html if @mode == "no_scripts"
64
74
 
65
- scripts = build_scripts(configuration, prefix)
75
+ scripts = script_tags(configuration, prefix, "client-search-dropdown.js")
66
76
  return scripts if @mode == "scripts_only"
67
77
 
68
78
  "#{form_html}\n#{scripts}"
@@ -87,33 +97,6 @@ module Jekyll
87
97
  </div>
88
98
  HTML
89
99
  end
90
-
91
- def build_scripts(configuration, prefix)
92
- scripts = []
93
- engine = engine_script(configuration)
94
- scripts << engine if engine
95
- scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
96
- scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
97
- scripts << "<script src=\"#{prefix}/assets/client-search-dropdown.js\"></script>"
98
- scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
99
- scripts.map { |script| " #{script}" }.join("\n")
100
- end
101
-
102
- def engine_script(configuration)
103
- url = configuration.engine_url
104
- return nil unless url
105
-
106
- attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
107
- if configuration.engine_crossorigin
108
- crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
109
- attrs << "crossorigin=\"#{crossorigin}\""
110
- end
111
- if configuration.engine_sri
112
- integrity = CGI.escapeHTML(configuration.engine_sri)
113
- attrs << "integrity=\"#{integrity}\""
114
- end
115
- "<script #{attrs.join(' ')}></script>"
116
- end
117
100
  end
118
101
  end
119
102
  end
@@ -9,9 +9,25 @@ module Jekyll
9
9
  "document" => "title: none | text: ",
10
10
  "query" => "task: search result | query: "
11
11
  },
12
+ "embeddinggemma-2" => {
13
+ "document" => "title: none | text: ",
14
+ "query" => "task: search result | query: "
15
+ },
12
16
  "nomic-embed-text" => {
13
17
  "document" => "search_document: ",
14
18
  "query" => "search_query: "
19
+ },
20
+ "voyage-4-nano" => {
21
+ "document" => "Represent the document for retrieval: ",
22
+ "query" => "Represent the query for retrieving supporting documents: "
23
+ },
24
+ "qwen3-embedding" => {
25
+ "document" => "",
26
+ "query" => "Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: "
27
+ },
28
+ "bge-large" => {
29
+ "document" => "",
30
+ "query" => "Represent this sentence for searching relevant passages: "
15
31
  }
16
32
  }.freeze
17
33
 
@@ -44,43 +60,63 @@ module Jekyll
44
60
  configured.nil? ? engine == "semantic" : configured == true
45
61
  end
46
62
 
63
+ # Document prefix for the index model (semantic.index_model).
47
64
  def embedding_document_prefix
48
- configured_prefix("document_prefix", "document")
65
+ configured_prefix("document", model: embedding_index_model, override: @semantic.document_prefix)
49
66
  end
50
67
 
68
+ # Query prefix for the index model — must match the browser query
69
+ # embedder's vector space.
51
70
  def embedding_query_prefix
52
- configured_prefix("query_prefix", "query")
71
+ configured_prefix("query", model: embedding_index_model, override: @semantic.query_prefix)
72
+ end
73
+
74
+ # Document prefix for the related-analysis model (related.index_model).
75
+ def embedding_related_document_prefix
76
+ configured_prefix("document", model: embedding_related_model)
53
77
  end
54
78
 
55
79
  def embedding_identity
80
+ embedding_identity_for(model: embedding_index_model,
81
+ document_prefix: embedding_document_prefix,
82
+ query_prefix: embedding_query_prefix)
83
+ end
84
+
85
+ def embedding_related_identity
86
+ embedding_identity_for(model: embedding_related_model,
87
+ document_prefix: embedding_related_document_prefix,
88
+ query_prefix: configured_prefix("query", model: embedding_related_model))
89
+ end
90
+
91
+ private
92
+
93
+ def embedding_identity_for(model:, document_prefix:, query_prefix:)
56
94
  {
57
95
  "provider" => "ollama",
58
- "model" => embedding_model,
96
+ "model" => model,
59
97
  "base_url" => embedding_base_url,
60
- "document_prefix" => embedding_document_prefix,
61
- "query_prefix" => embedding_query_prefix,
98
+ "document_prefix" => document_prefix,
99
+ "query_prefix" => query_prefix,
62
100
  "schema" => 2
63
101
  }
64
102
  end
65
103
 
66
- private
104
+ def configured_prefix(role, model:, override: nil)
105
+ return override.to_s if override
67
106
 
68
- def configured_prefix(config_key, role)
69
107
  embedding = @values.fetch("embedding")
70
- return embedding.fetch(config_key).to_s if embedding.key?(config_key)
108
+ config_key = "#{role}_prefix"
109
+ return embedding[config_key].to_s if model == embedding_model && embedding.key?(config_key)
71
110
 
72
- model_name = embedding_model.split(":").first
73
- PREFIXES.fetch(model_name, {}).fetch(role, "")
111
+ PREFIXES.fetch(model.split(":").first, {}).fetch(role, "")
74
112
  end
75
113
 
76
114
  def positive_timeout(key)
77
115
  value = @values.fetch("embedding").fetch(key).to_f
78
- unless value.positive? && value.finite?
79
- raise Jekyll::Errors::FatalException,
80
- "embedding #{key} must be a finite value greater than zero"
81
- end
116
+ return value if value.positive? && value.finite?
82
117
 
83
- value
118
+ raise Jekyll::Errors::FatalException,
119
+ "embedding #{key} must be a finite value greater than zero"
84
120
  end
85
121
 
86
122
  def validate_embedding!
@@ -25,6 +25,7 @@ module Jekyll
25
25
 
26
26
  def add_generated_pages(site, documents, configuration)
27
27
  add_related_page(site, documents, configuration) if configuration.related_enabled?
28
+ documents.each { |document| document.delete("related_embedding") }
28
29
  remove_embeddings(documents) unless configuration.embedding_include_in_index?
29
30
  site.pages << SearchIndexPage.new(site, configuration.output, documents)
30
31
  site.pages << RuntimeConfigPage.new(site, configuration) if configuration.copy_runtime?
@@ -39,18 +40,60 @@ module Jekyll
39
40
  end
40
41
 
41
42
  def enhance_with_embeddings(site, documents, configuration)
42
- cache = IndexCache.new(site.source, embedding_identity: configuration.embedding_identity)
43
- adapter = build_embedding_adapter(configuration)
44
-
45
- documents.each { |entry| embed_document(entry, cache, adapter, configuration) }
46
- cache.prune(documents.map { |doc| doc["id"] })
47
- cache.save
43
+ embedding_jobs(configuration).each do |job|
44
+ cache = IndexCache.new(site.source, embedding_identity: job[:identity],
45
+ file_name: job[:cache_file])
46
+ adapter = build_embedding_adapter(configuration, model: job[:model])
47
+
48
+ documents.each { |entry| embed_document(entry, cache, adapter, configuration, job) }
49
+ cache.prune(documents.map { |doc| doc["id"] })
50
+ cache.save
51
+ end
48
52
  documents
49
53
  end
50
54
 
51
- def build_embedding_adapter(configuration)
55
+ # Each job embeds every document once per model into a target field.
56
+ # The index model writes "embedding" (kept when include_in_index); a
57
+ # divergent related model writes "related_embedding" (stripped before
58
+ # the index is emitted). Identical models share a single pass.
59
+ def embedding_jobs(configuration)
60
+ jobs = []
61
+ jobs << index_embedding_job(configuration) if configuration.embedding_include_in_index?
62
+ jobs.concat(related_embedding_jobs(configuration)) if configuration.related_enabled?
63
+ jobs
64
+ end
65
+
66
+ def index_embedding_job(configuration)
67
+ {
68
+ model: configuration.embedding_index_model,
69
+ field: "embedding",
70
+ document_prefix: configuration.embedding_document_prefix,
71
+ identity: configuration.embedding_identity,
72
+ cache_file: IndexCache::CACHE_FILE
73
+ }
74
+ end
75
+
76
+ def related_embedding_jobs(configuration)
77
+ return [related_embedding_job("embedding", IndexCache::CACHE_FILE, configuration)] unless
78
+ configuration.embedding_include_in_index?
79
+ return [] if configuration.embedding_related_model == configuration.embedding_index_model
80
+
81
+ [related_embedding_job("related_embedding", IndexCache::RELATED_CACHE_FILE, configuration)]
82
+ end
83
+
84
+ def related_embedding_job(field, cache_file, configuration)
85
+ {
86
+ model: configuration.embedding_related_model,
87
+ field: field,
88
+ document_prefix: configuration.embedding_related_document_prefix,
89
+ identity: configuration.embedding_related_identity,
90
+ cache_file: cache_file
91
+ }
92
+ end
93
+
94
+ def build_embedding_adapter(configuration, model: configuration.embedding_model)
52
95
  OllamaEmbeddingAdapter.new(
53
- model: configuration.embedding_model,
96
+ model: model,
54
97
  base_url: configuration.embedding_base_url,
55
98
  connect_timeout: configuration.embedding_connect_timeout,
56
99
  read_timeout: configuration.embedding_read_timeout
@@ -66,17 +109,13 @@ module Jekyll
66
109
  documents.each { |document| document.delete("embedding") }
67
110
  end
68
111
 
69
- def embed_document(document, cache, adapter, configuration)
70
- hash = IndexCache.content_hash(document)
71
- cached = cache.lookup(document["id"], hash)
72
- if cached && cached["embedding"]
73
- document["embedding"] = cached["embedding"]
74
- return
75
- end
112
+ def embed_document(document, cache, adapter, configuration, job)
113
+ hash = IndexCache.content_hash(document.except("embedding", "related_embedding"))
114
+ return if embedding_restored?(document, cache, hash, job[:field])
76
115
 
77
- embedding = adapter.embed(embedding_text(document, configuration))
116
+ embedding = adapter.embed(embedding_text(document, job[:document_prefix]))
78
117
  if embedding
79
- document["embedding"] = embedding
118
+ document[job[:field]] = embedding
80
119
  cache.store(document["id"], hash, embedding)
81
120
  elsif configuration.embedding_fail_on_error?
82
121
  raise Jekyll::Errors::FatalException,
@@ -84,9 +123,17 @@ module Jekyll
84
123
  end
85
124
  end
86
125
 
87
- def embedding_text(document, configuration)
126
+ def embedding_restored?(document, cache, hash, field)
127
+ cached = cache.lookup(document["id"], hash)
128
+ return false unless cached && cached["embedding"]
129
+
130
+ document[field] = cached["embedding"]
131
+ true
132
+ end
133
+
134
+ def embedding_text(document, document_prefix)
88
135
  text = [document["title"], document["excerpt"], document["content"]].compact.join(" ")
89
- "#{configuration.embedding_document_prefix}#{text}"
136
+ "#{document_prefix}#{text}"
90
137
  end
91
138
 
92
139
  def collection_documents(site, configuration, builder)
@@ -12,11 +12,12 @@ module Jekyll
12
12
  # should be git-ignored.
13
13
  class IndexCache
14
14
  CACHE_FILE = ".jekyll-client-search-cache.json"
15
+ RELATED_CACHE_FILE = ".jekyll-client-search-related-cache.json"
15
16
 
16
17
  attr_reader :path
17
18
 
18
- def initialize(site_source, embedding_identity: nil)
19
- @path = File.join(site_source, CACHE_FILE)
19
+ def initialize(site_source, embedding_identity: nil, file_name: CACHE_FILE)
20
+ @path = File.join(site_source, file_name)
20
21
  @embedding_identity = embedding_identity
21
22
  @entries = load
22
23
  end
@@ -11,6 +11,11 @@ module Jekyll
11
11
  # enabled: true
12
12
  # model: all-minilm # or nomic-embed-text, bge-m3, etc.
13
13
  # base_url: http://localhost:11434
14
+ #
15
+ # A +localhost+ base_url is resolved once to the loopback literal
16
+ # (127.0.0.1 or ::1) that accepts a TCP connection, because Excon
17
+ # does not fall back across address families when the DNS name maps
18
+ # to a family the server is not listening on.
14
19
  class OllamaEmbeddingAdapter
15
20
  attr_reader :model, :base_url, :connect_timeout, :read_timeout
16
21
 
@@ -48,12 +53,53 @@ module Jekyll
48
53
  @client ||= begin
49
54
  require "ollama"
50
55
  Ollama::Client.new(
51
- base_url: @base_url,
56
+ base_url: resolved_base_url,
52
57
  connect_timeout: @connect_timeout,
53
58
  read_timeout: @read_timeout
54
59
  )
55
60
  end
56
61
  end
62
+
63
+ # "localhost" may resolve to ::1 while the server only listens on
64
+ # 127.0.0.1, or the reverse. Excon does not fall back across
65
+ # address families, so probe both loopback literals once and use
66
+ # the one that accepts a TCP connection. Any other host name is
67
+ # used exactly as configured.
68
+ def resolved_base_url
69
+ @resolved_base_url ||= resolve_localhost(@base_url)
70
+ end
71
+
72
+ def resolve_localhost(base_url)
73
+ require "uri"
74
+ uri = URI.parse(base_url)
75
+ return base_url unless uri.host.to_s.chomp(".").casecmp("localhost").zero?
76
+
77
+ resolved = loopback_base_url(uri)
78
+ return base_url unless resolved
79
+
80
+ Jekyll.logger.info "ClientSearch:", "resolved localhost to #{resolved}"
81
+ resolved
82
+ rescue URI::InvalidURIError
83
+ base_url
84
+ end
85
+
86
+ def loopback_base_url(uri)
87
+ %w[127.0.0.1 ::1].each do |address|
88
+ next unless tcp_reachable?(address, uri.port)
89
+
90
+ resolved = uri.dup
91
+ resolved.host = address.include?(":") ? "[#{address}]" : address
92
+ return resolved.to_s
93
+ end
94
+ nil
95
+ end
96
+
97
+ def tcp_reachable?(address, port)
98
+ require "socket"
99
+ Socket.tcp(address, port, connect_timeout: @connect_timeout) { true }
100
+ rescue SystemCallError, SocketError
101
+ false
102
+ end
57
103
  end
58
104
  end
59
105
  end
@@ -8,6 +8,9 @@ module Jekyll
8
8
 
9
9
  MODEL_MAP = {
10
10
  "embeddinggemma:300m" => "onnx-community/embeddinggemma-300m-ONNX",
11
+ "embeddinggemma-2" => "onnx-community/embeddinggemma-2-ONNX",
12
+ "embeddinggemma-2:270m" => "onnx-community/embeddinggemma-2-ONNX",
13
+ "embeddinggemma-2:570m" => "onnx-community/embeddinggemma-2-ONNX",
11
14
  "all-minilm" => "Xenova/all-MiniLM-L6-v2"
12
15
  }.freeze
13
16
 
@@ -29,7 +29,10 @@ module Jekyll
29
29
 
30
30
  def build_relation(source, target)
31
31
  metadata = metadata_relation(source, target)
32
- semantic_similarity = similarity(source["embedding"], target["embedding"])
32
+ semantic_similarity = similarity(
33
+ source["related_embedding"] || source["embedding"],
34
+ target["related_embedding"] || target["embedding"]
35
+ )
33
36
  return unless metadata || semantic_match?(semantic_similarity)
34
37
 
35
38
  relation = base_relation(target, metadata, semantic_similarity)
@@ -62,12 +62,27 @@ module Jekyll
62
62
  @values.fetch("max_items")
63
63
  end
64
64
 
65
+ # Ollama model used for related-article similarity analysis. nil means
66
+ # the site uses the general embedding.model.
67
+ def index_model
68
+ value = @values["index_model"]
69
+ value&.to_s
70
+ end
71
+
65
72
  private
66
73
 
67
74
  def validate!
68
75
  validate_booleans!
69
76
  validate_similarity!
70
77
  validate_max_items!
78
+ validate_index_model!
79
+ end
80
+
81
+ def validate_index_model!
82
+ return if index_model.nil? || !index_model.empty?
83
+
84
+ raise Jekyll::Errors::FatalException,
85
+ "client_search related index_model must not be empty"
71
86
  end
72
87
 
73
88
  def validate_booleans!
@@ -18,11 +18,34 @@ module Jekyll
18
18
 
19
19
  def initialize(tag_name, markup, tokens)
20
20
  super
21
+ # No validation here: `{% comment %}` still parses nested tags, so
22
+ # `initialize` runs on commented-out tags. Validate in `render` —
23
+ # a bad tag inside a comment must not break the build.
21
24
  @markup = markup.to_s.strip
25
+ end
26
+
27
+ def render(context)
22
28
  parse_tokens(@markup)
29
+ site = context.registers[:site]
30
+ return "" if site.nil?
31
+
32
+ config = site.config.fetch("client_search", {})
33
+ return "" if config == false
34
+ return "" unless related_enabled?(config)
35
+
36
+ asset_prefix = asset_prefix(site)
37
+ sort_attr = @sort ? " data-related-sort=\"#{@sort}\"" : ""
38
+ max_attr = resolve_max_attr(config)
39
+ scripts = build_scripts(asset_prefix)
40
+ build_html(sort_attr, max_attr, scripts)
23
41
  end
24
42
 
43
+ private
44
+
25
45
  def parse_tokens(markup)
46
+ return if @parsed
47
+
48
+ @parsed = true
26
49
  @sort = nil
27
50
  @max_items = nil
28
51
  @include_scripts = true
@@ -49,23 +72,6 @@ module Jekyll
49
72
  "or {% related_articles no_scripts %}"
50
73
  end
51
74
 
52
- def render(context)
53
- site = context.registers[:site]
54
- return "" if site.nil?
55
-
56
- config = site.config.fetch("client_search", {})
57
- return "" if config == false
58
- return "" unless related_enabled?(config)
59
-
60
- asset_prefix = asset_prefix(site)
61
- sort_attr = @sort ? " data-related-sort=\"#{@sort}\"" : ""
62
- max_attr = resolve_max_attr(config)
63
- scripts = build_scripts(asset_prefix)
64
- build_html(sort_attr, max_attr, scripts)
65
- end
66
-
67
- private
68
-
69
75
  def related_enabled?(config)
70
76
  related = config.fetch("related", {})
71
77
  related["enabled"] == true
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Jekyll
6
+ module ClientSearch
7
+ # Shared <script> tag assembly for the client-search Liquid tags so each
8
+ # tag only declares its own asset file.
9
+ module ScriptTags
10
+ private
11
+
12
+ def script_tags(configuration, prefix, tag_asset, extra: [])
13
+ scripts = []
14
+ engine = engine_script(configuration)
15
+ scripts << engine if engine
16
+ scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
17
+ scripts.concat(extra)
18
+ scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
19
+ scripts << "<script src=\"#{prefix}/assets/#{tag_asset}\"></script>"
20
+ scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
21
+ scripts.map { |script| " #{script}" }.join("\n")
22
+ end
23
+
24
+ def engine_script(configuration)
25
+ url = configuration.engine_url
26
+ return nil unless url
27
+
28
+ attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
29
+ if configuration.engine_crossorigin
30
+ crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
31
+ attrs << "crossorigin=\"#{crossorigin}\""
32
+ end
33
+ if configuration.engine_sri
34
+ integrity = CGI.escapeHTML(configuration.engine_sri)
35
+ attrs << "integrity=\"#{integrity}\""
36
+ end
37
+ "<script #{attrs.join(' ')}></script>"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
4
-
5
3
  module Jekyll
6
4
  module ClientSearch
7
5
  # Liquid tag that renders the search form, status/results containers,
@@ -14,21 +12,20 @@ module Jekyll
14
12
  #
15
13
  # When client_search is disabled the tag renders nothing.
16
14
  class SearchTag < Liquid::Tag
15
+ include ScriptTags
16
+
17
17
  SYNTAX = /\A(scripts_only|no_scripts)?\z/
18
18
 
19
19
  def initialize(tag_name, markup, tokens)
20
20
  super
21
+ # No validation here: `{% comment %}` still parses nested tags, so
22
+ # `initialize` runs on commented-out tags. Validate in `render` —
23
+ # a bad tag inside a comment must not break the build.
21
24
  @markup = markup.to_s.strip
22
- unless (match = @markup.match(SYNTAX))
23
- raise Liquid::SyntaxError,
24
- "search_form: invalid syntax. Use {% search_form %}, " \
25
- "{% search_form scripts_only %}, or {% search_form no_scripts %}"
26
- end
27
-
28
- @mode = match[1] || "full"
29
25
  end
30
26
 
31
27
  def render(context)
28
+ validate_markup!
32
29
  site = context.registers[:site]
33
30
  return "" if site.nil?
34
31
 
@@ -51,6 +48,18 @@ module Jekyll
51
48
 
52
49
  private
53
50
 
51
+ def validate_markup!
52
+ return if @mode
53
+
54
+ unless (match = @markup.match(SYNTAX))
55
+ raise Liquid::SyntaxError,
56
+ "search_form: invalid syntax. Use {% search_form %}, " \
57
+ "{% search_form scripts_only %}, or {% search_form no_scripts %}"
58
+ end
59
+
60
+ @mode = match[1] || "full"
61
+ end
62
+
54
63
  def build_form
55
64
  <<~HTML
56
65
  <form id="search-form" role="search">
@@ -64,31 +73,8 @@ module Jekyll
64
73
  end
65
74
 
66
75
  def build_scripts(configuration, prefix)
67
- scripts = []
68
- engine = engine_script(configuration)
69
- scripts << engine if engine
70
- scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
71
- scripts.concat(embedder_scripts(configuration, prefix))
72
- scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
73
- scripts << "<script src=\"#{prefix}/assets/client-search-base.js\"></script>"
74
- scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
75
- scripts.map { |script| " #{script}" }.join("\n")
76
- end
77
-
78
- def engine_script(configuration)
79
- url = configuration.engine_url
80
- return nil unless url
81
-
82
- attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
83
- if configuration.engine_crossorigin
84
- crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
85
- attrs << "crossorigin=\"#{crossorigin}\""
86
- end
87
- if configuration.engine_sri
88
- integrity = CGI.escapeHTML(configuration.engine_sri)
89
- attrs << "integrity=\"#{integrity}\""
90
- end
91
- "<script #{attrs.join(' ')}></script>"
76
+ script_tags(configuration, prefix, "client-search-base.js",
77
+ extra: embedder_scripts(configuration, prefix))
92
78
  end
93
79
 
94
80
  def embedder_scripts(configuration, prefix)