giblish 0.8.2 → 1.0.0.rc2
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/.github/workflows/unit_tests.yml +30 -0
- data/.gitignore +7 -3
- data/.ruby-version +1 -1
- data/Changelog.adoc +61 -0
- data/README.adoc +267 -0
- data/docs/concepts/text_search.adoc +213 -0
- data/docs/concepts/text_search_im/cgi-search_request.puml +35 -0
- data/docs/concepts/text_search_im/cgi-search_request.svg +397 -0
- data/docs/concepts/text_search_im/search_request.puml +40 -0
- data/docs/concepts/text_search_im/search_request.svg +408 -0
- data/docs/howtos/trigger_generation.adoc +180 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/Render Documents.png +0 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/View Documents.png +0 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.graphml +0 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.svg +0 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.graphml +0 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.svg +0 -0
- data/docs/howtos/trigger_generation_im/docgen_github.puml +51 -0
- data/docs/{setup_server_assets → howtos/trigger_generation_im}/giblish_deployment.graphml +0 -0
- data/docs/howtos/trigger_generation_im/post-receive-example.sh +50 -0
- data/docs/reference/box_flow_spec.adoc +22 -0
- data/docs/reference/search_spec.adoc +185 -0
- data/giblish.gemspec +47 -29
- data/lib/giblish/adocsrc_providers.rb +23 -0
- data/lib/giblish/application.rb +214 -41
- data/lib/giblish/cmdline.rb +273 -259
- data/lib/giblish/config_utils.rb +41 -0
- data/lib/giblish/configurator.rb +163 -0
- data/lib/giblish/conversion_info.rb +120 -0
- data/lib/giblish/docattr_providers.rb +125 -0
- data/lib/giblish/docid/docid.rb +181 -0
- data/lib/giblish/github_trigger/webhook_manager.rb +64 -0
- data/lib/giblish/gitrepos/checkoutmanager.rb +124 -0
- data/lib/giblish/{gititf.rb → gitrepos/gititf.rb} +30 -4
- data/lib/giblish/gitrepos/gitsummary.erb +61 -0
- data/lib/giblish/gitrepos/gitsummaryprovider.rb +78 -0
- data/lib/giblish/gitrepos/history_pb.rb +41 -0
- data/lib/giblish/indexbuilders/d3treegraph.rb +88 -0
- data/lib/giblish/indexbuilders/depgraphbuilder.rb +109 -0
- data/lib/giblish/indexbuilders/dotdigraphadoc.rb +174 -0
- data/lib/giblish/indexbuilders/standard_index.erb +10 -0
- data/lib/giblish/indexbuilders/subtree_indices.rb +132 -0
- data/lib/giblish/indexbuilders/templates/circles.html.erb +111 -0
- data/lib/giblish/indexbuilders/templates/flame.html.erb +61 -0
- data/lib/giblish/indexbuilders/templates/tree.html.erb +366 -0
- data/lib/giblish/indexbuilders/templates/treemap.html.erb +127 -0
- data/lib/giblish/indexbuilders/verbatimtree.rb +94 -0
- data/lib/giblish/pathtree.rb +473 -74
- data/lib/giblish/resourcepaths.rb +150 -0
- data/lib/giblish/search/expand_adoc.rb +55 -0
- data/lib/giblish/search/headingindexer.rb +312 -0
- data/lib/giblish/search/request_manager.rb +110 -0
- data/lib/giblish/search/searchquery.rb +68 -0
- data/lib/giblish/search/textsearcher.rb +349 -0
- data/lib/giblish/subtreeinfobuilder.rb +77 -0
- data/lib/giblish/treeconverter.rb +272 -0
- data/lib/giblish/utils.rb +142 -294
- data/lib/giblish/version.rb +1 -1
- data/lib/giblish.rb +10 -7
- data/scripts/hooks/post-receive.example +66 -0
- data/{docgen/scripts/githook_examples → scripts/hooks}/post-update.example +0 -0
- data/{docgen → scripts}/resources/css/adoc-colony.css +0 -0
- data/scripts/resources/css/giblish-serif.css +419 -0
- data/scripts/resources/css/giblish.css +1979 -419
- data/{docgen → scripts}/resources/fonts/Ubuntu-B.ttf +0 -0
- data/{docgen → scripts}/resources/fonts/Ubuntu-BI.ttf +0 -0
- data/{docgen → scripts}/resources/fonts/Ubuntu-R.ttf +0 -0
- data/{docgen → scripts}/resources/fonts/Ubuntu-RI.ttf +0 -0
- data/{docgen → scripts}/resources/fonts/mplus1p-regular-fallback.ttf +0 -0
- data/{docgen → scripts}/resources/images/giblish_logo.png +0 -0
- data/{docgen → scripts}/resources/images/giblish_logo.svg +0 -0
- data/{docgen → scripts}/resources/themes/giblish.yml +0 -0
- data/scripts/wserv_development.rb +32 -0
- data/web_apps/cgi_search/gibsearch.rb +43 -0
- data/web_apps/gh_webhook_trigger/config.ru +2 -0
- data/web_apps/gh_webhook_trigger/gh_webhook_trigger.rb +73 -0
- data/web_apps/gh_webhook_trigger/public/dummy.txt +3 -0
- data/web_apps/sinatra_search/config.ru +2 -0
- data/web_apps/sinatra_search/public/dummy.txt +3 -0
- data/web_apps/sinatra_search/sinatra_search.rb +34 -0
- data/web_apps/sinatra_search/tmp/restart.txt +0 -0
- metadata +168 -73
- data/.rubocop.yml +0 -7
- data/.travis.yml +0 -3
- data/Changelog +0 -16
- data/Gemfile +0 -4
- data/README.adoc +0 -1
- data/Rakefile +0 -41
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/data/testdocs/malformed/no_header.adoc +0 -5
- data/data/testdocs/toplevel.adoc +0 -19
- data/data/testdocs/wellformed/adorned_purpose.adoc +0 -17
- data/data/testdocs/wellformed/docidtest/docid_1.adoc +0 -24
- data/data/testdocs/wellformed/docidtest/docid_2.adoc +0 -8
- data/data/testdocs/wellformed/simple.adoc +0 -14
- data/data/testdocs/wellformed/source_highlighting/highlight_source.adoc +0 -38
- data/docgen/resources/css/giblish.css +0 -1979
- data/docgen/scripts/Jenkinsfile +0 -18
- data/docgen/scripts/gen_adoc_org.sh +0 -58
- data/docs/README.adoc +0 -387
- data/docs/setup_server.adoc +0 -202
- data/lib/giblish/buildgraph.rb +0 -216
- data/lib/giblish/buildindex.rb +0 -459
- data/lib/giblish/core.rb +0 -451
- data/lib/giblish/docconverter.rb +0 -308
- data/lib/giblish/docid.rb +0 -180
- data/lib/giblish/docinfo.rb +0 -75
- data/lib/giblish/indexheadings.rb +0 -251
- data/lib/giblish-search.cgi +0 -459
- data/scripts/hooks/post-receive +0 -57
- data/scripts/publish_html.sh +0 -99
@@ -0,0 +1,150 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module Giblish
|
4
|
+
# Provides relevant paths for layout resources based on the given options
|
5
|
+
class ResourcePaths
|
6
|
+
STYLE_EXTENSIONS = {
|
7
|
+
"html5" => ".css",
|
8
|
+
"html" => ".css",
|
9
|
+
"pdf" => ".yml",
|
10
|
+
"web-pdf" => ".css"
|
11
|
+
}
|
12
|
+
|
13
|
+
FONT_REGEX = /.*\.(ttf)|(TTF)$/
|
14
|
+
RESOURCE_DST_TOP_BASENAME = Pathname.new("web_assets")
|
15
|
+
|
16
|
+
# the relative path from the top of the resource area to the
|
17
|
+
# style file
|
18
|
+
attr_reader :src_style_path_rel
|
19
|
+
|
20
|
+
# the absolute path from the top of the resource area to the
|
21
|
+
# style file
|
22
|
+
attr_reader :src_style_path_abs
|
23
|
+
|
24
|
+
# a set with all dirs containing ttf files, paths are relative to resource area top
|
25
|
+
attr_reader :font_dirs_abs
|
26
|
+
|
27
|
+
# the relative path from the dst top dir to the copied style file (if it would be copied)
|
28
|
+
attr_reader :dst_style_path_rel
|
29
|
+
|
30
|
+
# the abs path to the top of the destination dir for resources
|
31
|
+
attr_reader :dst_resource_dir_abs
|
32
|
+
|
33
|
+
def initialize(cmd_opts)
|
34
|
+
@style_ext = STYLE_EXTENSIONS.fetch(cmd_opts.format, nil)
|
35
|
+
raise OptionParser::InvalidArgument, "Unsupported format: #{cmd_opts.format}" if @style_ext.nil?
|
36
|
+
|
37
|
+
# Cache all file paths in the resource area
|
38
|
+
r_top = cmd_opts.resource_dir
|
39
|
+
file_tree = PathTree.build_from_fs(r_top)
|
40
|
+
|
41
|
+
# find and validate paths
|
42
|
+
@dst_resource_dir_abs = cmd_opts.dstdir / RESOURCE_DST_TOP_BASENAME
|
43
|
+
@src_style_path_rel = find_style_file(file_tree, cmd_opts)
|
44
|
+
@src_style_path_abs = r_top / @src_style_path_rel
|
45
|
+
@dst_style_path_rel = RESOURCE_DST_TOP_BASENAME / @src_style_path_rel
|
46
|
+
@font_dirs_abs = find_font_dirs(file_tree)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# returns:: the relative path from the top of the file_tree to
|
52
|
+
# the style file
|
53
|
+
def find_style_file(file_tree, cmd_opts)
|
54
|
+
# Get all files matching the style name
|
55
|
+
style_basename = Pathname.new(cmd_opts.style_name).sub_ext(@style_ext)
|
56
|
+
style_tree = file_tree.match(/.*#{style_basename}$/)
|
57
|
+
|
58
|
+
# make sure we have exactly one css file with the given name
|
59
|
+
raise OptionParser::InvalidArgument, "Did not find #{style_basename} under #{file_tree.pathname}" if style_tree.nil?
|
60
|
+
|
61
|
+
l = style_tree.leave_pathnames(prune: true)
|
62
|
+
if l.count > 1
|
63
|
+
raise OptionParser::InvalidArgument, "Found #{l.count} instances of #{style_basename} under #{file_tree.pathname}. Requires exactly one."
|
64
|
+
end
|
65
|
+
|
66
|
+
# return the (pruned) path
|
67
|
+
l[0]
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_font_dirs(file_tree)
|
71
|
+
tree = file_tree.match(FONT_REGEX)
|
72
|
+
dirs = Set.new
|
73
|
+
tree&.traverse_preorder do |level, node|
|
74
|
+
next unless node.leaf?
|
75
|
+
|
76
|
+
dirs << node.pathname.dirname
|
77
|
+
end
|
78
|
+
dirs
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# copy everything under cmd_opts.resource_dir to
|
83
|
+
# dst_top/web_assets/.
|
84
|
+
class CopyResourcesPreBuild
|
85
|
+
# required opts:
|
86
|
+
# resource_dir:: Pathname to the top of the resource area to be copied
|
87
|
+
# style_name:: basename of the css to use for styling
|
88
|
+
# dstdir:: Pathname to the destination dir where the copied resources will be
|
89
|
+
# stored.
|
90
|
+
def initialize(cmd_opts)
|
91
|
+
@opts = cmd_opts.dup
|
92
|
+
@paths = ResourcePaths.new(cmd_opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
def on_prebuild(src_tree, dst_tree, converter)
|
96
|
+
copy_resource_area(@opts)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# copy everyting under cmd_opts.resource_dir to @web_asset_dir
|
102
|
+
def copy_resource_area(cmd_opts)
|
103
|
+
web_assets_dir = @paths.dst_resource_dir_abs
|
104
|
+
web_assets_dir.mkpath unless web_assets_dir.exist?
|
105
|
+
|
106
|
+
resource_dir = cmd_opts.resource_dir.cleanpath.to_s + "/."
|
107
|
+
|
108
|
+
Giblog.logger&.info "Copy web assets (stylesheets et al) from #{resource_dir} to #{web_assets_dir}"
|
109
|
+
FileUtils.cp_r(
|
110
|
+
resource_dir,
|
111
|
+
web_assets_dir.to_s
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class CopyAssetDirsPostBuild
|
117
|
+
def initialize(cmd_opts)
|
118
|
+
@asset_regex = cmd_opts.copy_asset_folders
|
119
|
+
@srcdir = cmd_opts.srcdir
|
120
|
+
@dstdir = cmd_opts.dstdir
|
121
|
+
end
|
122
|
+
|
123
|
+
# Called from TreeConverter during post build phase
|
124
|
+
#
|
125
|
+
# copy all directories matching the regexp pattern from the
|
126
|
+
# src tree to the dst tree
|
127
|
+
def on_postbuild(src_tree, dst_tree, converter)
|
128
|
+
return if @asset_regex.nil?
|
129
|
+
|
130
|
+
# build a tree with all dirs matching the given regexp
|
131
|
+
st = PathTree.build_from_fs(@srcdir, prune: true) do |p|
|
132
|
+
p.directory? && @asset_regex =~ p.to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
return if st.nil?
|
136
|
+
|
137
|
+
Giblog.logger&.info "Copy asset directories from #{@srcdir} to #{@dstdir}"
|
138
|
+
st.traverse_preorder do |level, node|
|
139
|
+
next unless node.leaf?
|
140
|
+
|
141
|
+
n = node.relative_path_from(st)
|
142
|
+
src = @srcdir.join(n)
|
143
|
+
dst = @dstdir.join(n).dirname
|
144
|
+
dst.mkpath
|
145
|
+
|
146
|
+
FileUtils.cp_r(src.to_s, dst.to_s)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "asciidoctor"
|
2
|
+
|
3
|
+
module Giblish
|
4
|
+
# Expands any 'include' preprocessor directives found in the given document
|
5
|
+
# and merges the the lines in the included document with the ones
|
6
|
+
# from the including document.
|
7
|
+
# Nesting of includes is supported to the given level.
|
8
|
+
#
|
9
|
+
# NOTE: Only includes of asciidoc files are supported, other includes (eg url)
|
10
|
+
# are silently dropped.
|
11
|
+
class ExpandAdoc
|
12
|
+
INCLUDE_DIRECTIVE_REGEX = /^(\\)?include::([^\[][^\[]*)\[(.+)?\]$/
|
13
|
+
def initialize(document, target_lines, max_depth = 3)
|
14
|
+
source_lines = document.reader.source_lines
|
15
|
+
source_lines.each do |line|
|
16
|
+
if INCLUDE_DIRECTIVE_REGEX =~ line
|
17
|
+
next unless max_depth > 0
|
18
|
+
|
19
|
+
p = resolve_include_path(document, $2, $3)
|
20
|
+
next if p.nil?
|
21
|
+
|
22
|
+
sub_doc = Asciidoctor.load_file(p, {parse: false, safe: :unsafe})
|
23
|
+
ExpandAdoc.new(sub_doc, target_lines, max_depth - 1)
|
24
|
+
else
|
25
|
+
target_lines << wash_line(document, line)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def resolve_include_path(document, target, attrlist)
|
31
|
+
target = replace_attrs(document.attributes, target)
|
32
|
+
parsed_attrs = document.parse_attributes attrlist, [], sub_input: true
|
33
|
+
|
34
|
+
# use an asciidoctor-internal method to resolve the path in an attempt to keep compatibility
|
35
|
+
inc_path, target_type, _relpath = document.reader.send(:resolve_include_path, target, attrlist, parsed_attrs)
|
36
|
+
return nil unless target_type == :file
|
37
|
+
|
38
|
+
inc_path
|
39
|
+
end
|
40
|
+
|
41
|
+
def wash_line(document, line)
|
42
|
+
replace_attrs(document.attributes, line)
|
43
|
+
end
|
44
|
+
|
45
|
+
# replace {a_doc_attr} with the value of the attribute
|
46
|
+
def replace_attrs(attrs, line)
|
47
|
+
# find all '{...}' occurrences
|
48
|
+
m_arr = line.scan(/\{\w+\}/)
|
49
|
+
# replace each found occurence with its doc attr if exists
|
50
|
+
m_arr.inject(line) do |memo, match|
|
51
|
+
attrs.key?(match[1..-2]) ? memo.gsub(match.to_s, attrs[match[1..-2]]) : memo
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "json"
|
5
|
+
require "pathname"
|
6
|
+
require "asciidoctor"
|
7
|
+
require "asciidoctor/extensions"
|
8
|
+
require_relative "../utils"
|
9
|
+
require_relative "expand_adoc"
|
10
|
+
|
11
|
+
module Giblish
|
12
|
+
# Implements both an Asciidoctor TreeProcessor hook and a giblish post-build
|
13
|
+
# hook.
|
14
|
+
#
|
15
|
+
# The TreeProcessor hook indexes all headings found in all
|
16
|
+
# documents in the tree and copies a 'washed' version of the source lines
|
17
|
+
# to a search asset folder in the destination tree.
|
18
|
+
#
|
19
|
+
# The post build hook copies the completed heading index to the search
|
20
|
+
# assets folder.
|
21
|
+
#
|
22
|
+
# Format of the heading index database:
|
23
|
+
# {
|
24
|
+
# fileinfos : [{
|
25
|
+
# filepath : filepath_1,
|
26
|
+
# title : Title,
|
27
|
+
# sections : [{
|
28
|
+
# id : section_id_1,
|
29
|
+
# title : section_title_1,
|
30
|
+
# line_no : line_no
|
31
|
+
# },
|
32
|
+
# {
|
33
|
+
# id : section_id_1,
|
34
|
+
# title : section_title_1,
|
35
|
+
# line_no : line_no
|
36
|
+
# },
|
37
|
+
# ...
|
38
|
+
# ]
|
39
|
+
# },
|
40
|
+
# {
|
41
|
+
# filepath : filepath_2,
|
42
|
+
# ...
|
43
|
+
# }]
|
44
|
+
# }
|
45
|
+
class HeadingIndexer < Asciidoctor::Extensions::TreeProcessor
|
46
|
+
HEADING_REGEX = /^=+\s+(.*)$/
|
47
|
+
ANCHOR_REGEX = /^\[\[(\w+)\]\]\s*$/
|
48
|
+
HEADING_DB_BASENAME = "heading_db.json"
|
49
|
+
SEARCH_ASSET_DIRNAME = "gibsearch_assets"
|
50
|
+
|
51
|
+
# src_topdir:: a Pathname to the top dir of the src files
|
52
|
+
def initialize(src_topdir)
|
53
|
+
super({})
|
54
|
+
|
55
|
+
@src_topdir = src_topdir
|
56
|
+
@heading_index = {fileinfos: []}
|
57
|
+
end
|
58
|
+
|
59
|
+
# called by Asciidoctor during the conversion of the document.
|
60
|
+
def process(document)
|
61
|
+
attrs = document.attributes
|
62
|
+
src_node = attrs["giblish-info"][:src_node]
|
63
|
+
|
64
|
+
# only index source files that reside on the 'physical' file system
|
65
|
+
return if src_node.nil? || !src_node.pathname.exist?
|
66
|
+
|
67
|
+
# make sure we use the correct id elements when indexing
|
68
|
+
# sections
|
69
|
+
opts = {
|
70
|
+
id_prefix: (attrs.key?("idprefix") ? attrs["idprefix"] : "_"),
|
71
|
+
id_separator: (attrs.key?("id_separator") ? attrs["id_separator"] : "_")
|
72
|
+
}
|
73
|
+
|
74
|
+
# index sections and wash source lines
|
75
|
+
# Copy the washed document to the search asset folder
|
76
|
+
dst_top = attrs["giblish-info"][:dst_top]
|
77
|
+
write_washed_doc(
|
78
|
+
parse_document(document, src_node, opts),
|
79
|
+
dst_top.pathname / SEARCH_ASSET_DIRNAME / rel_src_path(src_node)
|
80
|
+
)
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# called by the TreeConverter during the post_build phase
|
85
|
+
def on_postbuild(src_topdir, dst_tree, converter)
|
86
|
+
search_topdir = dst_tree.pathname / SEARCH_ASSET_DIRNAME
|
87
|
+
|
88
|
+
# store the JSON file
|
89
|
+
serialize_section_index(search_topdir, search_topdir)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# get the relative path from the src_topdirdir to the source node
|
95
|
+
#
|
96
|
+
# returns:: a Pathname with the relative path
|
97
|
+
def rel_src_path(src_node)
|
98
|
+
src_node.pathname.relative_path_from(@src_topdir)
|
99
|
+
end
|
100
|
+
|
101
|
+
# returns:: the source lines after substituting attributes
|
102
|
+
def parse_document(document, src_node, opts)
|
103
|
+
Giblog.logger.debug "index headings in #{src_node.pathname} using prefix '#{opts[:id_prefix]}' and separator '#{opts[:id_separator]}'"
|
104
|
+
attrs = document.attributes
|
105
|
+
expanded_lines = []
|
106
|
+
ExpandAdoc.new(document, expanded_lines, 3)
|
107
|
+
# puts expanded_lines
|
108
|
+
|
109
|
+
doc_info = index_sections(expanded_lines, opts)
|
110
|
+
|
111
|
+
@heading_index[:fileinfos] << {
|
112
|
+
filepath: rel_src_path(src_node),
|
113
|
+
title: attrs.key?("doctitle") ? attrs["doctitle"] : "No title found!",
|
114
|
+
sections: doc_info[:sections]
|
115
|
+
}
|
116
|
+
doc_info[:washed_lines]
|
117
|
+
end
|
118
|
+
|
119
|
+
# lines:: [lines]
|
120
|
+
# dst_path:: Pathname to destination file
|
121
|
+
def write_washed_doc(lines, dst_path)
|
122
|
+
Giblog.logger.debug { "Copy searchable text to #{dst_path}" }
|
123
|
+
dst_path.dirname.mkpath
|
124
|
+
File.write(dst_path.to_s, lines.join("\n"))
|
125
|
+
end
|
126
|
+
|
127
|
+
# replace {a_doc_attr} with the value of the attribute
|
128
|
+
def replace_attrs(attrs, line)
|
129
|
+
# find all '{...}' occurrences
|
130
|
+
m_arr = line.scan(/\{\w+\}/)
|
131
|
+
# replace each found occurence with its doc attr if exists
|
132
|
+
m_arr.inject(line) do |memo, match|
|
133
|
+
attrs.key?(match[1..-2]) ? memo.gsub(match.to_s, attrs[match[1..-2]]) : memo
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# provide a 'washed' version of all source lines in the document and
|
138
|
+
# index all sections.
|
139
|
+
#
|
140
|
+
# returns:: { washed_lines: [lines], sections: [{:id, :title, :line_no}]}
|
141
|
+
def index_sections(lines, opts)
|
142
|
+
indexed_doc = {
|
143
|
+
washed_lines: [],
|
144
|
+
sections: []
|
145
|
+
}
|
146
|
+
sections = indexed_doc[:sections]
|
147
|
+
# lines = document.reader.source_lines.dup
|
148
|
+
|
149
|
+
line_no = 0
|
150
|
+
match_str = ""
|
151
|
+
state = :text
|
152
|
+
lines.each do |line|
|
153
|
+
line_no += 1
|
154
|
+
# line = replace_attrs(document.attributes, line)
|
155
|
+
indexed_doc[:washed_lines] << line.strip
|
156
|
+
|
157
|
+
# implement a state machine that supports both custom
|
158
|
+
# anchors for a heading and the default heading ids generated
|
159
|
+
# by asciidoctor
|
160
|
+
case state
|
161
|
+
when :text
|
162
|
+
case line
|
163
|
+
# detect a heading or an anchor preceeding a heading
|
164
|
+
when HEADING_REGEX
|
165
|
+
state = :heading
|
166
|
+
match_str = Regexp.last_match(1)
|
167
|
+
when ANCHOR_REGEX
|
168
|
+
state = :expecting_heading
|
169
|
+
match_str = Regexp.last_match(1)
|
170
|
+
end
|
171
|
+
when :expecting_heading
|
172
|
+
case line
|
173
|
+
when HEADING_REGEX
|
174
|
+
# we got a heading, index it
|
175
|
+
sections << {
|
176
|
+
"id" => match_str,
|
177
|
+
"title" => Regexp.last_match(1).strip,
|
178
|
+
"line_no" => line_no
|
179
|
+
}
|
180
|
+
else
|
181
|
+
# we did not get a heading, this is ok as well but we can not
|
182
|
+
# index it
|
183
|
+
Giblog.logger.debug do
|
184
|
+
"Did not index the anchor: #{match_str} at "\
|
185
|
+
"line #{line_no}, probably not associated with a heading."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
state = :text
|
189
|
+
when :heading
|
190
|
+
# last line was a heading without an accompanying anchor, index it
|
191
|
+
# by creating a new, unique id that matches the one that
|
192
|
+
# asciidoctor will assign to this heading when generating html
|
193
|
+
sections << {
|
194
|
+
"id" => create_unique_id(sections, match_str, opts),
|
195
|
+
"title" => Regexp.last_match(1).strip,
|
196
|
+
"line_no" => line_no - 1
|
197
|
+
}
|
198
|
+
state = :text
|
199
|
+
end
|
200
|
+
end
|
201
|
+
indexed_doc
|
202
|
+
end
|
203
|
+
|
204
|
+
# find the section id delimiters from the document
|
205
|
+
# header, if they are set there
|
206
|
+
def find_section_id_attributes(lines, result)
|
207
|
+
Giblish.process_header_lines(lines) do |line|
|
208
|
+
m = /^:idprefix:(.*)$/.match(line)
|
209
|
+
n = /^:idseparator:(.*)$/.match(line)
|
210
|
+
result[:id_prefix] = m[1].strip if m && !result[:id_prefix]
|
211
|
+
result[:id_separator] = n[1].strip if n && !result[:id_separator]
|
212
|
+
end
|
213
|
+
result
|
214
|
+
end
|
215
|
+
|
216
|
+
# create the anchor id for the given heading in a way that complies
|
217
|
+
# with how asciidoctor creates the same id when generating html
|
218
|
+
def create_unique_id(sections, heading_str, opts)
|
219
|
+
# create the 'default' id the same way as asciidoctor will do it
|
220
|
+
id_base = Giblish.to_valid_id(heading_str, opts[:id_prefix], opts[:id_separator])
|
221
|
+
return id_base unless sections.find { |s| s["id"] == id_base }
|
222
|
+
|
223
|
+
# handle the case with several sections with the same name by adding
|
224
|
+
# a sequence number at the end
|
225
|
+
idx = 1
|
226
|
+
heading_id = ""
|
227
|
+
loop do
|
228
|
+
heading_id = "#{id_base}_#{idx += 1}"
|
229
|
+
break unless sections.find { |s| s["id"] == heading_id }
|
230
|
+
end
|
231
|
+
heading_id
|
232
|
+
end
|
233
|
+
|
234
|
+
# write the index to a file in dst_dir and remove the base_dir
|
235
|
+
# part of the path for each filename
|
236
|
+
def serialize_section_index(dst_dir, base_dir)
|
237
|
+
dst_dir.mkpath
|
238
|
+
|
239
|
+
heading_db_path = dst_dir.join(HEADING_DB_BASENAME)
|
240
|
+
Giblog.logger.info { "writing json to #{heading_db_path}" }
|
241
|
+
|
242
|
+
File.write(heading_db_path.to_s, @heading_index.to_json)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class AddSearchForm < Asciidoctor::Extensions::DocinfoProcessor
|
247
|
+
use_dsl
|
248
|
+
at_location :header
|
249
|
+
|
250
|
+
FORM_DATA = <<~FORM_HTML
|
251
|
+
<script type="text/javascript">
|
252
|
+
window.onload = function () {
|
253
|
+
document.getElementById("calingurl_input").value = window.location.href;
|
254
|
+
};
|
255
|
+
</script>
|
256
|
+
|
257
|
+
<style>
|
258
|
+
#gibsearch-form {
|
259
|
+
position:fixed;
|
260
|
+
top:0.5rem;
|
261
|
+
left:70%;
|
262
|
+
width:30%;
|
263
|
+
height:3rem;
|
264
|
+
background:white;
|
265
|
+
z-index:2000;
|
266
|
+
}
|
267
|
+
</style>
|
268
|
+
|
269
|
+
<div id=gibsearch-form>
|
270
|
+
<form class="gibsearch" action="<%=action_path%>">
|
271
|
+
<input type="search" placeholder="Search the docs.." name="search-phrase" />
|
272
|
+
<button type="submit">Search</button>
|
273
|
+
<br>
|
274
|
+
|
275
|
+
<input type="checkbox" id="consider-case" name="consider-case" />
|
276
|
+
<label for="consider-case">case sensitive</label>
|
277
|
+
|
278
|
+
<input type="checkbox" id="as-regexp" name="as-regexp" />
|
279
|
+
<label for="as-regexp">use regexp</label>
|
280
|
+
|
281
|
+
<input type="hidden" name="calling-url" id=calingurl_input />
|
282
|
+
<input type="hidden" name="search-assets-top-rel" value="<%=sa_top_rel%>"/>
|
283
|
+
<input type="hidden" name="css-path" value="<%=css_path%>"/>
|
284
|
+
</form>
|
285
|
+
</div>
|
286
|
+
FORM_HTML
|
287
|
+
|
288
|
+
def initialize(action_path = nil, opts = {})
|
289
|
+
@action_path = action_path
|
290
|
+
super(opts)
|
291
|
+
end
|
292
|
+
|
293
|
+
def process(document)
|
294
|
+
attrs = document.attributes
|
295
|
+
src_node = attrs["giblish-info"][:src_node]
|
296
|
+
dst_node = attrs["giblish-info"][:dst_node]
|
297
|
+
dst_top = attrs["giblish-info"][:dst_top]
|
298
|
+
|
299
|
+
to_top_rel = dst_top.relative_path_from(dst_node.parent)
|
300
|
+
sa_top_rel = to_top_rel.join("gibsearch_assets").cleanpath
|
301
|
+
action_path = @action_path || to_top_rel.join("gibsearch.cgi").cleanpath
|
302
|
+
|
303
|
+
# only include css_path if it is given in the document's attributes
|
304
|
+
doc_attrs = src_node.data.document_attributes(src_node, dst_node, dst_top)
|
305
|
+
css_path = if %w[stylesdir stylesheet].all? { |k| doc_attrs.key?(k) }
|
306
|
+
doc_attrs["stylesdir"] + "/" + doc_attrs["stylesheet"]
|
307
|
+
end
|
308
|
+
|
309
|
+
ERB.new(FORM_DATA).result(binding)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "asciidoctor"
|
2
|
+
require_relative "textsearcher"
|
3
|
+
|
4
|
+
module Giblish
|
5
|
+
class DefaultHtmlGenerator
|
6
|
+
# search_result:: a hash conforming to the output of
|
7
|
+
# the TextSearcher::search method.
|
8
|
+
# returns:: a string with the html to return to the client
|
9
|
+
def response(search_result, css_path = nil)
|
10
|
+
adoc_2_html(search_2_adoc(search_result), css_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def adoc_2_html(adoc_source, css_path = nil)
|
16
|
+
# setup default document attributes used in the html conversion
|
17
|
+
doc_attr = css_path ? {
|
18
|
+
"stylesdir" => css_path.dirname.to_s,
|
19
|
+
"stylesheet" => css_path.basename.to_s,
|
20
|
+
"linkcss" => true,
|
21
|
+
"copycss" => nil
|
22
|
+
} : {}
|
23
|
+
doc_attr["data-uri"] = 1
|
24
|
+
doc_attr["example-caption"] = nil
|
25
|
+
|
26
|
+
# setup default conversion options
|
27
|
+
converter_options = {
|
28
|
+
backend: "html5",
|
29
|
+
# need this to let asciidoctor include the default css if user
|
30
|
+
# has not specified any css
|
31
|
+
safe: Asciidoctor::SafeMode::SAFE,
|
32
|
+
header_footer: true,
|
33
|
+
attributes: doc_attr
|
34
|
+
}
|
35
|
+
|
36
|
+
# for debugging of adoc source
|
37
|
+
# File.write("search.adoc", adoc_source)
|
38
|
+
|
39
|
+
# convert to html and return result
|
40
|
+
Asciidoctor.convert(adoc_source, converter_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# search_result:: a hash conforming to the output of
|
44
|
+
# the TextSearcher::search method.
|
45
|
+
def search_2_adoc(search_result)
|
46
|
+
str = ""
|
47
|
+
search_result.each do |filepath, info|
|
48
|
+
str << ".From: '#{info[:doc_title]}'\n"
|
49
|
+
str << "====\n\n"
|
50
|
+
info[:sections].each do |section|
|
51
|
+
str << "#{section[:url]}[#{section[:title]}]::\n\n"
|
52
|
+
section[:lines].each do |line|
|
53
|
+
str << line
|
54
|
+
end.join("\n+\n")
|
55
|
+
str << "\n\n"
|
56
|
+
end
|
57
|
+
str << "\n"
|
58
|
+
str << "====\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
<<~ADOC
|
62
|
+
= Search Result
|
63
|
+
|
64
|
+
#{str}
|
65
|
+
ADOC
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# A gateway class that implements everything needed to produce
|
70
|
+
# an html page with search results given a search request.
|
71
|
+
#
|
72
|
+
# The class implements internal caching for better performance. It
|
73
|
+
# is thus probably wise to instantiate this class once and then use
|
74
|
+
# that instance for all subsequent search queries.
|
75
|
+
class RequestManager
|
76
|
+
# url_path_mappings:: a Hash with mappings from url paths to
|
77
|
+
# local file system directories.
|
78
|
+
# html_generator:: an object that generates html by implementing
|
79
|
+
# the method 'def response(search_result, css_path = nil)'. See
|
80
|
+
# eg DefaultHtmlGenerator. If nil, a DefaultHtmlGenerator is used.
|
81
|
+
def initialize(uri_mappings, html_generator = nil)
|
82
|
+
@uri_mappings = uri_mappings || {"/" => "/var/www/html/"}
|
83
|
+
|
84
|
+
@html_generator = html_generator || DefaultHtmlGenerator.new
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return an html page with the search result from the given query.
|
88
|
+
#
|
89
|
+
# search_params:: a Hash containing the parameters of the search query.
|
90
|
+
def response(search_params)
|
91
|
+
sp = SearchParameters.from_hash(search_params, uri_mappings: @uri_mappings)
|
92
|
+
@html_generator.response(searcher.search(sp), sp.css_path)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# a convenience method to give shorter access to the class-wide
|
98
|
+
# TextSearcher instance.
|
99
|
+
def searcher
|
100
|
+
RequestManager.searcher
|
101
|
+
end
|
102
|
+
|
103
|
+
# provide a class-wide TextSearcher instance
|
104
|
+
class << self
|
105
|
+
def searcher
|
106
|
+
@searcher ||= TextSearcher.new(SearchRepoCache.new)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Giblish
|
2
|
+
class SearchQuery
|
3
|
+
attr_reader :parameters
|
4
|
+
|
5
|
+
REQUIRED_PARAMS = %w[calling-url search-assets-top-rel search-phrase]
|
6
|
+
OPTIONAL_PARAMS = %w[css-path consider-case as-regexp]
|
7
|
+
|
8
|
+
def initialize(uri: nil, query_params: nil)
|
9
|
+
@parameters = case [uri, query_params]
|
10
|
+
in [String, nil]
|
11
|
+
uri_2_params(uri)
|
12
|
+
in [nil, _]
|
13
|
+
validate_parameters(query_params)
|
14
|
+
else
|
15
|
+
raise ArgumentError, "You must supply one of 'uri: [String]' or 'query_params: [Hash]' got: #{query_params}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def css_path
|
20
|
+
@parameters.key?("css-path") && !@parameters["css-path"].empty? ? Pathname.new(@parameters["css-path"]) : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def search_assets_top_rel
|
24
|
+
# convert to pathname and make sure we always are relative
|
25
|
+
Pathname.new("./" + @parameters["search-assets-top-rel"]).cleanpath
|
26
|
+
end
|
27
|
+
|
28
|
+
def consider_case?
|
29
|
+
@parameters.key?("consider-case")
|
30
|
+
end
|
31
|
+
|
32
|
+
def as_regexp?
|
33
|
+
@parameters.key?("as-regexp")
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(meth, *args, &block)
|
37
|
+
unless respond_to_missing?(meth)
|
38
|
+
super(meth, *args, &block)
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
@parameters[meth.to_s.tr("_", "-")]
|
43
|
+
end
|
44
|
+
|
45
|
+
def respond_to_missing?(method_name, include_private = false)
|
46
|
+
@parameters.key?(method_name.to_s.tr("_", "-"))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def uri_2_params(uri_str)
|
52
|
+
uri = URI(uri_str)
|
53
|
+
raise ArgumentError, "No query parameters found!" if uri.query.nil?
|
54
|
+
|
55
|
+
parameters = URI.decode_www_form(uri.query).to_h
|
56
|
+
validate_parameters(parameters)
|
57
|
+
end
|
58
|
+
|
59
|
+
# validate that all required parameters are included
|
60
|
+
def validate_parameters(uri_params)
|
61
|
+
REQUIRED_PARAMS.each do |p|
|
62
|
+
raise ArgumentError, "Missing or empty parameter: #{p}" if !uri_params.key?(p) || uri_params[p].empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
uri_params
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|