asciidoctor-html 0.1.13 → 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,14 +11,14 @@ 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
17
18
  # A book is a collection of documents with cross referencing
18
19
  # supported via the cref macro.
19
20
  class Book
20
- attr_reader :title, :author, :date, :chapname,
21
- :refs, :templates
21
+ attr_reader :title, :chapname, :refs, :templates
22
22
 
23
23
  Asciidoctor::Extensions.register do
24
24
  tree_processor RefTreeProcessor
@@ -39,7 +39,6 @@ module Asciidoctor
39
39
 
40
40
  DEFAULT_OPTS = {
41
41
  title: "Untitled Book",
42
- author: "Anonymous Author",
43
42
  chapname: "Chapter"
44
43
  }.freeze
45
44
 
@@ -54,19 +53,16 @@ module Asciidoctor
54
53
  # opts:
55
54
  # - title
56
55
  # - short_title
57
- # - author
58
- # - date
59
- # - se_id
56
+ # - authors
60
57
  # - chapname
61
58
  def initialize(opts = {})
62
59
  opts = DEFAULT_OPTS.merge opts
63
60
  @title = ERB::Escape.html_escape opts[:title]
64
61
  @short_title = ERB::Escape.html_escape opts[:short_title]
65
- @author = ERB::Escape.html_escape opts[:author]
66
- @date = opts.include?(:date) ? Date.parse(opts[:date]) : Date.today
67
- @se_id = opts[:se_id]
62
+ @authors = opts[:authors]
68
63
  @base_url = opts[:base_url]
69
64
  @chapname = opts[:chapname]
65
+ @search_index = {} # Hash(docname => Array[SearchData])
70
66
  @refs = {} # Hash(docname => Hash(id => reftext))
71
67
  @templates = {} # Hash(docname => TData)
72
68
  end
@@ -98,32 +94,17 @@ module Asciidoctor
98
94
  read(chapters, appendices).each do |name, html|
99
95
  filename = "#{name}.html"
100
96
  File.write("#{outdir}/#{filename}", html)
97
+ build_index(name, html) unless omit_search?
101
98
  entries << Template.sitemap_entry("#{@base_url}#{filename}") if needs_sitemap
102
99
  end
103
- File.write("#{outdir}/#{SEARCH_PAGE}", search_page(@se_id)) if @se_id
100
+ File.write "#{outdir}/#{SEARCH_PAGE}", search_page unless omit_search?
104
101
  File.write("#{outdir}/sitemap.xml", Template.sitemap(entries)) if needs_sitemap
105
102
  end
106
103
 
107
104
  private
108
105
 
109
106
  include Pagination
110
-
111
- def search_page(se_id)
112
- content = <<~HTML
113
- <script async src="https://cse.google.com/cse.js?cx=#{se_id}"></script>
114
- <div class="gcse-search"></div>
115
- HTML
116
- Template.html(
117
- content,
118
- nav_items,
119
- title: @title,
120
- short_title: @short_title,
121
- author: @author,
122
- date: @date,
123
- chapsubheading: "Search",
124
- langs: []
125
- )
126
- end
107
+ include Search
127
108
 
128
109
  def register!(docs, filename, doc)
129
110
  key = key filename
@@ -198,6 +179,26 @@ module Asciidoctor
198
179
  node.reftext || (node.title unless node.inline?) || "[#{node.id}]" if node.id
199
180
  end
200
181
 
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
188
+ end
189
+ if authors.empty? && @authors
190
+ authors = @authors.map do |author|
191
+ ERB::Escape.html_escape author
192
+ end
193
+ end
194
+ return if authors.empty?
195
+
196
+ [
197
+ authors[0..-2].join(", "),
198
+ authors[-1]
199
+ ].reject(&:empty?).join(" and ")
200
+ end
201
+
201
202
  def outline(doc)
202
203
  items = []
203
204
  doc.sections.each do |section|
@@ -216,6 +217,10 @@ module Asciidoctor
216
217
  html
217
218
  end
218
219
 
220
+ def omit_search?
221
+ @templates.size < 2 && @search_index.empty?
222
+ end
223
+
219
224
  def nav_items(active_key = -1, doc = nil)
220
225
  items = @templates.map do |k, td|
221
226
  active = (k == active_key)
@@ -223,7 +228,7 @@ module Asciidoctor
223
228
  navtext = Template.nav_text td.chapprefix, td.chaptitle
224
229
  Template.nav_item "#{k}.html", navtext, subnav, active:
225
230
  end
226
- return items unless @se_id
231
+ return items if omit_search?
227
232
 
228
233
  items.unshift(Template.nav_item(
229
234
  SEARCH_PAGE,
@@ -241,7 +246,7 @@ module Asciidoctor
241
246
  nav_items,
242
247
  title: @title,
243
248
  short_title: @short_title,
244
- author: @author,
249
+ authors: display_authors(doc),
245
250
  date: @date,
246
251
  description: doc.attr("description"),
247
252
  chapheading: tdata.chapheading,
@@ -85,7 +85,7 @@ module Asciidoctor
85
85
 
86
86
  def self.generate_bookopts(config)
87
87
  book_opts = {}
88
- %i[title short_title author date 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
@@ -50,7 +50,7 @@ module Asciidoctor
50
50
  # - chapheading: String
51
51
  # - chapsubheading: String
52
52
  # - content: String
53
- # - author: String
53
+ # - authors: String
54
54
  # - date: Date
55
55
  def self.main(opts)
56
56
  <<~HTML
@@ -59,7 +59,7 @@ module Asciidoctor
59
59
  #{%(<h1 class="chapheading">#{opts[:chapheading]}</h1>) if opts[:chapheading]}
60
60
  <h1 class="chaptitle">#{opts[:chapsubheading]}</h1>
61
61
  #{opts[:content]}
62
- #{footer opts[:author], opts[:date].year}
62
+ #{footer opts[:authors]}
63
63
  </div>
64
64
  </main>
65
65
  HTML
@@ -92,10 +92,10 @@ module Asciidoctor
92
92
  HTML
93
93
  end
94
94
 
95
- def self.footer(author, year)
95
+ def self.footer(authors)
96
96
  <<~HTML
97
97
  <footer class="footer">
98
- <div class="footer-left">&#169; #{year} #{author}</div>
98
+ <div class="footer-left">&#169; <span id="cr-year"></span> #{authors}</div>
99
99
  <div class="footer-right">Built with
100
100
  <a href="https://github.com/ravirajani/asciidoctor-html">asciidoctor-html</a>
101
101
  </div>
@@ -109,13 +109,13 @@ module Asciidoctor
109
109
  end.join("\n ")
110
110
  end
111
111
 
112
- def self.head(title, description, author, langs)
112
+ def self.head(title, description, authors, langs)
113
113
  <<~HTML
114
114
  <head>
115
115
  <meta charset="utf-8">
116
116
  <meta name="viewport" content="width=device-width, initial-scale=1">
117
117
  #{%(<meta name="description" content="#{description}">) if description}
118
- #{%(<meta name="author" content="#{author}">) if author}
118
+ #{%(<meta name="author" content="#{authors}">) if authors}
119
119
  <title>#{title}</title>
120
120
  <link rel="apple-touch-icon" sizes="180x180" href="#{FAVICON_PATH}/apple-touch-icon.png">
121
121
  <link rel="icon" type="image/png" sizes="32x32" href="#{FAVICON_PATH}/favicon-32x32.png">
@@ -143,9 +143,8 @@ module Asciidoctor
143
143
  # opts:
144
144
  # - title: String
145
145
  # - short_title: String
146
- # - author: String
146
+ # - authors: String
147
147
  # - description: String
148
- # - date: Date
149
148
  # - chapheading: String
150
149
  # - chapsubheading: String
151
150
  # - langs: Array[String]
@@ -154,7 +153,7 @@ module Asciidoctor
154
153
  <<~HTML
155
154
  <!DOCTYPE html>
156
155
  <html lang="en">
157
- #{head opts[:title], opts[:description], opts[:author], opts[:langs]}
156
+ #{head opts[:title], opts[:description], opts[:authors], opts[:langs]}
158
157
  <body>
159
158
  #{sidebar(nav_items) if nav}
160
159
  <div id="page" class="page">
@@ -163,6 +162,7 @@ module Asciidoctor
163
162
  #{main content:, **opts}
164
163
  </div> <!-- .page -->
165
164
  <script type="module">
165
+ document.getElementById("cr-year").textContent = (new Date()).getFullYear();
166
166
  #{Highlightjs::PLUGIN}
167
167
  hljs.highlightAll();
168
168
  #{Popovers::POPOVERS}
@@ -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
 
@@ -55,7 +55,7 @@ module Minitest
55
55
  Pathname(TESTS_DIR).children.reject { |f| f.file? || f.basename.to_s.start_with?("_") }.sort.each do |pn|
56
56
  report_files results, pn
57
57
  end
58
- adoc = %(= Test Results\n\n#{time}\n#{results.join "\n"})
58
+ adoc = %(= Test Results\nRavi Rajani\n#{time}\n#{results.join "\n"})
59
59
  File.write("#{DOCS_DIR}/tests.adoc", adoc)
60
60
  Asciidoctor::Html::CLI.run({ "config-file": CONFIG_FILE, watch: false })
61
61
  end
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.13
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