giblish 0.5.2 → 0.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d917c8b6c513f7759aecbbdb5a93a7cb13cb5774d66ccf0d602a615248723f76
4
- data.tar.gz: b94be771b19dd5890e238ffc0ee0ee5af361919c0e526d63ac4f380b4f6698a0
3
+ metadata.gz: 70c09507979ec9eacf59cfcb8405c929b35b7c24882db0099d4493c18439b754
4
+ data.tar.gz: e707817cee1413bbc9fd28a04ac54a1f5147176ef01070e189096a387620a0e9
5
5
  SHA512:
6
- metadata.gz: 4c9f6b75a6266c65baf0e7876016139703b7b46677fea2dcee54b9ad95b23c04756c8b9383398cdd8055c91e78ea3d275c7968eeabf1154478a44b76faf6c27c
7
- data.tar.gz: c7cb03e7fe95cdf3a13a74c36f0d5efbb06e49828ff04db05b5d231f098bb411ab34584067e03b7a3b93c025224d3d93ccd13366e951fe93826cfdafeaae1bd9
6
+ metadata.gz: 54fbe435b49ec492065b3ba9f9c6bab31d08e56f5843937b36a4bd8c0bd4253ec6bacecc5f2cc530a1ebb188e8289c498fd68d8a7a6951b7b3831d2e7956bffe
7
+ data.tar.gz: 83eb9c41b4437f29e4e779ca7782bc464d42daffd97e0d36ef8482f7155c2ce9364097b97745d700773368c419354d086d2b767e08d1758f9c7ddb11dfdb6aca
@@ -174,3 +174,82 @@ cgi-script must be located at http://your_web_site.com/cgi-bin/giblish-search.cg
174
174
  and this gem provides a default implementation that you can copy from the .../lib
175
175
  folder to the correct destination.
176
176
  ====
177
+
178
+ == Text search implementation
179
+
180
+ The text search enables a user to search for a text string and receive matching
181
+ sections in the documentation tree.
182
+
183
+ To make this work, three things are needed
184
+
185
+ . the source text of all adoc files together with a JSON file that maps sections to
186
+ their line numbers. This 'search data' is collected by giblish when it generates the
187
+ html files to the destination directory. The JSON file and all adoc source files
188
+ are copied to a well-known place in the destination tree (see below).
189
+ . an html form somewhere in the rendered pages that receives the user input (text string
190
+ and other parameters) to start a search.
191
+ ** giblish injects such an html form in the generated index page when the user
192
+ specifies the '-m' switch.
193
+ . a server side script that
194
+ ** receive a text string to search for
195
+ ** parses the search data for matches to the text string
196
+ ** presents the result to the user
197
+
198
+ This gem contains an example implementation of a server side script. It is implemented
199
+ in ruby and uses 'grep' to parse the search data. It then generates a result page where
200
+ each matching section is shown and when clicked, will navigate the user to that section
201
+ in the corresponding document.
202
+
203
+ === Search data and html form parameters
204
+
205
+ giblish will copy all search data to a 'search_assets' dir just under the destination
206
+ root. This is illustrated below.
207
+
208
+ .When rendering documents from a git branch
209
+ dst_root_dir
210
+ |- branch_1_top_dir
211
+ | |- index.html
212
+ | |- file_1.html
213
+ | |- dir_1
214
+ | | |- file2.html
215
+ |- branch_2_top_dir
216
+ |- branch_x_...
217
+ |- web_assets
218
+ |- search_assets
219
+ | |- branch_1_top_dir
220
+ | |- heading_index.json
221
+ | |- file1.adoc
222
+ | |- dir_1
223
+ | | |- file2.adoc
224
+ | |- ...
225
+ | |- branch_2_top_dir
226
+ | | ...
227
+
228
+ assume that the file tree looks like this when not
229
+ rendering a git branch:
230
+
231
+ .When rendering documents not in a git branch
232
+ dst_root_dir
233
+ |- index.html
234
+ |- file_1.html
235
+ |- dir_1
236
+ | |- file2.html
237
+ |...
238
+ |- web_assets (only if a custom stylesheet is used...)
239
+ |- search_assets
240
+ | |- heading_index.json
241
+ | |- file1.adoc
242
+ | |- dir_1
243
+ | | |- file2.adoc
244
+ | |- ...
245
+
246
+ The parameters that is sent to the cgi script via the html form generated
247
+ by giblish are:
248
+
249
+ * searchphrase -> the text string to search for
250
+ * ignorecase -> wether to ignore case or not
251
+ * useregexp -> wether the searchphrase above is treated as a regexp or
252
+ ordinary text
253
+ * css -> the css file name to use when styling the search result page
254
+ * topdir -> the absolute path to the root of the generated document tree
255
+ * reltop -> <clarify this>
data/Rakefile CHANGED
@@ -13,6 +13,24 @@ Rake::TestTask.new(:current) do |t|
13
13
  t.test_files = FileList["test/**/index_heading_test.rb"]
14
14
  end
15
15
 
16
+ Rake::TestTask.new(:paths) do |t|
17
+ t.libs << "test"
18
+ t.libs << "lib"
19
+ t.test_files = FileList["test/**/pathmanager_test.rb"]
20
+ end
21
+
22
+ Rake::TestTask.new(:graph) do |t|
23
+ t.libs << "test"
24
+ t.libs << "lib"
25
+ t.test_files = FileList["test/**/depgraph_test.rb"]
26
+ end
27
+
28
+ Rake::TestTask.new(:css) do |t|
29
+ t.libs << "test"
30
+ t.libs << "lib"
31
+ t.test_files = FileList["test/**/linkcss_test.rb"]
32
+ end
33
+
16
34
  Rake::TestTask.new(:sandbox) do |t|
17
35
  t.libs << "test"
18
36
  t.libs << "lib"
@@ -0,0 +1,18 @@
1
+ pipeline {
2
+ agent any
3
+ stages {
4
+ stage('Render html documentation') {
5
+ steps {
6
+ echo "workspace path: ${env.WORKSPACE}"
7
+ // render html versions of all docs found under the 'docs' subdir
8
+ // in the repo
9
+ sh "giblish -n -f html -w ${env.WORKSPACE} -s giblish -r docgen/resources docs gendocs"
10
+ }
11
+ }
12
+ // stage('Render pdf documentation') {
13
+ // steps {
14
+ // sh "giblish -n -f pdf -s vironova-theme -r scripts/adocgen/resources Documents/MiniTEM/third_party_software/ MiniTEM/Deployment/doc"
15
+ // }
16
+ // }
17
+ }
18
+ }
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ #
3
+ # This hook script kicks-in after git has completed
4
+ # a push, it will thus never be able to abort a push.
5
+ #
6
+ # This hook will:
7
+ # - ping Jenkins to trigger any builds related to the
8
+ # git push
9
+
10
+ # let the user perfoming a git push know that we actually do something
11
+ echo "Start post-update"
12
+
13
+ # Hit Jenkins to initiate a Jenkins poll for which jobs that shall be started as
14
+ # consequence of a push to a specific git repo. The generic format for this is:
15
+ # curl <jenkins_url>/git/notifyCommit?url=<git repo url>
16
+ #
17
+ # If jenkins is accessible on http://jenkins.example.com:8080 and
18
+ # you want to initiate a poll for jobs associated with the giblish repository on
19
+ # github located at https://github.com/rillbert/giblish.git
20
+ # you would use the following:
21
+ curl http://jenkins.example.com:8080/git/notifyCommit?url=https://github.com/rillbert/giblish.git
22
+
23
+ # Tell user we're done
24
+ echo "Finished post-update"
@@ -0,0 +1,67 @@
1
+ = Setup web site powered by giblish
2
+ :imagesdir: setup_server_assets
3
+ :numbered:
4
+
5
+ == Purpose
6
+
7
+ To describe how to setup a web site with documents automatically generated from a git repo.
8
+
9
+ == Toolchain
10
+
11
+ * git
12
+ * giblish
13
+ * git server side hooks to kick-off the document rendering after a git push.
14
+ * a tool that can execute scripts after a git push. *Jenkins* is used in this instruction. Some advantages over calling giblish directly from a git hook:
15
+ ** the document rendering is asynchronous to the git push from the client.
16
+ ** the script executed by Jenkins (the 'pipeline' in Jenkis lingo) can be stored and versioned in the same git repo as the documents to be rendered.
17
+ * a web server to publish the rendered html documents.
18
+
19
+ === Sequence for rendering documents
20
+
21
+ The following image shows how the sequence from user commit to rendered documents.
22
+
23
+ image::Render Documents.png[]
24
+
25
+ === Sequence for viewing documents
26
+
27
+ This is just the standard way of accessing a web site. The html page served by the web server will be the latest generated html page.
28
+
29
+ image::View Documents.png[]
30
+
31
+ == Server requirements
32
+
33
+ The server that will render the documents need the following tools installed:
34
+
35
+ * git
36
+ * giblish
37
+
38
+ The server that runs Jenkins will (of course) need Jenkins installed.
39
+
40
+ === Setup server for both Jenkins and doc rendering on Ubuntu 16.04
41
+
42
+ The easiest setup is to use the same server to run Jenkins and to render the documents. For an Ubuntu 16.04 installation, you can install the needed tools as follows:
43
+
44
+ * install git `sudo apt install git`
45
+ * install Jenkins See https://www.digitalocean.com/community/tutorials/how-to-install-jenkins-on-ubuntu-16-04[this doc].
46
+ * install giblish `sudo gem install giblish`
47
+
48
+
49
+ == server side git hooks
50
+
51
+ git supports hooks in several parts of the sequence of getting changes ommitted and pushed to a remote repo. For details on git hooks see https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks[this doc]. See <<post-update-hook>> An example of a `post-update` hook that triggers Jenkins
52
+
53
+
54
+ [appendix]
55
+ [[post-update-hook]]
56
+ == post-update hook
57
+
58
+ Below is an example of a `post-update` hook that triggers Jenkins jobs after a push to a git repo. This hook should be installed on the server side git repository to trigger Jenkins builds
59
+
60
+ .Example of a git hook triggering Jenkins builds
61
+ ----
62
+ include::../docgen/scripts/githook_examples/post-update.example[]
63
+ ----
64
+
65
+ == Jenkins pipeline script
66
+
67
+ Below is a very basic example of a Jenkins pipeline that triggers giblish to render html and pdf documents located in a specific directory in a git repository.
@@ -38,11 +38,8 @@ Gem::Specification.new do |spec|
38
38
  # Usage: spec.add_runtime_dependency "[gem name]", [[version]]
39
39
  spec.add_runtime_dependency "asciidoctor", "~>2.0", ">= 2.0.10"
40
40
  spec.add_runtime_dependency "asciidoctor-diagram", ["~> 1.5"]
41
- spec.add_runtime_dependency "asciidoctor-pdf", [">= 1.5.0.alpha.18"]
41
+ spec.add_runtime_dependency "asciidoctor-pdf", [">= 1.5.0.beta.6"]
42
42
  spec.add_runtime_dependency "git", "~> 1.3"
43
43
  spec.add_runtime_dependency "rouge", "~> 3.3"
44
- # needed by asciidoctor-pdf, see instructions at
45
- # https://github.com/asciidoctor/asciidoctor-pdf/releases
46
- # spec.add_runtime_dependency "prawn-svg", "~> 0.27.1"
47
- spec.add_runtime_dependency "prawn-svg", "~> 0.29.1"
44
+ spec.add_runtime_dependency "prawn-svg", "~> 0.29.1"
48
45
  end
@@ -215,10 +215,7 @@ class SearchDocTree
215
215
  private
216
216
 
217
217
  def get_uri_top
218
- if @input_data[:gitbranch]
219
- return @input_data[:referer][0,@input_data[:referer].rindex('/')]
220
- end
221
- return @input_data[:referer].chomp('/')
218
+ return @input_data[:referer][0,@input_data[:referer].rindex('/')]
222
219
  end
223
220
 
224
221
  def wash_line line
@@ -239,6 +236,8 @@ class SearchDocTree
239
236
  # ...
240
237
  def format_search_adoc index,uri_top
241
238
  str = ""
239
+ # debug print referer...
240
+ # str << "uri_top: #{uri_top}\n"
242
241
  index.each do |file_info|
243
242
  filename = Pathname.new(file_info["filepath"]).basename
244
243
  str << "== #{file_info["title"]}\n\n"
@@ -313,7 +312,7 @@ def cgi_main cgi
313
312
  ignorecase: cgi.has_key?("ignorecase"),
314
313
  useregexp: cgi.has_key?("useregexp"),
315
314
  doc_root_abs: Pathname.new(cgi["topdir"]),
316
- referer_rel_top: Pathname.new("/#{cgi["reltop"]}"),
315
+ referer_rel_top: Pathname.new("#{cgi["reltop"]}"),
317
316
  referer: cgi.referer,
318
317
  uri_path: URI(cgi.referer).path,
319
318
  client_css: cgi["css"],
@@ -328,17 +327,16 @@ def cgi_main cgi
328
327
  #
329
328
  # if the source was rendered from a git branch, the paths
330
329
  # search_assets = <index_dir>/../search_assets/<branch_name>/
331
- # styles_dir = ../web_assets/css
330
+ # styles_top = ../web_assets/css
332
331
  #
333
332
  # and if not, the path is
334
333
  # search_assets = <index_dir>/search_assets
335
- # styles_dir = ./web_assets/css
334
+ # styles_top = /web_assets/css
335
+ # <link rel="stylesheet" href="/web_assets/css/virodoc.css">
336
336
  #
337
- # The styles dir shall be a relative path
338
337
  if input_data[:doc_root_abs].join("./search_assets").exist?
339
338
  # this is not from a git branch
340
339
  input_data[:search_top] = input_data[:doc_root_abs].join("./search_assets")
341
- # input_data[:styles_top] = Pathname.new(input_data[:uri_path]).join("./web_assets/css")
342
340
  input_data[:styles_top] = Pathname.new(input_data[:referer_rel_top]).join("web_assets/css")
343
341
  input_data[:gitbranch] = false
344
342
  elsif input_data[:doc_root_abs].join("../search_assets").exist?
@@ -350,21 +348,53 @@ def cgi_main cgi
350
348
  raise ScriptError, "Could not find search_assets dir!"
351
349
  end
352
350
 
353
- # use a relative stylesheet (same as the index page was rendered with)
354
- adoc_options = {
351
+ # Set some reasonable default attributes and options
352
+ adoc_attributes = {
355
353
  "data-uri" => 1,
356
- "linkcss" => 1,
357
- "stylesdir" => input_data[:styles_top].to_s,
358
- "stylesheet" => input_data[:client_css],
359
- "copycss!" => 1
360
354
  }
361
355
 
356
+ converter_options = {
357
+ backend: "html5",
358
+ # need this to let asciidoctor include the default css if user
359
+ # has not specified any css
360
+ safe: Asciidoctor::SafeMode::SAFE,
361
+ header_footer: true,
362
+ attributes: adoc_attributes
363
+ }
364
+
365
+ # use a relative stylesheet (same as the index page was rendered with)
366
+ # if the script has received input in the client_css form field
367
+ if !input_data[:client_css].nil? && !input_data[:client_css].empty?
368
+ css_path = if input_data[:styles_top].to_s[0] != '/'
369
+ "/" + input_data[:styles_top].to_s
370
+ else
371
+ input_data[:styles_top].to_s
372
+ end
373
+ adoc_attributes.merge!({
374
+ "linkcss" => 1,
375
+ "stylesdir" => css_path,
376
+ "stylesheet" => input_data[:client_css],
377
+ "copycss!" => 1
378
+ })
379
+ end
380
+
362
381
  # search the docs and render html
363
382
  sdt = SearchDocTree.new(input_data)
364
383
  docstr = sdt.search
365
384
 
385
+ # used for debug purposes
386
+ # docstr = <<~EOF
387
+ #
388
+ # #{input_data[:referer_rel_top]} is branch: #{input_data[:gitbranch]}
389
+ #
390
+ # #{adoc_attributes.to_s}
391
+ #
392
+ #
393
+ # #{sdt.search}
394
+ # EOF
395
+
366
396
  # send the result back to the client
367
- print Asciidoctor.convert docstr, header_footer: true, attributes: adoc_options
397
+ print Asciidoctor.convert(docstr, converter_options)
368
398
  end
369
399
 
370
400
  # assume that the file tree looks like this when running
@@ -410,10 +440,14 @@ end
410
440
 
411
441
  # Usage:
412
442
  # to start a local web server for development work
413
- # giblish-giblish-search.rb <web_root>
443
+ # giblish-search.rb <web_root>
444
+ #
445
+ # to run as a cgi script via a previously setup web server
446
+ # giblish-search.rb
414
447
  #
415
- # to run as a cgi script via a previously setup web server:
416
- # giblish-giblish-search.rb
448
+ # (note that you might need to rename the script to eg
449
+ # giblish-search.cgi or similar depending on your web server
450
+ # setup)
417
451
  #
418
452
  if __FILE__ == $PROGRAM_NAME
419
453
 
@@ -425,7 +459,6 @@ if __FILE__ == $PROGRAM_NAME
425
459
  cgi = CGI.new
426
460
  print cgi.header
427
461
  begin
428
- # hello_world
429
462
  cgi_main cgi
430
463
  rescue Exception => e
431
464
  print e.message
@@ -26,17 +26,20 @@ module Giblish
26
26
  @next_id = 0
27
27
  @processed_docs = processed_docs
28
28
  @paths = paths
29
- @options = options.dup
30
- @extension = options.key?(:extension) ? options[:extension] : "html"
29
+ @converter_options = options.dup
30
+ # @options = options.dup
31
+ @extension = @converter_options.key?(:extension) ? options[:extension] : "html"
31
32
  @docid_cache = DocidCollector.docid_cache
32
33
  @docid_deps = DocidCollector.docid_deps
33
34
  @dep_graph = build_dep_graph
34
35
  end
35
36
 
36
37
  # get the asciidoc source for the document.
37
- def source
38
+ def source(make_searchable = false)
38
39
  <<~DOC_STR
39
40
  #{generate_header}
41
+ #{add_search_box if make_searchable}
42
+ #{generate_graph_header}
40
43
  #{generate_labels}
41
44
  #{generate_deps}
42
45
  #{generate_footer}
@@ -58,15 +61,9 @@ module Giblish
58
61
  result
59
62
  end
60
63
 
61
- def generate_header
64
+ def generate_graph_header
62
65
  t = Time.now
63
66
  <<~DOC_STR
64
- = Document-id reference graph
65
- from #{@paths.src_root_abs}
66
-
67
- Generated by Giblish at::
68
- #{t.strftime('%Y-%m-%d %H:%M')}
69
-
70
67
  Below is a graph that visualizes what documents (by doc-id) a specific
71
68
  document references.
72
69
 
@@ -84,6 +81,18 @@ module Giblish
84
81
  DOC_STR
85
82
  end
86
83
 
84
+ def generate_header
85
+ t = Time.now
86
+ <<~DOC_STR
87
+ = Document-id reference graph
88
+ from #{@paths.src_root_abs}
89
+
90
+ Generated by Giblish at::
91
+ #{t.strftime('%Y-%m-%d %H:%M')}
92
+
93
+ DOC_STR
94
+ end
95
+
87
96
  def generate_footer
88
97
  <<~DOC_STR
89
98
  }
@@ -91,6 +100,15 @@ module Giblish
91
100
  DOC_STR
92
101
  end
93
102
 
103
+ def add_search_box
104
+ # TODO: Fix the hard-coded path
105
+ Giblish::generate_search_box_html(
106
+ @converter_options[:attributes]["stylesheet"],
107
+ "/cgi-bin/giblish-search.cgi",
108
+ @paths
109
+ )
110
+ end
111
+
94
112
  def make_dot_entry(doc_dict, info)
95
113
  # split title into multiple rows if it is too long
96
114
  line_length = 15
@@ -58,33 +58,13 @@ module Giblish
58
58
 
59
59
  def add_search_box
60
60
  # TODO: Fix the hard-coded path
61
- cgi_path = "/cgi-bin/giblish-search.cgi"
62
- css = @converter.converter_options[:attributes]["stylesheet"]
63
-
64
- # button with magnifying glass icon (not working when deployed)
65
- # <button id="search" type="submit"><i class="fa fa-search"></i></button>
66
- <<~SEARCH_INFO
67
- ++++
68
- <form class="example" action="#{cgi_path}" style="margin:20px 0px 20px 0px;max-width:380px">
69
- Search all documents:
70
- <input id="searchphrase" type="text" placeholder="Search.." name="searchphrase"/>
71
- <button id="search" type="submit">Search</button>
72
- <br>
73
-
74
- <input id="ignorecase" type="checkbox" value="true" name="ignorecase" checked/>
75
- <label for="ignorecase">Ignore Case</label>
76
- &nbsp;&nbsp;
77
- <input id="useregexp" type="checkbox" value="true" name="regexp"/>
78
- <label for="useregexp">Use Regexp</label>
79
-
80
- <input type="hidden" name="topdir" value="#{@paths.dst_root_abs.to_s}"</input>
81
- <input type="hidden" name="reltop" value="#{@paths.reldir_from_web_root(@paths.dst_root_abs)}"</input>
82
- <input type="hidden" name="css" value="#{css}"</input>
83
- </form>
84
- ++++
85
-
86
- SEARCH_INFO
61
+ Giblish::generate_search_box_html(
62
+ @converter.converter_options[:attributes]["stylesheet"],
63
+ "/cgi-bin/giblish-search.cgi",
64
+ @paths
65
+ )
87
66
  end
67
+
88
68
  def get_docid_statistics
89
69
  largest = ""
90
70
  clash = []
@@ -69,38 +69,53 @@ module Giblish
69
69
  # build index and other fancy stuff if not suppressed
70
70
  unless @options[:suppressBuildRef]
71
71
  # build a dependency graph (only if we resolve docids...)
72
- dep_graph_exist = if @options[:resolveDocid]
73
- if Giblish::GraphBuilderGraphviz.supported
74
- gb = Giblish::GraphBuilderGraphviz.new @processed_docs, @paths, {extension: @converter.converter_options[:fileext]}
75
- @converter.convert_str(gb.source, @paths.dst_root_abs, "graph")
76
- else
77
- Giblog.logger.warn { "Lacking access to needed tools for generating a visual dependency graph." }
78
- Giblog.logger.warn { "The dependency graph will not be generated !!" }
79
- false
80
- end
81
- else
82
- false
83
- end
72
+ dep_graph_exist = @options[:resolveDocid] && build_graph_page
84
73
 
85
74
  # build a reference index
86
- adoc_logger = Giblish::AsciidoctorLogger.new Logger::Severity::WARN
87
- ib = index_factory
88
- @converter.convert_str(
89
- ib.source(
90
- dep_graph_exist,@options[:make_searchable]
91
- ),
92
- @paths.dst_root_abs, "index",
93
- logger: adoc_logger)
94
-
95
- # clean up cached files and adoc resources
96
- remove_diagram_temps if dep_graph_exist
97
- GC.start
75
+ build_index_page(dep_graph_exist)
98
76
  end
99
77
  conv_error
100
78
  end
101
79
 
102
80
  protected
103
81
 
82
+ def build_graph_page
83
+ if Giblish::GraphBuilderGraphviz.supported
84
+ # gb = Giblish::GraphBuilderGraphviz.new @processed_docs, @paths, {extension: @converter.converter_options[:fileext]}
85
+ gb = Giblish::GraphBuilderGraphviz.new @processed_docs, @paths, @converter.converter_options
86
+ errors = @converter.convert_str(
87
+ gb.source(
88
+ @options[:make_searchable]
89
+ ),
90
+ @paths.dst_root_abs,
91
+ "graph"
92
+ )
93
+ remove_diagram_temps unless errors
94
+ !errors
95
+ else
96
+ Giblog.logger.warn { "Lacking access to needed tools for generating a visual dependency graph." }
97
+ Giblog.logger.warn { "The dependency graph will not be generated !!" }
98
+ false
99
+ end
100
+ end
101
+
102
+ def build_index_page(dep_graph_exist)
103
+ # build a reference index
104
+ adoc_logger = Giblish::AsciidoctorLogger.new Logger::Severity::WARN
105
+ ib = index_factory
106
+ @converter.convert_str(
107
+ ib.source(
108
+ dep_graph_exist,@options[:make_searchable]
109
+ ),
110
+ @paths.dst_root_abs,
111
+ "index",
112
+ logger: adoc_logger
113
+ )
114
+
115
+ # clean up cached files and adoc resources
116
+ GC.start
117
+ end
118
+
104
119
  # get the correct index builder type depending on supplied
105
120
  # user options
106
121
  def index_factory
@@ -39,8 +39,11 @@ module Giblish
39
39
  # the user if applicable
40
40
  @converter_options[:attributes] = DEFAULT_ATTRIBUTES.dup
41
41
  @converter_options[:attributes].merge!(options[:attributes]) unless options[:attributes].nil?
42
-
43
42
  @converter_options[:backend] = options[:backend]
43
+
44
+ # give derived classes the opportunity to add options and attributes
45
+ add_backend_options(@converter_options)
46
+ add_backend_attributes(@converter_options[:attributes])
44
47
  end
45
48
 
46
49
  # Public: Convert one single adoc file using the specific conversion
@@ -56,23 +59,18 @@ module Giblish
56
59
 
57
60
  Giblog.logger.info {"Processing: #{filepath}"}
58
61
 
59
- # create an asciidoc doc object and convert to requested
60
- # output using current conversion options
61
- @converter_options[:to_dir] = @paths.adoc_output_dir(filepath).to_s
62
- @converter_options[:base_dir] =
63
- Giblish::PathManager.closest_dir(filepath).to_s
64
- @converter_options[:to_file] =
65
- Giblish::PathManager.get_new_basename(filepath,
66
- @converter_options[:fileext])
62
+ # update the relevant options for each specific document
63
+ set_common_doc_specific_options(filepath,logger)
67
64
 
68
- # set a specific logger instance to-be-used by asciidoctor
69
- @converter_options[:logger] = logger unless logger.nil?
65
+ # give derived classes the opportunity to set doc specific attributes
66
+ add_doc_specific_attributes(filepath,true, @converter_options[:attributes])
70
67
 
71
68
  Giblog.logger.debug {"converter_options: #{@converter_options}"}
72
69
 
73
70
  # do the actual conversion
74
71
  doc = Asciidoctor.convert_file filepath, @converter_options
75
72
 
73
+ # bail out if asciidoctor failed to convert the doc
76
74
  if logger && logger.max_severity && logger.max_severity > Logger::Severity::WARN
77
75
  raise RuntimeError, "Failed to convert the file #{filepath}"
78
76
  end
@@ -88,6 +86,7 @@ module Giblish
88
86
  # Returns: whether any errors occured during conversion (true) or
89
87
  # not (false).
90
88
  def convert_str(src_str, dst_dir, basename,logger: nil)
89
+
91
90
  index_opts = @converter_options.dup
92
91
 
93
92
  # use the same options as when converting all docs
@@ -98,6 +97,10 @@ module Giblish
98
97
  index_opts[:base_dir] = dst_dir.to_s
99
98
  index_opts.delete_if {|k, _v| %i[to_file].include? k}
100
99
 
100
+ # give derived classes the opportunity to set doc specific attributes
101
+ index_filepath = dst_dir + "#{basename}.#{index_opts[:fileext]}"
102
+ add_doc_specific_attributes(index_filepath,false, index_opts[:attributes])
103
+
101
104
  # load and convert the document using the converter options
102
105
  doc = nil, output = nil
103
106
 
@@ -108,105 +111,176 @@ module Giblish
108
111
  doc = Asciidoctor.load src_str, index_opts
109
112
  output = doc.convert index_opts
110
113
 
111
- index_filepath = dst_dir + "#{basename}.#{index_opts[:fileext]}"
112
-
113
114
  if logger && logger.max_severity && logger.max_severity > Logger::Severity::WARN
114
115
  raise RuntimeError, "Failed to convert string to asciidoc!! Will _not_ generate #{index_filepath.to_s}"
115
116
  end
117
+
118
+ # write the converted document to an index file located at the
119
+ # destination root
120
+ doc.write output, index_filepath.to_s
116
121
  rescue Exception => e
117
122
  Giblog.logger.error(e)
118
123
  conv_error = true
119
124
  end
120
125
 
121
-
122
- # write the converted document to an index file located at the
123
- # destination root
124
- doc.write output, index_filepath.to_s
125
126
  conv_error
126
127
  end
127
128
 
128
129
  protected
129
130
 
130
- # Protected: Adds the supplied backend specific options and
131
- # attributes to the base ones.
132
- # The following options must be provided by the derived class:
133
- # :fileext - a string with the filename extention to use for the
134
- # generated file
131
+ # Hook for specific converters to inject their own options.
132
+ # The following options must be provided by the derived class:
133
+ # :fileext - a string with the filename extention to use for the
134
+ # generated file
135
135
  #
136
- # backend_opts - the options specific to the asciidoctor backend
137
- # that the derived class supports
138
- # backend_attribs - the attributes specific to the asciidoctor backend
139
- # that the derived class supports
140
- def add_backend_options(backend_opts, backend_attribs)
141
- @converter_options = @converter_options.merge(backend_opts)
142
- @converter_options[:attributes] =
143
- @converter_options[:attributes].merge(backend_attribs)
136
+ # backend_options - the option dict from the backend implementation
137
+ def add_backend_options(backend_options)
138
+ @converter_options.merge!(backend_options)
139
+ end
140
+
141
+ # Hook for specific converters to inject their own attributes
142
+ # valid for all conversions.
143
+ # backend_attributes - the attribute dict from the backend implementation
144
+ def add_backend_attributes(backend_attributes)
145
+ @converter_options[:attributes].merge!(backend_attributes)
146
+ end
147
+
148
+ # Hook for specific converters to inject attributes on a per-doc
149
+ # basis
150
+ def add_doc_specific_attributes(filepath, is_src, attributes)
151
+
152
+ end
153
+
154
+ private
155
+
156
+ def set_common_doc_specific_options(src_filepath,logger)
157
+ # create an asciidoc doc object and convert to requested
158
+ # output using current conversion options
159
+ @converter_options[:to_dir] = @paths.adoc_output_dir(src_filepath).to_s
160
+ @converter_options[:base_dir] =
161
+ Giblish::PathManager.closest_dir(src_filepath).to_s
162
+ @converter_options[:to_file] =
163
+ Giblish::PathManager.get_new_basename(src_filepath,
164
+ @converter_options[:fileext])
165
+ @converter_options[:logger] = logger unless logger.nil?
144
166
  end
145
167
  end
146
168
 
147
- # Converts asciidoc files to html5 output.
169
+ # Converts asciidoc files to html5 output.
148
170
  class HtmlConverter < DocConverter
149
171
  def initialize(paths, options)
150
172
  super paths, options
151
173
 
152
- # handle needed assets for the styling (css et al)
153
- html_attrib = setup_web_assets options[:webRoot]
174
+ # validate that things are ok on the resource front
175
+ # and copy if needed
176
+ @dst_asset_dir = @paths.dst_root_abs.join("web_assets")
177
+ validate_and_copy_resources @dst_asset_dir
154
178
 
155
- # Setting 'data-uri' makes asciidoctor embed images in the resulting
156
- # html file
157
- html_attrib["data-uri"] = 1
179
+ # convenience path to css dir
180
+ @dst_css_dir = @dst_asset_dir.join("css")
158
181
 
159
- # tell asciidoctor to use the html5 backend
160
- backend_options = {backend: "html5", fileext: "html"}
161
- add_backend_options backend_options, html_attrib
182
+ # identify ourselves as an html converter
183
+ add_backend_options({backend: "html5", fileext: "html"})
184
+ # setup the attributes specific for this converter
185
+ add_backend_attributes(get_common_attributes)
162
186
  end
163
187
 
164
- private
165
-
166
- def setup_stylesheet_attributes(css_dir)
167
- return {} if @paths.resource_dir_abs.nil?
188
+ protected
168
189
 
169
- # use the supplied stylesheet if there is one
170
- attrib = {"linkcss" => 1,
171
- "stylesdir" => css_dir,
172
- "stylesheet" => "giblish.css",
173
- "copycss!" => 1}
190
+ def add_doc_specific_attributes(filepath, is_src_file, attributes)
191
+ doc_attributes = {}
192
+ if @paths.resource_dir_abs and not @web_root
193
+ # user wants to use own styling without use of a
194
+ # web root. the correct css link is the relative path
195
+ # from the specific doc to the common css directory
196
+ css_rel_dir = if is_src_file
197
+ # the filepath is a src path
198
+ @paths.relpath_to_dir_after_generate(
199
+ filepath,
200
+ @dst_css_dir
201
+ )
202
+ else
203
+ # the given file path is the destination path of
204
+ # the generated file, find the relative path to the
205
+ # css dir
206
+ dst_dir = PathManager.closest_dir(filepath)
207
+ @dst_css_dir.relative_path_from(dst_dir)
208
+ end
209
+ doc_attributes["stylesdir"] = css_rel_dir.to_s
210
+ end
174
211
 
175
- # Make sure that a user supplied stylesheet ends with .css or .CSS
176
- @user_style &&
177
- attrib["stylesheet"] =
178
- /\.(css|CSS)$/ =~ @user_style ? @user_style : "#{@user_style}.css"
179
- Giblog.logger.debug {"stylesheet attributes: #{attrib}"}
180
- attrib
212
+ attributes.merge!(doc_attributes)
181
213
  end
182
214
 
183
- # make sure that linked assets are available at dst_root
184
- def setup_web_assets(html_dir_root = nil)
185
- # only set this up if user has specified a resource dir
186
- return {} unless @paths.resource_dir_abs
215
+ private
216
+
217
+ def get_common_attributes
218
+ # Setting 'data-uri' makes asciidoctor embed images in the resulting
219
+ # html file
220
+ html_attrib = {
221
+ "data-uri" => 1,
222
+ }
187
223
 
188
- # create dir for web assets directly under dst_root
189
- assets_dir = "#{@paths.dst_root_abs}/web_assets"
190
- Dir.exist?(assets_dir) || FileUtils.mkdir_p(assets_dir)
224
+ if @paths.resource_dir_abs
225
+ # user wants to use own styling, set common attributes
226
+ html_attrib.merge!(
227
+ {
228
+ "linkcss" => 1,
229
+ "stylesheet" => @user_style ||= "giblish.css",
230
+ "copycss!" => 1
231
+ }
232
+ )
233
+
234
+ # check if user wants to use a web root path
235
+ @web_root = @paths.web_root_abs
236
+
237
+ if @web_root
238
+ # if user requested a web root to be used, the correct
239
+ # css link is the relative path from the web root to the
240
+ # css file. This is true for all documents
241
+ wr_rel = @dst_css_dir.relative_path_from @web_root
242
+ Giblog.logger.info {"Relative web root: #{wr_rel}"}
243
+ html_attrib["stylesdir"] = "/" << wr_rel.to_s
244
+ end
245
+ end
246
+ html_attrib
247
+ end
191
248
 
192
- # copy needed assets
249
+ def copy_resource_dir(dst_dir)
250
+ # create assets_dir and copy everything in the resource dir
251
+ # to the destination
252
+ Dir.exist?(dst_dir) || FileUtils.mkdir_p(dst_dir)
253
+
254
+ # copy all subdirs that exist in the source tree to the
255
+ # dst tree
193
256
  %i[css fonts images].each do |dir|
194
257
  src = "#{@paths.resource_dir_abs}/#{dir}"
195
- Dir.exist?(src) && FileUtils.copy_entry(src, "#{assets_dir}/#{dir}")
258
+ Dir.exist?(src) && FileUtils.copy_entry(src, "#{dst_dir}/#{dir}")
196
259
  end
260
+ end
197
261
 
198
- # find the path to the assets dir that is correct when called from a url,
199
- # taking the DirectoryRoot for the web site into consideration.
200
- if html_dir_root
201
- wr = Pathname.new(
202
- assets_dir
203
- ).relative_path_from Pathname.new(html_dir_root)
204
- Giblog.logger.info {"Relative web root: #{wr}"}
205
- assets_dir = "/" << wr.to_s
262
+ # make as sure as we can that the user has given a
263
+ # directory with valid resources
264
+ def validate_and_copy_resources(dst_dir)
265
+ # we don't have a resource path, which is fine, use
266
+ # defaults
267
+ return nil unless @paths.resource_dir_abs
268
+
269
+ # If user has requested the use of a specific css, use that,
270
+ # otherwise use asciidoctor default css
271
+ if @user_style
272
+ # Make sure that a user supplied stylesheet ends with .css or .CSS
273
+ @user_style && @user_style =
274
+ /\.(css|CSS)$/ =~ @user_style ? @user_style : "#{@user_style}.css"
275
+
276
+ # bail out if we can not find the given css file
277
+ src_css_path = @paths.resource_dir_abs.
278
+ join("css").join(Pathname.new(@user_style))
279
+ raise RuntimeError, "Could not find the specified " +
280
+ "css file at: #{src_css_path}" unless src_css_path.exist?
206
281
  end
207
282
 
208
- Giblog.logger.info {"stylesheet dir: #{assets_dir}"}
209
- setup_stylesheet_attributes "#{assets_dir}/css"
283
+ copy_resource_dir dst_dir
210
284
  end
211
285
  end
212
286
 
@@ -214,10 +288,10 @@ module Giblish
214
288
  def initialize(paths, options)
215
289
  super paths, options
216
290
 
217
- pdf_attrib = setup_pdf_attribs
218
-
219
- backend_options = {backend: "pdf", fileext: "pdf"}
220
- add_backend_options backend_options, pdf_attrib
291
+ # identify ourselves as a pdf converter
292
+ add_backend_options({backend: "pdf", fileext: "pdf"})
293
+ # setup the attributes specific for this converter
294
+ add_backend_attributes(setup_pdf_attribs)
221
295
  end
222
296
 
223
297
  private
@@ -25,7 +25,7 @@ module Giblish
25
25
  # parent
26
26
  # message
27
27
  def file_log(filename)
28
- o, e, s = exec_cmd("log", %w[--follow --date=iso --], filename)
28
+ o, e, s = exec_cmd("log", %w[--follow --date=iso --], "'#{filename}'")
29
29
  raise "Failed to get git log for #{filename}!!\n#{e}" if s.exitstatus != 0
30
30
 
31
31
  process_log_output(o)
@@ -86,6 +86,7 @@ module Giblish
86
86
  attr_reader :src_root_abs
87
87
  attr_reader :dst_root_abs
88
88
  attr_reader :resource_dir_abs
89
+ attr_reader :web_root_abs
89
90
 
90
91
  # Public:
91
92
  #
@@ -95,7 +96,7 @@ module Giblish
95
96
  # tree
96
97
  # resource_dir - a string or pathname with the directory containing
97
98
  # resources
98
- def initialize(src_root, dst_root, resource_dir = nil, web_root = false)
99
+ def initialize(src_root, dst_root, resource_dir = nil, web_root = nil)
99
100
  # Make sure that the source root exists in the file system
100
101
  @src_root_abs = Pathname.new(src_root).realpath
101
102
  self.dst_root_abs = dst_root
@@ -104,7 +105,11 @@ module Giblish
104
105
  resource_dir && (@resource_dir_abs = Pathname.new(resource_dir).realpath)
105
106
 
106
107
  # Set web root if given by user
107
- @web_root_abs = web_root ? Pathname.new(web_root) : nil
108
+ @web_root_abs = nil
109
+ if web_root
110
+ web_root = "/" + web_root unless web_root[0] == '/'
111
+ @web_root_abs = web_root ? Pathname.new(web_root) : nil
112
+ end
108
113
  end
109
114
 
110
115
  def dst_root_abs=(dst_root)
@@ -115,26 +120,72 @@ module Giblish
115
120
  end
116
121
 
117
122
  # Public: Get the relative path from the source root dir to the
118
- # source file dir.
123
+ # directory where the supplied path points.
119
124
  #
120
- # in_path - an absolute or relative path
125
+ # in_path - an absolute or relative path to a file or dir
121
126
  def reldir_from_src_root(in_path)
122
- p = in_path.is_a?(Pathname) ? in_path : Pathname.new(in_path)
127
+ p = self.class.closest_dir in_path
128
+ p.relative_path_from(@src_root_abs)
129
+ end
123
130
 
124
- # Get absolute source dir path
125
- src_abs = p.directory? ? p.realpath : p.dirname.realpath
131
+ # Public: Get the relative path from the
132
+ # directory where the supplied path points to
133
+ # the src root dir
134
+ #
135
+ # path - an absolute or relative path to a file or dir
136
+ def reldir_to_src_root(path)
137
+ src = self.class.closest_dir path
138
+ @src_root_abs.relative_path_from(src)
139
+ end
126
140
 
127
- # Get relative path from source root dir
128
- src_abs.relative_path_from(@src_root_abs)
141
+ # Public: Get the relative path from the dst root dir to the
142
+ # directory where the supplied path points.
143
+ #
144
+ # path - an absolute or relative path to a file or dir
145
+ def reldir_from_dst_root(path)
146
+ dst = self.class.closest_dir path
147
+ dst.relative_path_from(@dst_root_abs)
129
148
  end
130
149
 
131
- def reldir_from_web_root(in_path)
132
- p = in_path.is_a?(Pathname) ? in_path : Pathname.new(in_path)
133
- return p if @web_root_abs.nil?
150
+ # Public: Get the relative path from the
151
+ # directory where the supplied path points to
152
+ # the dst root dir
153
+ #
154
+ # path - an absolute or relative path to a file or dir
155
+ def reldir_to_dst_root(path)
156
+ dst = self.class.closest_dir path
157
+ @dst_root_abs.relative_path_from(dst)
158
+ end
159
+
160
+ # return the destination dir corresponding to the given src path
161
+ # the src path must exist in the file system
162
+ def dst_abs_from_src_abs(src_path)
163
+ src_abs = (self.class.to_pathname src_path).realpath
164
+ src_rel = reldir_from_src_root src_abs
165
+ @dst_root_abs.join(src_rel)
166
+ end
134
167
 
168
+ # return the relative path from a generated document to
169
+ # the supplied folder given the corresponding absolute source
170
+ # file path
171
+ def relpath_to_dir_after_generate(src_filepath,dir_path)
172
+ dst_abs = dst_abs_from_src_abs(src_filepath)
173
+ dir = self.class.to_pathname(dir_path)
174
+ dir.relative_path_from(dst_abs)
175
+ end
176
+
177
+ def reldir_from_web_root(path)
178
+ p = self.class.closest_dir path
179
+ return p if @web_root_abs.nil?
135
180
  p.relative_path_from(@web_root_abs)
136
181
  end
137
182
 
183
+ def reldir_to_web_root(path)
184
+ p = self.class.closest_dir path
185
+ return p if @web_root_abs.nil?
186
+ @web_root_abs.relative_path_from(p)
187
+ end
188
+
138
189
  def adoc_output_file(infile_path, extension)
139
190
  # Get absolute source dir path
140
191
  src_dir_abs = self.class.closest_dir infile_path
@@ -173,6 +224,12 @@ module Giblish
173
224
  dst_abs.relative_path_from(src_abs)
174
225
  end
175
226
 
227
+ # return a pathname, regardless if the given path is a Pathname or
228
+ # a string
229
+ def self.to_pathname(path)
230
+ path.is_a?(Pathname) ? path : Pathname.new(path)
231
+ end
232
+
176
233
  # Public: Get the basename for a file by replacing the file
177
234
  # extention of the source file with the supplied one.
178
235
  #
@@ -196,7 +253,7 @@ module Giblish
196
253
  # - the directory itself when called with an existing directory
197
254
  # - the parent dir when called with a non-existing file/directory
198
255
  def self.closest_dir(in_path)
199
- sr = in_path.is_a?(Pathname) ? in_path : Pathname.new(in_path)
256
+ sr = self.to_pathname(in_path)
200
257
  if sr.exist?
201
258
  sr.directory? ? sr.realpath : sr.dirname.realpath
202
259
  else
@@ -302,4 +359,40 @@ module Giblish
302
359
  module_function :which
303
360
 
304
361
 
362
+ # returns raw html that displays a search box to let the user
363
+ # acces the text search functionality.
364
+ #
365
+ # css - the name of the css file to use for the search box layout
366
+ # cgi_path - the path to a cgi script that implements the server side
367
+ # functionality of searching the text
368
+ # path_manager - an instance of the path manager class to keep track of all
369
+ # destinations.
370
+ def generate_search_box_html(css, cgi_path, paths)
371
+
372
+ # button with magnifying glass icon (not working when deployed)
373
+ # <button id="search" type="submit"><i class="fa fa-search"></i></button>
374
+ <<~SEARCH_INFO
375
+ ++++
376
+ <form class="example" action="#{cgi_path}" style="margin:20px 0px 20px 0px;max-width:380px">
377
+ Search all documents:
378
+ <input id="searchphrase" type="text" placeholder="Search.." name="searchphrase"/>
379
+ <button id="search" type="submit">Search</button>
380
+ <br>
381
+
382
+ <input id="ignorecase" type="checkbox" value="true" name="ignorecase" checked/>
383
+ <label for="ignorecase">Ignore Case</label>
384
+ &nbsp;&nbsp;
385
+ <input id="useregexp" type="checkbox" value="true" name="regexp"/>
386
+ <label for="useregexp">Use Regexp</label>
387
+
388
+ <input type="hidden" name="topdir" value="#{paths.dst_root_abs}"</input>
389
+ <input type="hidden" name="reltop" value="#{paths.reldir_from_web_root(paths.dst_root_abs)}"</input>
390
+ <input type="hidden" name="css" value="#{css}"</input>
391
+ </form>
392
+ ++++
393
+
394
+ SEARCH_INFO
395
+ end
396
+ module_function :generate_search_box_html
397
+
305
398
  end
@@ -1,3 +1,3 @@
1
1
  module Giblish
2
- VERSION = "0.5.2".freeze
2
+ VERSION = "0.6.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: giblish
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anders Rillbert
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-22 00:00:00.000000000 Z
11
+ date: 2019-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -106,14 +106,14 @@ dependencies:
106
106
  requirements:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
- version: 1.5.0.alpha.18
109
+ version: 1.5.0.beta.6
110
110
  type: :runtime
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
- version: 1.5.0.alpha.18
116
+ version: 1.5.0.beta.6
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: git
119
119
  requirement: !ruby/object:Gem::Requirement
@@ -181,6 +181,20 @@ files:
181
181
  - data/testdocs/wellformed/docidtest/docid_2.adoc
182
182
  - data/testdocs/wellformed/simple.adoc
183
183
  - data/testdocs/wellformed/source_highlighting/highlight_source.adoc
184
+ - docgen/resources/css/giblish.css
185
+ - docgen/resources/fonts/Ubuntu-B.ttf
186
+ - docgen/resources/fonts/Ubuntu-BI.ttf
187
+ - docgen/resources/fonts/Ubuntu-R.ttf
188
+ - docgen/resources/fonts/Ubuntu-RI.ttf
189
+ - docgen/resources/fonts/mplus1p-regular-fallback.ttf
190
+ - docgen/resources/images/giblish_logo.png
191
+ - docgen/resources/images/giblish_logo.svg
192
+ - docgen/resources/themes/giblish.yml
193
+ - docgen/scripts/Jenkinsfile
194
+ - docgen/scripts/githook_examples/post-update.example
195
+ - docs/setup_server.adoc
196
+ - docs/setup_server_assets/Render Documents.png
197
+ - docs/setup_server_assets/View Documents.png
184
198
  - exe/giblish
185
199
  - giblish.gemspec
186
200
  - lib/giblish-search.rb
@@ -198,15 +212,6 @@ files:
198
212
  - lib/giblish/pathtree.rb
199
213
  - lib/giblish/utils.rb
200
214
  - lib/giblish/version.rb
201
- - resources/css/giblish.css
202
- - resources/fonts/Ubuntu-B.ttf
203
- - resources/fonts/Ubuntu-BI.ttf
204
- - resources/fonts/Ubuntu-R.ttf
205
- - resources/fonts/Ubuntu-RI.ttf
206
- - resources/fonts/mplus1p-regular-fallback.ttf
207
- - resources/images/giblish_logo.png
208
- - resources/images/giblish_logo.svg
209
- - resources/themes/giblish.yml
210
215
  homepage: https://github.com/rillbert/giblish
211
216
  licenses:
212
217
  - MIT