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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/unit_tests.yml +30 -0
  3. data/.gitignore +7 -3
  4. data/.ruby-version +1 -1
  5. data/Changelog.adoc +61 -0
  6. data/README.adoc +267 -0
  7. data/docs/concepts/text_search.adoc +213 -0
  8. data/docs/concepts/text_search_im/cgi-search_request.puml +35 -0
  9. data/docs/concepts/text_search_im/cgi-search_request.svg +397 -0
  10. data/docs/concepts/text_search_im/search_request.puml +40 -0
  11. data/docs/concepts/text_search_im/search_request.svg +408 -0
  12. data/docs/howtos/trigger_generation.adoc +180 -0
  13. data/docs/{setup_server_assets → howtos/trigger_generation_im}/Render Documents.png +0 -0
  14. data/docs/{setup_server_assets → howtos/trigger_generation_im}/View Documents.png +0 -0
  15. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.graphml +0 -0
  16. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.svg +0 -0
  17. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.graphml +0 -0
  18. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.svg +0 -0
  19. data/docs/howtos/trigger_generation_im/docgen_github.puml +51 -0
  20. data/docs/{setup_server_assets → howtos/trigger_generation_im}/giblish_deployment.graphml +0 -0
  21. data/docs/howtos/trigger_generation_im/post-receive-example.sh +50 -0
  22. data/docs/reference/box_flow_spec.adoc +22 -0
  23. data/docs/reference/search_spec.adoc +185 -0
  24. data/giblish.gemspec +47 -29
  25. data/lib/giblish/adocsrc_providers.rb +23 -0
  26. data/lib/giblish/application.rb +214 -41
  27. data/lib/giblish/cmdline.rb +273 -259
  28. data/lib/giblish/config_utils.rb +41 -0
  29. data/lib/giblish/configurator.rb +163 -0
  30. data/lib/giblish/conversion_info.rb +120 -0
  31. data/lib/giblish/docattr_providers.rb +125 -0
  32. data/lib/giblish/docid/docid.rb +181 -0
  33. data/lib/giblish/github_trigger/webhook_manager.rb +64 -0
  34. data/lib/giblish/gitrepos/checkoutmanager.rb +124 -0
  35. data/lib/giblish/{gititf.rb → gitrepos/gititf.rb} +30 -4
  36. data/lib/giblish/gitrepos/gitsummary.erb +61 -0
  37. data/lib/giblish/gitrepos/gitsummaryprovider.rb +78 -0
  38. data/lib/giblish/gitrepos/history_pb.rb +41 -0
  39. data/lib/giblish/indexbuilders/d3treegraph.rb +88 -0
  40. data/lib/giblish/indexbuilders/depgraphbuilder.rb +109 -0
  41. data/lib/giblish/indexbuilders/dotdigraphadoc.rb +174 -0
  42. data/lib/giblish/indexbuilders/standard_index.erb +10 -0
  43. data/lib/giblish/indexbuilders/subtree_indices.rb +132 -0
  44. data/lib/giblish/indexbuilders/templates/circles.html.erb +111 -0
  45. data/lib/giblish/indexbuilders/templates/flame.html.erb +61 -0
  46. data/lib/giblish/indexbuilders/templates/tree.html.erb +366 -0
  47. data/lib/giblish/indexbuilders/templates/treemap.html.erb +127 -0
  48. data/lib/giblish/indexbuilders/verbatimtree.rb +94 -0
  49. data/lib/giblish/pathtree.rb +473 -74
  50. data/lib/giblish/resourcepaths.rb +150 -0
  51. data/lib/giblish/search/expand_adoc.rb +55 -0
  52. data/lib/giblish/search/headingindexer.rb +312 -0
  53. data/lib/giblish/search/request_manager.rb +110 -0
  54. data/lib/giblish/search/searchquery.rb +68 -0
  55. data/lib/giblish/search/textsearcher.rb +349 -0
  56. data/lib/giblish/subtreeinfobuilder.rb +77 -0
  57. data/lib/giblish/treeconverter.rb +272 -0
  58. data/lib/giblish/utils.rb +142 -294
  59. data/lib/giblish/version.rb +1 -1
  60. data/lib/giblish.rb +10 -7
  61. data/scripts/hooks/post-receive.example +66 -0
  62. data/{docgen/scripts/githook_examples → scripts/hooks}/post-update.example +0 -0
  63. data/{docgen → scripts}/resources/css/adoc-colony.css +0 -0
  64. data/scripts/resources/css/giblish-serif.css +419 -0
  65. data/scripts/resources/css/giblish.css +1979 -419
  66. data/{docgen → scripts}/resources/fonts/Ubuntu-B.ttf +0 -0
  67. data/{docgen → scripts}/resources/fonts/Ubuntu-BI.ttf +0 -0
  68. data/{docgen → scripts}/resources/fonts/Ubuntu-R.ttf +0 -0
  69. data/{docgen → scripts}/resources/fonts/Ubuntu-RI.ttf +0 -0
  70. data/{docgen → scripts}/resources/fonts/mplus1p-regular-fallback.ttf +0 -0
  71. data/{docgen → scripts}/resources/images/giblish_logo.png +0 -0
  72. data/{docgen → scripts}/resources/images/giblish_logo.svg +0 -0
  73. data/{docgen → scripts}/resources/themes/giblish.yml +0 -0
  74. data/scripts/wserv_development.rb +32 -0
  75. data/web_apps/cgi_search/gibsearch.rb +43 -0
  76. data/web_apps/gh_webhook_trigger/config.ru +2 -0
  77. data/web_apps/gh_webhook_trigger/gh_webhook_trigger.rb +73 -0
  78. data/web_apps/gh_webhook_trigger/public/dummy.txt +3 -0
  79. data/web_apps/sinatra_search/config.ru +2 -0
  80. data/web_apps/sinatra_search/public/dummy.txt +3 -0
  81. data/web_apps/sinatra_search/sinatra_search.rb +34 -0
  82. data/web_apps/sinatra_search/tmp/restart.txt +0 -0
  83. metadata +168 -73
  84. data/.rubocop.yml +0 -7
  85. data/.travis.yml +0 -3
  86. data/Changelog +0 -16
  87. data/Gemfile +0 -4
  88. data/README.adoc +0 -1
  89. data/Rakefile +0 -41
  90. data/bin/console +0 -14
  91. data/bin/setup +0 -8
  92. data/data/testdocs/malformed/no_header.adoc +0 -5
  93. data/data/testdocs/toplevel.adoc +0 -19
  94. data/data/testdocs/wellformed/adorned_purpose.adoc +0 -17
  95. data/data/testdocs/wellformed/docidtest/docid_1.adoc +0 -24
  96. data/data/testdocs/wellformed/docidtest/docid_2.adoc +0 -8
  97. data/data/testdocs/wellformed/simple.adoc +0 -14
  98. data/data/testdocs/wellformed/source_highlighting/highlight_source.adoc +0 -38
  99. data/docgen/resources/css/giblish.css +0 -1979
  100. data/docgen/scripts/Jenkinsfile +0 -18
  101. data/docgen/scripts/gen_adoc_org.sh +0 -58
  102. data/docs/README.adoc +0 -387
  103. data/docs/setup_server.adoc +0 -202
  104. data/lib/giblish/buildgraph.rb +0 -216
  105. data/lib/giblish/buildindex.rb +0 -459
  106. data/lib/giblish/core.rb +0 -451
  107. data/lib/giblish/docconverter.rb +0 -308
  108. data/lib/giblish/docid.rb +0 -180
  109. data/lib/giblish/docinfo.rb +0 -75
  110. data/lib/giblish/indexheadings.rb +0 -251
  111. data/lib/giblish-search.cgi +0 -459
  112. data/scripts/hooks/post-receive +0 -57
  113. 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
+ &nbsp;&nbsp;
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