jekyll-client-search 0.1.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +121 -0
  3. data/LICENSE +22 -0
  4. data/NOTICE +49 -0
  5. data/README.developer.md +204 -0
  6. data/README.md +948 -0
  7. data/assets/adapters/elasticlunr.js +59 -0
  8. data/assets/adapters/minisearch.js +57 -0
  9. data/assets/adapters/semantic.js +154 -0
  10. data/assets/client-search-base.js +294 -0
  11. data/assets/client-search-related.js +176 -0
  12. data/assets/includes/related-articles.html +36 -0
  13. data/assets/layouts/post-with-related.html +54 -0
  14. data/assets/query-embedders/ollama-api.js +63 -0
  15. data/assets/query-embedders/transformers-worker.js +130 -0
  16. data/assets/query-embedders/transformers.js +223 -0
  17. data/docs/assets/icon-256.png +0 -0
  18. data/docs/assets/icon.svg +133 -0
  19. data/lib/jekyll/client_search/configuration.rb +152 -0
  20. data/lib/jekyll/client_search/configuration_accessors.rb +48 -0
  21. data/lib/jekyll/client_search/document_builder.rb +66 -0
  22. data/lib/jekyll/client_search/embedder_config_page.rb +14 -0
  23. data/lib/jekyll/client_search/embedding_configuration.rb +95 -0
  24. data/lib/jekyll/client_search/generator.rb +134 -0
  25. data/lib/jekyll/client_search/index_cache.rb +82 -0
  26. data/lib/jekyll/client_search/live_search_configuration.rb +70 -0
  27. data/lib/jekyll/client_search/ollama_embedding_adapter.rb +59 -0
  28. data/lib/jekyll/client_search/query_embedder_configuration.rb +127 -0
  29. data/lib/jekyll/client_search/related_analyzer.rb +152 -0
  30. data/lib/jekyll/client_search/related_configuration.rb +103 -0
  31. data/lib/jekyll/client_search/related_page.rb +14 -0
  32. data/lib/jekyll/client_search/related_tag.rb +76 -0
  33. data/lib/jekyll/client_search/runtime_config_page.rb +30 -0
  34. data/lib/jekyll/client_search/search_index_page.rb +15 -0
  35. data/lib/jekyll/client_search/search_tag.rb +100 -0
  36. data/lib/jekyll/client_search/tasks.rb +137 -0
  37. data/lib/jekyll/client_search/version.rb +7 -0
  38. data/lib/jekyll/client_search.rb +25 -0
  39. data/lib/jekyll-client-search.rb +3 -0
  40. metadata +104 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module ClientSearch
5
+ # Generates browser runtime settings shared by all search adapters.
6
+ class RuntimeConfigPage < Jekyll::PageWithoutAFile
7
+ def initialize(site, configuration)
8
+ super(site, site.source, "assets", "search-runtime-config.js")
9
+ self.data = { "layout" => nil, "sitemap" => false }
10
+ defaults = {
11
+ "indexUrl" => index_url(site, configuration.output),
12
+ "liveSearch" => configuration.live_search_config
13
+ }
14
+ defaults["relatedUrl"] = index_url(site, configuration.related_output) if configuration.related_enabled?
15
+ json = JSON.generate(defaults)
16
+ self.content = "window.clientSearchConfig = (function (generated, existing) {" \
17
+ "var liveSearch = Object.assign({}, generated.liveSearch, existing.liveSearch || {});" \
18
+ "return Object.assign(generated, existing, { liveSearch: liveSearch });" \
19
+ "}(#{json}, window.clientSearchConfig || {}));\n"
20
+ end
21
+
22
+ private
23
+
24
+ def index_url(site, output)
25
+ parts = [site.baseurl, output].map { |part| part.to_s.gsub(%r{\A/+|/+$}, "") }.reject(&:empty?)
26
+ "/#{parts.join('/')}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module ClientSearch
5
+ # A Jekyll page that renders the search documents as JSON at the
6
+ # configured output path.
7
+ class SearchIndexPage < Jekyll::PageWithoutAFile
8
+ def initialize(site, output, documents)
9
+ super(site, site.source, File.dirname(output), File.basename(output))
10
+ self.data = { "layout" => nil, "sitemap" => false }
11
+ self.content = JSON.generate(documents)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module ClientSearch
5
+ # Liquid tag that renders the search form, status/results containers,
6
+ # and all runtime scripts in one line — config-driven so changing engines
7
+ # in _config.yml requires zero template changes.
8
+ #
9
+ # {% search_form %}
10
+ # {% search_form scripts_only %} — just scripts (custom form HTML)
11
+ # {% search_form no_scripts %} — just form HTML (user loads scripts)
12
+ #
13
+ # When client_search is disabled the tag renders nothing.
14
+ class SearchTag < Liquid::Tag
15
+ SYNTAX = /\A\s*(scripts_only|no_scripts)?\s*\z/
16
+
17
+ def initialize(tag_name, markup, tokens)
18
+ super
19
+ @markup = markup.to_s
20
+ unless (match = @markup.match(SYNTAX))
21
+ raise Liquid::SyntaxError,
22
+ "search_form: invalid syntax. Use {% search_form %}, " \
23
+ "{% search_form scripts_only %}, or {% search_form no_scripts %}"
24
+ end
25
+
26
+ @mode = match[1] || "full"
27
+ end
28
+
29
+ def render(context)
30
+ site = context.registers[:site]
31
+ config_hash = site&.config&.fetch("client_search", {})
32
+ return "" unless config_hash["enabled"] != false
33
+
34
+ configuration = Configuration.new(site)
35
+ baseurl = site.config["baseurl"].to_s.gsub(%r{\A/+|/+$}, "")
36
+ prefix = baseurl.empty? ? "" : "/#{baseurl}"
37
+
38
+ form_html = build_form
39
+ return form_html if @mode == "no_scripts"
40
+
41
+ scripts = build_scripts(configuration, prefix)
42
+ return scripts if @mode == "scripts_only"
43
+
44
+ "#{form_html}\n#{scripts}"
45
+ end
46
+
47
+ private
48
+
49
+ def build_form
50
+ <<~HTML
51
+ <form id="search-form" role="search">
52
+ <label class="is-sr-only" for="search-query">Search</label>
53
+ <input id="search-query" type="search" name="q" placeholder="Search">
54
+ <button type="submit">Search</button>
55
+ </form>
56
+ <div id="search-status" aria-live="polite"></div>
57
+ <div id="search-results"></div>
58
+ HTML
59
+ end
60
+
61
+ def build_scripts(configuration, prefix)
62
+ scripts = []
63
+ engine = engine_script(configuration)
64
+ scripts << engine if engine
65
+ scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
66
+ scripts.concat(embedder_scripts(configuration, prefix))
67
+ scripts << "<script src=\"#{prefix}/assets/client-search-base.js\"></script>"
68
+ scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
69
+ scripts.map { |script| " #{script}" }.join("\n")
70
+ end
71
+
72
+ def engine_script(configuration)
73
+ url = configuration.engine_url
74
+ return nil unless url
75
+
76
+ attrs = ["src=\"#{url}\""]
77
+ attrs << "crossorigin=\"#{configuration.engine_crossorigin}\"" if configuration.engine_crossorigin
78
+ attrs << "integrity=\"#{configuration.engine_sri}\"" if configuration.engine_sri
79
+ "<script #{attrs.join(' ')}></script>"
80
+ end
81
+
82
+ def embedder_scripts(configuration, prefix)
83
+ return [] unless semantic_with_embedder?(configuration)
84
+
85
+ scripts = ["<script src=\"#{prefix}/assets/search-embedder-config.js\"></script>"]
86
+ embedder_asset = configuration.query_embedder_asset
87
+ scripts << "<script src=\"#{prefix}/#{embedder_asset}\"></script>" if embedder_asset
88
+ scripts
89
+ end
90
+
91
+ def semantic_with_embedder?(configuration)
92
+ configuration.engine == "semantic" &&
93
+ configuration.embedding_enabled? &&
94
+ configuration.query_embedder_type != "none"
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ Liquid::Template.register_tag("search_form", Jekyll::ClientSearch::SearchTag)
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "rubygems"
5
+
6
+ module Jekyll
7
+ module ClientSearch
8
+ # Rake tasks for inspecting and installing reference files.
9
+ #
10
+ # Tasks:
11
+ # jekyll_client_search:reference_files — list gem reference files,
12
+ # diff against site copies, offer to update
13
+ # jekyll_client_search:install — copy reference files into the site's
14
+ # _layouts/ and _includes/ directories
15
+ module Tasks
16
+ REFERENCE_FILES = {
17
+ "assets/layouts/post-with-related.html" => "_layouts/post-with-related.html",
18
+ "assets/includes/related-articles.html" => "_includes/related-articles.html"
19
+ }.freeze
20
+
21
+ class << self
22
+ def gem_root
23
+ spec = Gem::Specification.find_by_name("jekyll-client-search")
24
+ spec.gem_dir
25
+ rescue Gem::MissingSpecError
26
+ # Fall back to the development source directory
27
+ File.expand_path("../../..", __dir__)
28
+ end
29
+
30
+ def reference_files
31
+ REFERENCE_FILES
32
+ end
33
+
34
+ def site_root
35
+ Dir.pwd
36
+ end
37
+
38
+ def list_reference_files
39
+ puts "jekyll-client-search reference files:"
40
+ puts
41
+ reference_files.each do |gem_rel, site_rel|
42
+ gem_path = File.join(gem_root, gem_rel)
43
+ site_path = File.join(site_root, site_rel)
44
+ site_exists = File.exist?(site_path)
45
+
46
+ status = if !site_exists
47
+ "not installed"
48
+ elsif same_content?(gem_path, site_path)
49
+ "up to date"
50
+ else
51
+ "modified or outdated"
52
+ end
53
+
54
+ puts " #{site_rel}"
55
+ puts " gem: #{gem_path}"
56
+ puts " site: #{site_path}"
57
+ puts " status: #{status}"
58
+ puts
59
+ end
60
+ end
61
+
62
+ def diff_reference_files
63
+ reference_files.each do |gem_rel, site_rel|
64
+ gem_path = File.join(gem_root, gem_rel)
65
+ site_path = File.join(site_root, site_rel)
66
+
67
+ unless File.exist?(site_path)
68
+ puts "#{site_rel}: not installed (no diff)"
69
+ next
70
+ end
71
+
72
+ if same_content?(gem_path, site_path)
73
+ puts "#{site_rel}: up to date"
74
+ else
75
+ puts "Diff for #{site_rel}:"
76
+ system("diff", "-u", site_path, gem_path)
77
+ puts
78
+ end
79
+ end
80
+ end
81
+
82
+ def install_reference_files(overwrite: false)
83
+ installed = []
84
+ skipped = []
85
+
86
+ reference_files.each do |gem_rel, site_rel|
87
+ result = install_file(gem_rel, site_rel, overwrite: overwrite)
88
+ case result
89
+ when :installed then installed << site_rel
90
+ when :skipped then skipped << site_rel
91
+ end
92
+ end
93
+
94
+ { installed: installed, skipped: skipped }
95
+ end
96
+
97
+ def install_file(gem_rel, site_rel, overwrite:)
98
+ gem_path = File.join(gem_root, gem_rel)
99
+ site_path = File.join(site_root, site_rel)
100
+
101
+ unless File.exist?(gem_path)
102
+ warn " WARNING: gem file missing: #{gem_path}"
103
+ return nil
104
+ end
105
+
106
+ return handle_existing(site_rel, gem_path, site_path) if File.exist?(site_path) && !overwrite
107
+
108
+ copy_file(gem_path, site_path, site_rel, overwrite)
109
+ end
110
+
111
+ def handle_existing(site_rel, gem_path, site_path)
112
+ if same_content?(gem_path, site_path)
113
+ puts " #{site_rel}: already up to date (skip)"
114
+ nil
115
+ else
116
+ puts " #{site_rel}: exists and differs (skip — use overwrite: true to replace)"
117
+ :skipped
118
+ end
119
+ end
120
+
121
+ def copy_file(gem_path, site_path, site_rel, overwrite)
122
+ FileUtils.mkdir_p(File.dirname(site_path))
123
+ FileUtils.cp(gem_path, site_path)
124
+ action = overwrite ? "updated" : "installed"
125
+ puts " #{site_rel}: #{action}"
126
+ :installed
127
+ end
128
+
129
+ def same_content?(path_a, path_b)
130
+ File.read(path_a) == File.read(path_b)
131
+ rescue Errno::ENOENT
132
+ false
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module ClientSearch
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+ require "json"
5
+ require "jekyll"
6
+ require "pathname"
7
+ require "time"
8
+ require_relative "client_search/version"
9
+ require_relative "client_search/embedding_configuration"
10
+ require_relative "client_search/live_search_configuration"
11
+ require_relative "client_search/related_configuration"
12
+ require_relative "client_search/query_embedder_configuration"
13
+ require_relative "client_search/configuration_accessors"
14
+ require_relative "client_search/configuration"
15
+ require_relative "client_search/document_builder"
16
+ require_relative "client_search/search_index_page"
17
+ require_relative "client_search/runtime_config_page"
18
+ require_relative "client_search/related_analyzer"
19
+ require_relative "client_search/related_page"
20
+ require_relative "client_search/embedder_config_page"
21
+ require_relative "client_search/index_cache"
22
+ require_relative "client_search/ollama_embedding_adapter"
23
+ require_relative "client_search/generator"
24
+ require_relative "client_search/related_tag"
25
+ require_relative "client_search/search_tag"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jekyll/client_search"
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-client-search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Svend Gundestrup
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '4.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '4.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5.0'
32
+ description: A Jekyll plugin that generates a configurable JSON search index with
33
+ pluggable browser runtime adapters for client-side JavaScript search engines.
34
+ email:
35
+ - svend@gundestrup.dk
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - CHANGELOG.md
41
+ - LICENSE
42
+ - NOTICE
43
+ - README.developer.md
44
+ - README.md
45
+ - assets/adapters/elasticlunr.js
46
+ - assets/adapters/minisearch.js
47
+ - assets/adapters/semantic.js
48
+ - assets/client-search-base.js
49
+ - assets/client-search-related.js
50
+ - assets/includes/related-articles.html
51
+ - assets/layouts/post-with-related.html
52
+ - assets/query-embedders/ollama-api.js
53
+ - assets/query-embedders/transformers-worker.js
54
+ - assets/query-embedders/transformers.js
55
+ - docs/assets/icon-256.png
56
+ - docs/assets/icon.svg
57
+ - lib/jekyll-client-search.rb
58
+ - lib/jekyll/client_search.rb
59
+ - lib/jekyll/client_search/configuration.rb
60
+ - lib/jekyll/client_search/configuration_accessors.rb
61
+ - lib/jekyll/client_search/document_builder.rb
62
+ - lib/jekyll/client_search/embedder_config_page.rb
63
+ - lib/jekyll/client_search/embedding_configuration.rb
64
+ - lib/jekyll/client_search/generator.rb
65
+ - lib/jekyll/client_search/index_cache.rb
66
+ - lib/jekyll/client_search/live_search_configuration.rb
67
+ - lib/jekyll/client_search/ollama_embedding_adapter.rb
68
+ - lib/jekyll/client_search/query_embedder_configuration.rb
69
+ - lib/jekyll/client_search/related_analyzer.rb
70
+ - lib/jekyll/client_search/related_configuration.rb
71
+ - lib/jekyll/client_search/related_page.rb
72
+ - lib/jekyll/client_search/related_tag.rb
73
+ - lib/jekyll/client_search/runtime_config_page.rb
74
+ - lib/jekyll/client_search/search_index_page.rb
75
+ - lib/jekyll/client_search/search_tag.rb
76
+ - lib/jekyll/client_search/tasks.rb
77
+ - lib/jekyll/client_search/version.rb
78
+ homepage: https://github.com/gundestrup/jekyll-client-search
79
+ licenses:
80
+ - AGPL-3.0-or-later
81
+ metadata:
82
+ homepage_uri: https://github.com/gundestrup/jekyll-client-search
83
+ source_code_uri: https://github.com/gundestrup/jekyll-client-search/tree/main
84
+ changelog_uri: https://github.com/gundestrup/jekyll-client-search/blob/main/CHANGELOG.md
85
+ bug_tracker_uri: https://github.com/gundestrup/jekyll-client-search/issues
86
+ rubygems_mfa_required: 'true'
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 3.2.0
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.6.9
102
+ specification_version: 4
103
+ summary: Build client-side search indexes for Jekyll sites
104
+ test_files: []