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,163 @@
1
+ require_relative "config_utils"
2
+ require_relative "resourcepaths"
3
+ require_relative "docattr_providers"
4
+ require_relative "adocsrc_providers"
5
+ require_relative "subtreeinfobuilder"
6
+ require_relative "docid/docid"
7
+ require_relative "search/headingindexer"
8
+ require_relative "indexbuilders/depgraphbuilder"
9
+ require_relative "indexbuilders/subtree_indices"
10
+ require_relative "gitrepos/history_pb"
11
+
12
+ module Giblish
13
+ class HtmlLayoutConfig
14
+ attr_reader :pre_builders, :post_builders, :adoc_extensions, :adoc_api_opts, :docattr_providers
15
+
16
+ def initialize(config_opts)
17
+ @adoc_api_opts = {backend: "html"}
18
+ @pre_builders = []
19
+ @post_builders = []
20
+ @adoc_extensions = {}
21
+ @docattr_providers = []
22
+ case config_opts
23
+ in resource_dir:
24
+ # copy local resources to dst and link the generated html with
25
+ # the given css
26
+ @pre_builders << CopyResourcesPreBuild.new(config_opts)
27
+
28
+ # make sure generated html has relative link to the copied css
29
+ @docattr_providers << RelativeCssDocAttr.new(ResourcePaths.new(config_opts).dst_style_path_rel)
30
+ in server_css:
31
+ # do not copy any local resources, use the given web path to link to css
32
+ @docattr_providers << AbsoluteLinkedCss.new(config_opts.server_css)
33
+ else
34
+ 4 == 5 # workaround for bug in standardrb formatting
35
+ end
36
+
37
+ if config_opts.make_searchable
38
+ # enabling text search
39
+ search_provider = HeadingIndexer.new(config_opts.srcdir)
40
+ @adoc_extensions[:tree_processor] = search_provider
41
+ @post_builders << search_provider
42
+
43
+ # add search form to all docs
44
+ @adoc_extensions[:docinfo_processor] = AddSearchForm.new(config_opts.search_action_path)
45
+ end
46
+ end
47
+ end
48
+
49
+ class PdfLayoutConfig
50
+ attr_reader :pre_builders, :post_builders, :adoc_extensions, :adoc_api_opts, :docattr_providers
51
+
52
+ def initialize(config_opts)
53
+ @adoc_api_opts = {backend: "pdf"}
54
+ @pre_builders = []
55
+ @post_builders = []
56
+ @adoc_extensions = {}
57
+ @docattr_providers = []
58
+
59
+ unless config_opts.resource_dir.nil?
60
+ # generate pdf using asciidoctor-pdf with custom styling
61
+ rp = ResourcePaths.new(config_opts)
62
+ @docattr_providers << PdfCustomStyle.new(rp.src_style_path_abs, *rp.font_dirs_abs.to_a)
63
+ end
64
+ end
65
+ end
66
+
67
+ # configure all parts needed to execute the options specified by
68
+ # the user
69
+ class Configurator
70
+ attr_reader :build_options, :doc_attr, :config_opts
71
+
72
+ # config_opts:: a Cmdline::Options instance with config info
73
+ def initialize(config_opts)
74
+ @config_opts = config_opts
75
+ @build_options = {
76
+ pre_builders: [],
77
+ post_builders: [],
78
+ adoc_api_opts: {},
79
+ # add a hash where all values are initiated as empty arrays
80
+ adoc_extensions: Hash.new { |h, k| h[k] = [] }
81
+ }
82
+
83
+ # Initiate the doc attribute repo used during 'run-time'
84
+ @doc_attr = DocAttrBuilder.new(
85
+ GiblishDefaultDocAttribs.new
86
+ )
87
+
88
+ layout_config = case config_opts
89
+ in format: "html" then HtmlLayoutConfig.new(config_opts)
90
+ in format: "pdf" then PdfLayoutConfig.new(config_opts)
91
+ else
92
+ raise OptionParser::InvalidArgument, "The given cmd line flags are not supported: #{config_opts.inspect}"
93
+ end
94
+
95
+ # setup all options from the chosen layout configuration but
96
+ # override doc attributes with ones from the supplied configuration to
97
+ # ensure they have highest pref
98
+ @doc_attr.add_doc_attr_providers(
99
+ *layout_config.docattr_providers, CmdLineDocAttribs.new(config_opts)
100
+ )
101
+
102
+ setup_docid(config_opts, @build_options, @doc_attr)
103
+ setup_index_generation(config_opts, @build_options, @doc_attr)
104
+
105
+ # setup all pre,post, and build options
106
+ @build_options[:adoc_api_opts] = layout_config.adoc_api_opts
107
+ @build_options[:pre_builders] += layout_config.pre_builders
108
+ @build_options[:post_builders] += layout_config.post_builders
109
+ layout_config.adoc_extensions.each do |type, instance|
110
+ @build_options[:adoc_extensions][type] << instance
111
+ end
112
+
113
+ # add copy of asset dirs if options stipulates this
114
+ @build_options[:post_builders] << CopyAssetDirsPostBuild.new(@config_opts) unless @config_opts.copy_asset_folders.nil?
115
+ end
116
+
117
+ protected
118
+
119
+ def setup_index_generation(config_opts, build_options, doc_attr)
120
+ return if config_opts.no_index
121
+
122
+ # setup index generation
123
+ idx = SubtreeInfoBuilder.new(doc_attr, nil, SubtreeIndexBase, config_opts.index_basename)
124
+ build_options[:post_builders] << idx
125
+ end
126
+
127
+ def setup_docid(config_opts, build_options, doc_attr)
128
+ return unless config_opts.resolve_docid
129
+
130
+ # setup docid resolution
131
+ d = DocIdExtension::DocidPreBuilder.new
132
+ build_options[:pre_builders] << d
133
+ docid_pp = DocIdExtension::DocidProcessor.new({id_2_node: d.id_2_node})
134
+ build_options[:adoc_extensions][:preprocessor] << docid_pp
135
+
136
+ # early exit if user does not want indices
137
+ return if config_opts.no_index
138
+
139
+ # generate dep graph if graphviz is available
140
+ dg = DependencyGraphPostBuilder.new(docid_pp.node_2_ids, doc_attr, nil, nil, config_opts.graph_basename)
141
+ build_options[:post_builders] << dg
142
+ end
143
+ end
144
+
145
+ # swap standard index generation from the base class to ones including
146
+ # git history.
147
+ class GitRepoConfigurator < Configurator
148
+ def initialize(config_opts, git_repo_dir)
149
+ @git_repo_dir = git_repo_dir
150
+ config_opts.search_action_path ||= "/gibsearch.cgi"
151
+ super(config_opts)
152
+ end
153
+
154
+ protected
155
+
156
+ def setup_index_generation(config_opts, build_options, doc_attr)
157
+ return if config_opts.no_index
158
+
159
+ build_options[:post_builders] << AddHistoryPostBuilder.new(@git_repo_dir)
160
+ build_options[:post_builders] << SubtreeInfoBuilder.new(doc_attr, nil, SubtreeIndexGit, config_opts.index_basename)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,120 @@
1
+ require "pathname"
2
+
3
+ module Giblish
4
+ class FileHistory
5
+ # History info from git
6
+ LogEntry = Struct.new(:date, :author, :message, :sha1)
7
+
8
+ attr_accessor :history, :branch
9
+
10
+ def initialize(branch)
11
+ @branch = branch
12
+ @history = []
13
+ end
14
+ end
15
+
16
+ # Base class for bundling together data we always cache for
17
+ # each asciidoc file we come across.
18
+ #
19
+ # Users are expected to use the derived classes.
20
+ class ConversionInfo
21
+ attr_reader :converted, :src_node, :dst_node, :dst_top
22
+
23
+ # The relative Pathname from the root dir to the src doc
24
+ # Ex Pathname("my/subdir/file1.adoc")
25
+ attr_reader :src_rel_path
26
+
27
+ @@nof_missing_titles = 0
28
+
29
+ def initialize(converted:, src_node:, dst_node:, dst_top:)
30
+ @converted = converted
31
+ @src_node = src_node
32
+ @dst_node = dst_node
33
+ @dst_top = dst_top
34
+ @title = nil
35
+
36
+ @src_rel_path = dst_node.relative_path_from(dst_top).dirname / src_node.pathname.basename
37
+ end
38
+
39
+ # return:: a String with the basename of the source file
40
+ def src_basename
41
+ @src_node.pathname.basename.to_s
42
+ end
43
+
44
+ def title
45
+ return @title if @title
46
+
47
+ "NO TITLE FOUND (#{@@nof_missing_titles += 1}) !"
48
+ end
49
+
50
+ def docid
51
+ nil
52
+ end
53
+
54
+ def to_s
55
+ "Conversion #{@converted ? "succeeded" : "failed"} - src: #{@src_node.pathname} dst: #{@dst_node.pathname}"
56
+ end
57
+ end
58
+
59
+ # Provide data and access methods available when a conversion has
60
+ # succeeded
61
+ class SuccessfulConversion < ConversionInfo
62
+ attr_reader :stderr, :adoc
63
+
64
+ # The relative Pathname from the root dir to the dst file
65
+ # Ex Pathname("my/subdir/file1.html")
66
+ attr_reader :dst_rel_path
67
+
68
+ attr_reader :purpose_str
69
+
70
+ def initialize(src_node:, dst_node:, dst_top:, adoc:, adoc_stderr: "")
71
+ super(converted: true, src_node: src_node, dst_node: dst_node, dst_top: dst_top)
72
+
73
+ @adoc = adoc
74
+ @stderr = adoc_stderr
75
+ @title = @adoc.doctitle
76
+
77
+ @dst_rel_path = dst_node.relative_path_from(dst_top)
78
+
79
+ # Cach the purpose info if it exists
80
+ @purpose_str = get_purpose_info adoc
81
+ end
82
+
83
+ def docid
84
+ @adoc.attributes["docid"]
85
+ end
86
+
87
+ private
88
+
89
+ # TODO: Move this somewhere else
90
+ def get_purpose_info(adoc)
91
+ # Get the 'Purpose' section if it exists
92
+ purpose_str = +""
93
+ adoc.blocks.each do |section|
94
+ next unless section.is_a?(Asciidoctor::Section) &&
95
+ (section.level == 1) &&
96
+ (section.name =~ /^Purpose$/)
97
+
98
+ # filter out 'odd' text, such as lists etc...
99
+ section.blocks.each do |bb|
100
+ next unless bb.is_a?(Asciidoctor::Block)
101
+
102
+ purpose_str << "#{bb.source}\n+\n"
103
+ end
104
+ end
105
+ purpose_str
106
+ end
107
+ end
108
+
109
+ # Provide data and access methods available when a conversion has
110
+ # failed
111
+ class FailedConversion < ConversionInfo
112
+ attr_reader :error_msg
113
+
114
+ def initialize(src_node:, dst_node:, dst_top:, error_msg: nil)
115
+ super(converted: false, src_node: src_node, dst_node: dst_node, dst_top: dst_top)
116
+
117
+ @error_msg = error_msg
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,125 @@
1
+ module Giblish
2
+ class DocAttributesBase
3
+ def document_attributes(src_node, dst_node, dst)
4
+ raise NotImplementedError
5
+ end
6
+ end
7
+
8
+ class AbsoluteLinkedCss < DocAttributesBase
9
+ def initialize(css_path)
10
+ @css_path = Pathname.new(css_path)
11
+ end
12
+
13
+ def document_attributes(src_node, dst_node, dst_top)
14
+ {
15
+ "stylesdir" => @css_path.dirname.to_s,
16
+ "stylesheet" => @css_path.basename.to_s,
17
+ "linkcss" => true,
18
+ "copycss" => nil
19
+ }
20
+ end
21
+ end
22
+
23
+ class RelativeCssDocAttr < DocAttributesBase
24
+ # dst_css_path_rel:: the relative path to the dst top to the location of the
25
+ # css file to use
26
+ def initialize(dst_css_path_rel)
27
+ @css_path = Pathname.new(dst_css_path_rel)
28
+ end
29
+
30
+ def document_attributes(src_node, dst_node, dst_top)
31
+ href_path = dst_top.relative_path_from(dst_node).dirname / @css_path
32
+ {
33
+ "stylesdir" => href_path.dirname.to_s,
34
+ "stylesheet" => href_path.basename.to_s,
35
+ "linkcss" => true,
36
+ "copycss" => nil
37
+ }
38
+ end
39
+ end
40
+
41
+ class PdfCustomStyle < DocAttributesBase
42
+ # pdf_style_path:: the path name (preferable absolute) to the yml file
43
+ # pdf_font_dirs:: one or more Pathnames to each font dir that shall be
44
+ # checked for fonts
45
+ def initialize(pdf_style_path, *pdf_font_dirs)
46
+ @pdf_style_path = pdf_style_path
47
+ # one can specify multiple font dirs as:
48
+ # -a pdf-fontsdir="path/to/fonts;path/to/more-fonts"
49
+ # Always use the GEM_FONTS_DIR token to load the adoc-pdf gem's font dirs as well
50
+ @pdf_fontsdir = (Array(pdf_font_dirs) << "GEM_FONTS_DIR").collect { |d| d.to_s }&.join(";")
51
+ end
52
+
53
+ def document_attributes(src_node, dst_node, dst_top)
54
+ result = {
55
+ "pdf-style" => @pdf_style_path.basename.to_s,
56
+ "pdf-stylesdir" => @pdf_style_path.dirname.to_s,
57
+ "icons" => "font"
58
+ }
59
+ result["pdf-fontsdir"] = @pdf_fontsdir unless @pdf_fontsdir.nil?
60
+ result
61
+ end
62
+ end
63
+
64
+ # provides the default, opinionated doc attributes used
65
+ # when nothing else is set
66
+ class GiblishDefaultDocAttribs
67
+ def document_attributes(src_node, dst_node, dst_top)
68
+ {
69
+ "source-highlighter" => "rouge",
70
+ "rouge-css" => "style",
71
+ "rouge-style" => "github",
72
+ "xrefstyle" => "short",
73
+ "icons" => "font"
74
+ }
75
+ end
76
+ end
77
+
78
+ class CmdLineDocAttribs
79
+ def initialize(cmd_opts)
80
+ @cmdline_attrs = cmd_opts.doc_attributes.dup
81
+ end
82
+
83
+ def document_attributes(src_node, dst_node, dst_top)
84
+ @cmdline_attrs
85
+ end
86
+ end
87
+
88
+ # a builder class that can be setup with one or more document
89
+ # attribute providers and then used as the sole doc attrib provider
90
+ # where the merged sum of all added providers will be presented.
91
+ #
92
+ # If more than one added provider set the same doc attrib, the last
93
+ # added has preference.
94
+ #
95
+ # Each added provider must conform to the itf defined in
96
+ # the DocAttributesBase class.
97
+ class DocAttrBuilder
98
+ attr_reader :providers
99
+
100
+ def initialize(*attr_providers)
101
+ @providers = []
102
+ add_doc_attr_providers(*attr_providers)
103
+ end
104
+
105
+ def add_doc_attr_providers(*attr_providers)
106
+ return if attr_providers.empty?
107
+
108
+ # check itf compliance of added providers
109
+ attr_providers.each do |ap|
110
+ unless ap.respond_to?(:document_attributes) &&
111
+ ap.method(:document_attributes).arity == 3
112
+ raise ArgumentError, "The supplied doc attribute provider of type: #{ap.class} did not conform to the interface"
113
+ end
114
+ end
115
+
116
+ @providers += attr_providers
117
+ end
118
+
119
+ def document_attributes(src_node, dst_node, dst_top)
120
+ result = {}
121
+ @providers.each { |p| result.merge!(p.document_attributes(src_node, dst_node, dst_top)) }
122
+ result
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,181 @@
1
+ require "asciidoctor"
2
+ require_relative "../pathtree"
3
+
4
+ module Giblish
5
+ module DocIdExtension
6
+ # Build a hash of {docid => src_node} that can be used to resolve
7
+ # doc id references to valid dst paths
8
+ class DocidPreBuilder
9
+ attr_accessor :id_2_node
10
+
11
+ # The minimum number of characters required for a valid doc id
12
+ ID_MIN_LENGTH = 2
13
+
14
+ # The maximum number of characters required for a valid doc id
15
+ ID_MAX_LENGTH = 10
16
+
17
+ # the regex used to find :docid: entries in the doc header
18
+ DOCID_REGEX = /^:docid: +(.*)$/
19
+
20
+ def initialize
21
+ @id_2_node = {}
22
+ end
23
+
24
+ # called during the pre-build phase
25
+ def on_prebuild(src_tree, dst_tree, converter)
26
+ src_tree.traverse_preorder do |level, src_node|
27
+ next unless src_node.leaf?
28
+
29
+ parse_node(src_node)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Check if a :docid: <id> entry exists in the header of the doc
36
+ # denoted by 'node'.
37
+ # According to http://www.methods.co.nz/asciidoc/userguide.html#X95
38
+ # the header is optional, but if it exists it:
39
+ # - must start with a titel (=+ <My Title>)
40
+ # - ends with one or more blank lines
41
+ # - does not contain any blank line
42
+ def parse_node(node)
43
+ p = node.pathname
44
+ Giblog.logger.debug { "parsing file #{p} for docid..." }
45
+ Giblish.process_header_lines_from_file(p) do |line|
46
+ next unless DOCID_REGEX.match(line)
47
+
48
+ # There is a docid defined, cache the path and doc id
49
+ id = Regexp.last_match(1).strip
50
+ Giblog.logger.debug { "found possible docid: #{id}" }
51
+ if doc_id_ok?(id, p)
52
+ @id_2_node[id] = node
53
+ else
54
+ Giblog.logger.error { "Invalid docid: #{id} in file #{p}, this will be ignored!" }
55
+ end
56
+ end
57
+ end
58
+
59
+ # make sure the id is within the designated length and
60
+ # does not contain a '#' symbol
61
+ def doc_id_ok?(doc_id, path)
62
+ if @id_2_node.key? doc_id
63
+ Giblog.logger.warn { "Found same doc id twice: (#{doc_id}). This id will be associated with '#{path}' and _not_ with '#{@id_2_node[id]}'." }
64
+ end
65
+ (doc_id.length.between?(ID_MIN_LENGTH, ID_MAX_LENGTH) && !doc_id.include?("#"))
66
+ end
67
+ end
68
+
69
+ # A preprocessor extension to the Asciidoctor engine that transforms all
70
+ # <<:docid:>> references found in the adoc source into the matching
71
+ # file reference.
72
+ #
73
+ # It requiers a populated 'id_2_node' with {docid => src_node} before
74
+ # the first invokation of the 'process' method via Asciidoctor.
75
+ #
76
+ # When running, it builds a publicly available 'node_2_ids' map.
77
+ class DocidProcessor < Asciidoctor::Extensions::Preprocessor
78
+ # {src_node => [referenced doc_id's]}
79
+ attr_reader :node_2_ids
80
+
81
+ # required options:
82
+ #
83
+ # id_2_node:: {docid => src_node }
84
+ def initialize(opts)
85
+ raise ArgumentError, "Missing required option: :id_2_node!" unless opts.key?(:id_2_node)
86
+
87
+ super(opts)
88
+ @id_2_node = opts[:id_2_node]
89
+
90
+ # init new keys in the hash with an empty array
91
+ @node_2_ids = Hash.new { |h, k| h[k] = [] }
92
+ end
93
+
94
+ # The regex that matches docid references in files
95
+ DOCID_REF_REGEX = /<<\s*:docid:\s*(.*?)>>/
96
+ PASS_MACRO_REGEX = /pass:\[.*\]/
97
+
98
+ # This hook is called by Asciidoctor once for each document _before_
99
+ # Asciidoctor processes the adoc content.
100
+ #
101
+ # It replaces references of the format <<:docid: ID-1234,Hello >> with
102
+ # references to a resolved relative path.
103
+ def process(document, reader)
104
+ # Add doc as a source dependency for doc ids
105
+ gib_data = document.attributes["giblish-info"]
106
+ if gib_data.nil?
107
+ Giblog.logger.error "The docid preprocessor did not find required info in the doc attribute. (Doc title: #{document.title}"
108
+ return reader
109
+ end
110
+
111
+ src_node = gib_data[:src_node]
112
+
113
+ # Convert all docid refs to valid relative refs
114
+ reader.lines.each do |line|
115
+ # remove commented lines
116
+ next if line.start_with?("//")
117
+
118
+ @node_2_ids[src_node] += parse_line(line, src_node)
119
+ end
120
+
121
+ # we only care for one ref to a specific target, remove duplicates
122
+ @node_2_ids[src_node] = @node_2_ids[src_node].uniq
123
+
124
+ # the asciidoctor engine wants the reader back
125
+ reader
126
+ end
127
+
128
+ private
129
+
130
+ # substitutes docid references with the corresponding relative path
131
+ # references.
132
+ #
133
+ # returns:: Array of found docid references
134
+ def parse_line(line, src_node)
135
+ refs = []
136
+ # remove all content within a 'pass:[]' macro from the parser
137
+ line.gsub!(PASS_MACRO_REGEX)
138
+
139
+ line.gsub!(DOCID_REF_REGEX) do |_m|
140
+ # parse the ref
141
+ target_id, section, display_str = parse_doc_id_ref(Regexp.last_match(1))
142
+ Giblog.logger.debug { "Found docid ref to #{target_id} in file: #{src_node.pathname}..." }
143
+
144
+ # make sure it exists in the cache
145
+ unless @id_2_node.key?(target_id)
146
+ Giblog.logger.warn { "Could not resolve ref to #{target_id} from file: #{src_node.pathname}..." }
147
+ break "<<UNKNOWN_DOC, Could not resolve doc id reference !!!>>"
148
+ end
149
+
150
+ # add the referenced doc id as a target dependency of this document
151
+ refs << target_id
152
+
153
+ # get the relative path from this file to the target file
154
+ target_node = @id_2_node[target_id]
155
+ rel_path = target_node.pathname.relative_path_from(src_node.pathname.dirname)
156
+
157
+ # return the resolved reference
158
+ "<<#{rel_path}##{section}#{display_str}>>"
159
+ end
160
+ refs
161
+ end
162
+
163
+ # input_str shall be the expression between
164
+ # <<:docid:<input_str>>> where the <input_str> is in the form
165
+ # <id>[#section][,display_str]
166
+ #
167
+ # returns an array with [id, section, display_str]
168
+ def parse_doc_id_ref(input_str)
169
+ ref, display_str = input_str.split(",").each(&:strip)
170
+ id, section = ref.split "#"
171
+
172
+ display_str = id.dup if display_str.nil?
173
+ display_str.prepend ","
174
+
175
+ section = "" if section.nil?
176
+
177
+ [id, section, display_str]
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,64 @@
1
+ require "git"
2
+ require_relative "../application"
3
+
4
+ module Giblish
5
+ # Generate documentation using giblish based on the supplied parameters
6
+ class GenerateFromRefs
7
+ STANDARD_GIBLISH_FLAGS = %W[-f html -m --copy-asset-folders _assets$ --server-search-path /cgi-bin/gibsearch.cgi]
8
+
9
+ # doc_repo_url:: the url of the repo hosting the docs to be generated. this repo will be cloned
10
+ # ref_regexp:: a regexp that both defines what git refs that trigger a document generation and what refs will be
11
+ # generated.
12
+ # clone_dir_parent:: path to the local directory under which the doc_repo_url will be cloned.
13
+ # clone_name:: the name of the local clone of the doc_repo_url
14
+ # doc_src_rel:: the relative path from the repo root to the directory where the docs reside
15
+ # doc_dst_abs:: the absolute path to the target location for the generated docs
16
+ # logger:: a ruby Logger instance that will receive log messages
17
+ def initialize(doc_repo_url, ref_regexp, clone_dir_parent, clone_name, giblish_args, doc_src_rel, doc_dst_abs, logger)
18
+ @doc_repo_url = doc_repo_url
19
+ @ref_regexp = ref_regexp
20
+ @giblish_args = STANDARD_GIBLISH_FLAGS
21
+ @giblish_args += giblish_args unless giblish_args.nil?
22
+ @doc_src_rel = doc_src_rel
23
+ @dstdir = doc_dst_abs
24
+ @logger = logger
25
+
26
+ @repo_root = clone(doc_repo_url, clone_dir_parent, clone_name)
27
+ end
28
+
29
+ # Generate documents from the git refs found in the GitHub
30
+ # webhook payload.
31
+ def docs_from_gh_webhook(github_hash)
32
+ # TODO Implement support for other refs than branches
33
+ ref = github_hash.fetch(:ref).sub("refs/heads/", "")
34
+ if ref.empty? || !(@ref_regexp =~ ref)
35
+ @logger&.info { "Ref '#{ref}' does not match the document generation trigger -> No document generation triggereed" }
36
+ return
37
+ end
38
+
39
+ generate_docs(ref)
40
+ end
41
+
42
+ private
43
+
44
+ def clone(doc_repo_url, dst_dir, clone_name)
45
+ p = Pathname.new(dst_dir).join(clone_name)
46
+
47
+ @logger&.info { "Cloning #{doc_repo_url} to #{p}..." }
48
+ repo = Git.clone(doc_repo_url, clone_name, path: dst_dir.to_s, logger: @logger)
49
+ repo.config("user.name", "Giblish Webhook Manager")
50
+ repo.config("user.email", "dummy@giblish.com")
51
+ @logger&.info { "Cloning done" }
52
+ p
53
+ end
54
+
55
+ def generate_docs(ref)
56
+ srcdir = @repo_root.join(@doc_src_rel)
57
+ args = @giblish_args + %W[-g #{@ref_regexp} #{srcdir} #{@dstdir}]
58
+
59
+ @logger&.info { "Generate docs using parameters: #{args}" }
60
+ # run giblish with all args
61
+ EntryPoint.run(args, @logger)
62
+ end
63
+ end
64
+ end