giblish 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 +59 -0
  6. data/README.adoc +261 -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 +50 -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 +180 -71
  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