asciidoctor-html 0.1.14 → 0.1.15

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.
@@ -11,6 +11,7 @@ require_relative "bi_inline_macro"
11
11
  require_relative "text_inline_macro"
12
12
  require_relative "template"
13
13
  require_relative "pagination"
14
+ require_relative "search"
14
15
 
15
16
  module Asciidoctor
16
17
  module Html
@@ -53,16 +54,15 @@ module Asciidoctor
53
54
  # - title
54
55
  # - short_title
55
56
  # - authors
56
- # - se_id
57
57
  # - chapname
58
58
  def initialize(opts = {})
59
59
  opts = DEFAULT_OPTS.merge opts
60
60
  @title = ERB::Escape.html_escape opts[:title]
61
61
  @short_title = ERB::Escape.html_escape opts[:short_title]
62
62
  @authors = opts[:authors]
63
- @se_id = opts[:se_id]
64
63
  @base_url = opts[:base_url]
65
64
  @chapname = opts[:chapname]
65
+ @search_index = {} # Hash(docname => Array[SearchData])
66
66
  @refs = {} # Hash(docname => Hash(id => reftext))
67
67
  @templates = {} # Hash(docname => TData)
68
68
  end
@@ -94,32 +94,17 @@ module Asciidoctor
94
94
  read(chapters, appendices).each do |name, html|
95
95
  filename = "#{name}.html"
96
96
  File.write("#{outdir}/#{filename}", html)
97
+ build_index(name, html) unless omit_search?
97
98
  entries << Template.sitemap_entry("#{@base_url}#{filename}") if needs_sitemap
98
99
  end
99
- File.write("#{outdir}/#{SEARCH_PAGE}", search_page(@se_id)) if @se_id
100
+ File.write "#{outdir}/#{SEARCH_PAGE}", search_page unless omit_search?
100
101
  File.write("#{outdir}/sitemap.xml", Template.sitemap(entries)) if needs_sitemap
101
102
  end
102
103
 
103
104
  private
104
105
 
105
106
  include Pagination
106
-
107
- def search_page(se_id)
108
- content = <<~HTML
109
- <script async src="https://cse.google.com/cse.js?cx=#{se_id}"></script>
110
- <div class="gcse-search"></div>
111
- HTML
112
- Template.html(
113
- content,
114
- nav_items,
115
- title: @title,
116
- short_title: @short_title,
117
- authors: display_authors,
118
- date: @date,
119
- chapsubheading: "Search",
120
- langs: []
121
- )
122
- end
107
+ include Search
123
108
 
124
109
  def register!(docs, filename, doc)
125
110
  key = key filename
@@ -194,17 +179,18 @@ module Asciidoctor
194
179
  node.reftext || (node.title unless node.inline?) || "[#{node.id}]" if node.id
195
180
  end
196
181
 
197
- def display_authors(doc)
198
- authors = doc.authors.map do |author|
199
- doc.sub_replacements author.name
182
+ def display_authors(doc = nil)
183
+ authors = []
184
+ if doc
185
+ authors = doc.authors.map do |author|
186
+ doc.sub_replacements author.name
187
+ end
200
188
  end
201
-
202
189
  if authors.empty? && @authors
203
190
  authors = @authors.map do |author|
204
191
  ERB::Escape.html_escape author
205
192
  end
206
193
  end
207
-
208
194
  return if authors.empty?
209
195
 
210
196
  [
@@ -231,6 +217,10 @@ module Asciidoctor
231
217
  html
232
218
  end
233
219
 
220
+ def omit_search?
221
+ @templates.size < 2 && @search_index.empty?
222
+ end
223
+
234
224
  def nav_items(active_key = -1, doc = nil)
235
225
  items = @templates.map do |k, td|
236
226
  active = (k == active_key)
@@ -238,7 +228,7 @@ module Asciidoctor
238
228
  navtext = Template.nav_text td.chapprefix, td.chaptitle
239
229
  Template.nav_item "#{k}.html", navtext, subnav, active:
240
230
  end
241
- return items unless @se_id
231
+ return items if omit_search?
242
232
 
243
233
  items.unshift(Template.nav_item(
244
234
  SEARCH_PAGE,
@@ -85,7 +85,7 @@ module Asciidoctor
85
85
 
86
86
  def self.generate_bookopts(config)
87
87
  book_opts = {}
88
- %i[title short_title authors se_id base_url chapname].each do |opt|
88
+ %i[title short_title authors base_url chapname].each do |opt|
89
89
  key = opt.to_s
90
90
  book_opts[opt] = config[key] if config.include?(key)
91
91
  end
@@ -5,7 +5,7 @@ module Asciidoctor
5
5
  # Mixin to add pagination support to Book class
6
6
  module Pagination
7
7
  # Pagination item
8
- PagItem = Struct.new("PagItem", :url, :title)
8
+ PagItem = Struct.new "PagItem", :url, :title
9
9
 
10
10
  def display_paginator(prv, nxt)
11
11
  return "" unless prv || nxt
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Asciidoctor
6
+ module Html
7
+ # Mixed in to Book class to provide full text search
8
+ module Search
9
+ SEARCH_RESULT_OVERFLOW = 10 # characters
10
+
11
+ def search_page
12
+ content = <<~HTML
13
+ <div class="search-form-container">
14
+ <form id="search-form" class="search-form">
15
+ <input type="text" id="search-text" name="search-text" class="form-control search-box" placeholder="Search">
16
+ <button type="submit" class="btn btn-primary search-btn">Go</button>
17
+ </form>
18
+ </div>
19
+ <div id="search-results-container" class="hidden">
20
+ <h4>Found <span id="search-nmatches">0 matches</span></h4>
21
+ <ul id="search-results" class="search-results list-group list-group-flush"></ul>
22
+ </div>
23
+ <script src="https://unpkg.com/lunr/lunr.js"></script>
24
+ <script>#{lunr_script}</script>
25
+ HTML
26
+ Template.html(
27
+ content,
28
+ nav_items,
29
+ title: @title,
30
+ short_title: @short_title,
31
+ authors: display_authors,
32
+ date: @date,
33
+ chapsubheading: "Search",
34
+ langs: []
35
+ )
36
+ end
37
+
38
+ def build_index(key, html)
39
+ doctree = Nokogiri::HTML5.parse html
40
+ ref = @refs[key]
41
+ page_text = "#{doctree.at_css(".chaptitle")&.text} #{doctree.at_css(".preamble")&.text}"
42
+ index = [{
43
+ id: "#{key}.html",
44
+ title: ref["chapref"],
45
+ text: page_text
46
+ }]
47
+ doctree.css("section[id]").each do |section|
48
+ sectid = section["id"]
49
+ id = "#{key}.html##{sectid}"
50
+ title = "#{ref["chapref"]} › #{ref[sectid]}"
51
+ text = section.text
52
+ index << { id:, title:, text: }
53
+ end
54
+ @search_index[key] = index
55
+ end
56
+
57
+ def search_json
58
+ index_arr = []
59
+ @search_index.each_value do |search_data|
60
+ index_arr.concat search_data
61
+ end
62
+ index_arr.to_json
63
+ end
64
+
65
+ def lunr_script
66
+ <<~JS
67
+ (function() {
68
+ const resultsContainer = document.getElementById('search-results-container');
69
+ const nmatches = document.getElementById('search-nmatches');
70
+ const searchResults = document.getElementById('search-results');
71
+ const searchForm = document.getElementById('search-form');
72
+ const searchBox = document.getElementById('search-text');
73
+ const positionOverflow = 20;
74
+ const documents = #{search_json};
75
+ const idx = lunr(function() {
76
+ this.ref('id');
77
+ this.field('title');
78
+ this.field('text');
79
+ this.metadataWhitelist = ['position'];
80
+
81
+ documents.forEach(doc => {
82
+ this.add(doc)
83
+ });
84
+ });
85
+ function processSearchText(searchText) {
86
+ const results = idx.search(searchText).map(match => {
87
+ const doc = documents.find(d => d.id == match.ref);
88
+ const result = document.createElement('li');
89
+ result.classList.add('list-group-item');
90
+ const link = document.createElement('a');
91
+ const br = document.createElement('br');
92
+ link.setAttribute('href', match.ref);
93
+ link.innerHTML = doc.title;
94
+ result.append(link, br);
95
+ const metadata = match.matchData.metadata;
96
+ for(const term in metadata) {
97
+ const datum = metadata[term];
98
+ for(const type in datum) {
99
+ datum[type].position.forEach(pos => {
100
+ const start = pos[0];
101
+ const end = pos[0] + pos[1];
102
+ const text = doc[type];
103
+ const textMatch = text.substring(start, end)
104
+ const matchingText = document.createElement('mark');
105
+ const overflowLeft = document.createElement('span');
106
+ overflowLeft.classList.add('overflow-text-left');
107
+ const overflowRight = document.createElement('span');
108
+ overflowRight.classList.add('overflow-text-right');
109
+ const reLeft = /.{#{SEARCH_RESULT_OVERFLOW}}$/s;
110
+ let left = text.substring(0, start - 1).search(reLeft) + 1;
111
+ while(text[left] && text[left].trim() == text[left]) {
112
+ left--;
113
+ }
114
+ const reRight = /^.{#{SEARCH_RESULT_OVERFLOW}}/s;
115
+ let right = text.length;
116
+ if(rightMatch = text.substring(end + 1).match(reRight)) {
117
+ right = rightMatch[0].length + end;
118
+ while(text[right] && text[right].trim() == text[right]) {
119
+ right++;
120
+ }
121
+ }
122
+ overflowLeft.textContent = text.substring(left, start - 1);
123
+ matchingText.textContent = textMatch;
124
+ overflowRight.textContent = text.substring(end + 1, right);
125
+ result.append(overflowLeft, matchingText, overflowRight);
126
+ });
127
+ }
128
+ }
129
+ return result;
130
+ });
131
+ searchResults.replaceChildren(...results);
132
+ const n = results.length;
133
+ nmatches.textContent = n == 1 ? (n + ' match') : (n + ' matches');
134
+ resultsContainer.classList.remove('hidden');
135
+ }
136
+ searchForm.addEventListener('submit', e => {
137
+ e.preventDefault();
138
+ const searchText = searchBox.value;
139
+ processSearchText(searchText);
140
+ });
141
+ })();
142
+ JS
143
+ end
144
+ end
145
+ end
146
+ end
@@ -8,6 +8,7 @@ module Asciidoctor
8
8
  CSS_PATH = "#{ASSETS_PATH}/css".freeze
9
9
  IMG_PATH = "#{ASSETS_PATH}/img".freeze
10
10
  SEARCH_PAGE = "search.html"
11
+ SEARCH_INDEX = "search-index.json"
11
12
  end
12
13
  end
13
14
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-html
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ravi Rajani
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: nokogiri
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.18'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.18'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: psych
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -102,6 +116,7 @@ files:
102
116
  - lib/asciidoctor/html/popovers.rb
103
117
  - lib/asciidoctor/html/ref_tree_processor.rb
104
118
  - lib/asciidoctor/html/scroll.rb
119
+ - lib/asciidoctor/html/search.rb
105
120
  - lib/asciidoctor/html/sidebar.rb
106
121
  - lib/asciidoctor/html/table.rb
107
122
  - lib/asciidoctor/html/template.rb