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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/assets/css/styles.css +2 -2
- data/lib/asciidoctor/html/book.rb +34 -29
- data/lib/asciidoctor/html/cli.rb +1 -1
- data/lib/asciidoctor/html/pagination.rb +1 -1
- data/lib/asciidoctor/html/search.rb +146 -0
- data/lib/asciidoctor/html/template.rb +9 -9
- data/lib/asciidoctor/html.rb +1 -0
- data/lib/minitest/html_plugin.rb +1 -1
- metadata +16 -1
@@ -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, :
|
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
|
-
# -
|
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
|
-
@
|
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
|
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
|
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
|
-
|
249
|
+
authors: display_authors(doc),
|
245
250
|
date: @date,
|
246
251
|
description: doc.attr("description"),
|
247
252
|
chapheading: tdata.chapheading,
|
data/lib/asciidoctor/html/cli.rb
CHANGED
@@ -85,7 +85,7 @@ module Asciidoctor
|
|
85
85
|
|
86
86
|
def self.generate_bookopts(config)
|
87
87
|
book_opts = {}
|
88
|
-
%i[title short_title
|
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
|
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
|
-
# -
|
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[:
|
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(
|
95
|
+
def self.footer(authors)
|
96
96
|
<<~HTML
|
97
97
|
<footer class="footer">
|
98
|
-
<div class="footer-left">©
|
98
|
+
<div class="footer-left">© <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,
|
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="#{
|
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
|
-
# -
|
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[:
|
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}
|
data/lib/asciidoctor/html.rb
CHANGED
data/lib/minitest/html_plugin.rb
CHANGED
@@ -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\
|
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.
|
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
|